‫تطبيق (أ)
‫لعبة المضرب و الطوب



لعبة المضرب و الطوب من أشهر الألعاب و أبسطها على الإطلاق. و العديد من المبرمجين قد قاموا بعمل تلك اللعبة. بل‬ ‫لا أبالغ حين أقول أن أغلب مبرمجي الألعاب قد قاموا بعمل هذه اللعبة مرة واحدة على الأقل كبداية لرحلتهم في برمجة الألعاب. و مع إختلاف الأفكار و الأشكال و‬ ‫الطرق و الأساليب إلا أن الأصل والفكرة واحدة. إنظر إلى تعدد صور اللعبة و الإضافات التي عليها رغم أن فكرتها واحدة:


فهل يمكننا أن نقوم بلعبة مماثلة بما قد عرفناه في الفصول السابقة ؟؟ ماذا تعلمنا في الفصول السابقة ؟؟ قد عرفنا‬ ‫كيف نقوم بإنشاء نافذة و عرفنا كيف نقوم برسم بعض الأشكال البسيطة و تلوينها و عمل تدريجات للألوان بمكتبة الرسوم‬ ‫المفتوحة. كما تعلمنا كيف نتحكم بالزمن و الوقت و نستطيع كذلك أن نعمل أشكالً متحركة. كما نستطيع أن نستقبل مدخلات لوحة المفاتيح و مدخلات المؤشر و نستطيع أن نتحكم بشكل و مكان المؤشر. و كل ذلك يكفي لعمل نموذج بسيط لتلك اللعبة. و‬ ‫سوف نبدأ بالتدريج في اللعبة خطوة خطوة حتى نهايتها.
‫إن فكرة اللعبة بسيطة للغاية. في اللعبة هناك مجموعة من الطوب في الأعلى و الهدف هو تحطيم كل هذا الطوب. و في‬ ‫الأسفل عندك مضرب يسير في الإتجاه الأفقي و هناك كرة تقوم بتحطيم الطوب و يجب على المضرب أن يمنع الكرة من أن تنزل‬ ‫فيصدها فتقوم الكرة بالرجوع ناحية الطوب مرة آخرى و تحطم بعض الطوب ثم تنزل إلى الأسفل فيضربها المضرب حتى لا ‫تسقط فترجع إلى الطوب و هكذا. و إذا سقطت الكرة يخسر اللاعب و إذا حطمت كل الطوب يكسب. هذا هو أساس اللعبة و يمكنك‬ ‫أن تقوم بإضافة بعض الأشياء مثل أن تضيف المزيد من الكرات أو المزيد من المحاولات أو تجعل بعض الطوب يتحطم بضربتان‬ ‫بدلًا من ضربة واحدة أو غير ذلك. و نحن نريد في البداية أن نقوم بالأساسي ثم إذا أردت إضافة بعض الأشياء الآخرى فلك ذلك.
و لنقم أولًا بترتيب أولوياتنا و عمل مخطط كروكي للعبة قبل الخوض في كتابة الكود. و يمكنك إعتبار هذا الكلام‬ ‫مجرد ما يدور في ذهن المبرمج حين يقوم بالتفكير في كتابة إحدى الألعاب. فأولًا مما تتكون اللعبة ؟؟ أو ما هي الكائنات التي عندنا ؟؟ تتكون من مضرب يمشي‬ ‫في الأسفل. إذاً نحن نحتاج إلى متغيرات تحمل مكان هذا المضرب. هل نحتاج إلى متغيران إحداهما لمحور السينات و الآخر لمحور‬ ‫الصادات ؟؟ لا لأن المضرب يسير في الإتجاه الفقي فقط و أما مكانه في محور الصادات فثابت لا يتغير. كيف سيتحرك هذا‬ ‫المضرب ؟؟ الأفضل تحريكه بمؤشر الفأرة. و بناءاً على ذلك فإن المتغير الذي يحمل مكان المضرب هو متغير مكان المؤشر في‬ ‫محور السينات. و لنقم الآن بوضع هذا الأمر في اللعبة ثم نفكر بعد ذلك في عمل الكرة. كل ما سنقوم بعمله في الكود هو إنشاء‬ ‫نافذة كما تعلمنا و نقوم بعمل الدالة التي ترصد تحرك المؤشر و تقوم بتحويل قيمة مكانه في محور السينات إلى الإحداثيات‬ ‫النسبية. و سنقوم بعمل دالة التوقيت و التي لن ترسم إلى مضرب في أسفل الشاشة حول النقطة التي يتحرك فيها المؤشر. و‬ ‫بالطبع سنقوم بإلغاء شكل المؤشر كما تعلمنا. و ها هو كود ما ذكرناه إلى الآن:
#include <GL/gl.h>
#include <GL/glut.h>
float Mx;
int H,W;
int Win;
void Mouse(int X, int Y)
{
Mx = (float)X/W*2-1;
}
void DispTimer(int V)
{
glutTimerFunc(10,DispTimer,0);
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_QUADS);
glColor3f(1,0,0);
glVertex2f(Mx-0.1,-0.87);
glColor3f(0,0,1);
glVertex2f(Mx-0.1,-0.93);
glColor3f(1,1,0);
glVertex2f(Mx+0.1,-0.93);
glColor3f(1,0,1);
glVertex2f(Mx+0.1,-0.87);
glEnd();
glFlush();
glutSwapBuffers();
}
void Resize(int Width, int Height)
{
W = Width;
H = Height;
glViewport(0,0,Width,Height);
}
void Keys(unsigned char K, int X, int Y)
{
if (K == 27)
glutDestroyWindow(Win);
}
void Dummy() {}
int main(int argc, char **argv)
{
glutInit(&argc,argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
Win = glutCreateWindow(“Brix”);
glutFullScreen();
glutSetCursor(GLUT_CURSOR_NONE);
glClearColor(0.9,0.5,0,1);
glutReshapeFunc(Resize);
glutPassiveMotionFunc(Mouse);
glutMotionFunc(Mouse);
glutDisplayFunc(Dummy);
glutKeyboardFunc(Keys);
glutTimerFunc(10,DispTimer,0);
glutMainLoop();
return 0;
}
‫هذا الكود هو عبارة عن برنامج يقوم برسم المضرب الذي يتحرك في الأسفل عن طريق مؤشر الفأرة. و إذا قمت‬ ‫بتشغيل هذا الكود فسوف يظهر لك مثل هذه الصورة:

‫و الآن لنشرح هذا الكود حتى لا يكون فيه أي غموض. و كانت هذه أول سطور في البرنامج:‬
float Mx;
int H,W;
int Win;
‫المتغير الأول ‪ Mx‬سنضع فيه مكان المؤشر في محور السينات ‪ X‬بالإحداثيات النسبية (راجع بداية الفصل الرابع) و‬ ‫سنستخدم هذا المتغير لكي نرسم المضرب حوله. و المتغير الثاني و الثالث ‪ H‬و ‪ W‬سنضع فيهما طول و عرض الشاشة و هذا‬ ‫لكي نقوم بتحويل إحداثيات البكسل إلى الإحداثيات النسبية. و أما المتغير الرابع ‪ Win‬فقد ذكرنا في الفصل الثالث أن دالة‬ ‫إنشاء النوافذ ‪ glutCreateWindow‬تقوم بإرجاع قيمة من نوع عدد صحيح و نحن نستعمل هذه القيمة و نأخذها عندنا‬ ‫في متغير حتى إذا أردنا أن نستدعي دالة إغلاق النافذة نمرر لها هذا الرقم (أو كان عندنا عدة نوافذ أنشأناها و نريد أن‬ ‫نتنقل بين النوافذ مثلاً). و المتغير ‪ Win‬هو الذي سنضع فيه هذا الرقم.‬
void Mouse(int X, int Y)
{
Mx = (float)X/W*2-1;
}
‫هذه الدالة هي الدالة التي سوف تستدعيها مكتبة جلوت إذا تحرك المؤشر و تمرر له مكانه بإحداثيات البكسل. و نحن‬ ‫هنا نقوم بتحويل إحداثيات المؤشر في محور السينات إلى إحداثيات نسبية و نضع مكان المؤشر في محور السينات في المتغير‬ ‫العام ‪.Mx‬‬
void DispTimer(int V)
{
glutTimerFunc(10,DispTimer,0);
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_QUADS);
glColor3f(1,0,0);
glVertex2f(Mx-0.1,-0.87);
glColor3f(0,0,1);
glVertex2f(Mx-0.1,-0.93);
glColor3f(1,1,0);
glVertex2f(Mx+0.1,-0.93);
glColor3f(1,0,1);
glVertex2f(Mx+0.1,-0.87);
glEnd();
glFlush();
glutSwapBuffers();
}
‫هذه هي الدالة التي تقوم بعمليات الرسمة و تحديثها كل 10 ميلي ثانية. في أول سطر في الدالة قمنا بإخبار‬ ‫مكتبة جلوت بأن تقوم بإستدعاء الدالة بعد 10 ميلي ثانية لكي تقوم بتحديث الرسمة في وقت ثابت (راجع الفصل الثاني). ثم‬ ‫قمنا بتنظيف الشاشة في دالة ‪) glClear‬راجع الفصل الأول). ثم قمنا برسم مضلع من أربعة أضلاع. و قمنا برسمه حول نقطة‬ ‫‪ .Mx‬إنظر إلى هذه الرسم التوضيحي الذي يوضح لك لماذا ذكرنا تلك الحداثيات في دالة ‪:glVertex2f
‬‬

‫فكما ترى , هذه هي الإحداثيات التي ذكرناها لدالة ‪ .glVertex2f‬فكما ترى فإن إحداثيات المضرب في محور الصادات ثابتة ‫على عكس محور السينات فإنه يعتمد على مكان المؤشر. و هذا ما يجعل المضرب يتحرك مع المؤشر. و تذكر أننا حين نريد أن‬ ‫نرسم مضلع من أربع أضلاع و نمرر لدالة ‪ glBegin‬الثابت ‪ GL_QUADS‬فإننا نقوم بإستدعاء دالة ‪glVertex2f‬‬ ‫أربع مرات و في كل مرة نذكر لها إحداثيات نقطة من نقاط هذا المضلع و ربما قمنا بتغيير لون هذه النقطة و مكتبة الرسوم‬ ‫المفتوحة بدورها تقوم بعمل تدريجات ألوان في المضلع حتى تصل النقاط المختلفة الألوان ببعضها.‬

void Resize(int Width, int Height)
{
W = Width;
H = Height;
glViewport(0,0,Width,Height);
}
‫هذه الدالة ستقوم مكتبة جلوت بإستدعائها حينما يتم تغيير حجم النافذة و قد قمنا بجعلها تضع المقاسات الجديدة في متغيرات عمومية. ثم بعد ذلك إستدعينا دالة ‪ glViewport‬حتى نعيد ضبط الإحداثيات النسبية على المساحة التي مررناها للدالة (راجع الفصل الرابع).
void Keys(unsigned char K, int X, int Y)
{
if (K == 27)
glutDestroyWindow(Win);
}
‫و هذه الدالة ستقوم مكتبة جلوت بإستدعائها حين يضغط المستخدم على زر له رقم في جدول آسكي (راجع الفصل‬ ‫الثالث). و في هذه الدالة قمنا بإختبار رقم الزر الذي تم تمريره للدالة فإذا كان 27 (رقم زر الـ ‪ (Escape‬فإننا نقوم‬ ‫بإغلاق النافذة و قد قمنا بتمرير المتغير ‪ Win‬الذي قلنا أنه يحمل رقم النافذة التي أنشأناها حتى ننهي البرنامج حين يضغط‬ ‫المستخدم على الزر ‪ .Escape‬و تذكر أننا نعمل على شاشة ‪ FullScreen‬لذلك يجب أن نوفر طريقة للغلق.‬
void Dummy() {}
int main(int argc, char **argv)
{
glutInit(&argc,argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
Win = glutCreateWindow(“Brix”);
glutFullScreen();
glutSetCursor(GLUT_CURSOR_NONE);
glClearColor(0.9,0.5,0,1);
glutReshapeFunc(Resize);
glutPassiveMotionFunc(Mouse);
glutMotionFunc(Mouse);
glutDisplayFunc(Dummy);
glutKeyboardFunc(Keys);
glutTimerFunc(10,DispTimer,0);
glutMainLoop();
return 0;
}
‫قمنا بإنشاء دالة فارغة و أسميناها ‪ Dummy‬و ذلك حتى تقوم مكتبة جلوت بإستدعائها حين يحدث شئ على النافذة‬ ‫يتطلب إعادة الرسم و نحن قد آثرنا ان نقوم بتحديث الرسم كل جزء محدد من الثانية فلسنا بحاجة لهذا الأمر و لكن بعض‬ ‫المترجمات تتعسف في هذا الأمر و ترغمك أن تقوم بعمل دالة لعادة الرسم (راجع الفصل الثاني). أما في الدالة الرئيسية فقد‬ ‫قمنا بإنشاء نافذة و وضعنا رقمها في المتغير العام ‪ Win‬ثم قمنا بإستدعاء دالة ‪ glutSetCursor‬لكي نقوم بإلغاء‬ ‫شكل المؤشر (السهم التقليدي). و قمنا بتحديد لون الخلفية في دالة ‪) glClearColor‬راجع الفصل الأول). و بعد ذلك‬ ‫أخبرنا مكتبة جلوت بأن تقوم بإستدعاء دالة ‪ Resize‬حين يتغير حجم النافذة و دالة ‪ Mouse‬حين يتحرك المؤشر. و قد‬ ‫ذكرناها مرتين حتى نخبر مكتبة جلوت بأن تستدعيها في حالة الضغط على أي زر و في حالة عدم الضغط على أي زر (راجع‬ ‫الفصل الرابع) و دالة ‪ Keys‬حين يضغط المستخدم على أي زر (راجع الفصل الثالث) ثم قمنا بتشغيل المؤقت و هو الدالة التي‬ ‫أسميناها ‪.DispTimer‬‬
‫الآن و بعد أن قمنا بعمل المضرب يتبقى عندنا الكرة و الطوب. و لنبدأ أولًا بالكرة. الكرة تتحرك في أي إتجاه لذلك‬ ‫سنعمل لها متغيرين واحد لمكانها في محور ‪ X‬و الآخر لمكانها في محور ‪ .Y‬إذاً سنقوم في دالة التوقيت برسم كرة عند قيمة‬ ‫المتغيرين الذان يحملان مكان الكرة. هذه القيمتين هل هما ثابتتين أم متغيريتين ؟؟ بالطبع متغيريتين إذ أنهما لو كانتا ثابتتين‬ ‫ستبدو الكرة ثابتة. لذلك يجب علينا أن نقوم بشئ يجعل قيمة المتغيرين حتى تبدوا الكرة و كأنها تتحرك. نفترض أن عندنا‬ ‫متغير إسمه ‪ Bx‬و ‪ By‬يعبران عن مكان الكرة. و سنفترض أن عندنا متغير يعبر عن إتجاه الكرة. بمعنى أن الكرة ربما تتحرك‬ ‫متجهه إلى أعلى يسار النافذة أو إلى أسفل يمينها أو غير ذلك. و هذا الإتجاه يتغير دائماً فمثلًا عندما تصطدم الكرة بالمضرب‬ ‫و قد كانت سائرة ناحية أسفل يمين الشاشة فإنها ترتد و تسير ناحية أعلى يمين الشاشة. كما أنها قد تصطدم بالجدار فإذا‬ ‫إصطدمت به ترتد في التجاه الآخر. لنقم بحفظ إتجاه الكرة الأفقي في متغير و إتجاهها الرأسي في متغير آخر و على حسب هذا‬ ‫المتغير يتم تغيير مكان الكرة في ‪ Bx‬و ‪ .By‬يمكن مثلًا أن نضع مثل هذا الكود في الدالة التي تستدعى كل 10 ميلي ثانية:‬
if (Dx == 1)
Bx += 0.01;
else
Bx -= 0.01;
if (Dy == 1)
By += 0.01;
else
By -= 0.01;
‫هذا كان مجرد كود إبتدائي لعمل تحريك للكرة حيث إعتبرنا أن هناك متغيرين لتحديد إتجاه الكرة و قد أسميناهم ‪ Dx‬و ‪Dy‬‬ ‫و إذا كان ‪ Dy‬بقيمة 1 فإن المتغير ‪ By‬سوف يزيد فهكذا ستسير الكرة إلى العلى. و إن لم يكن بقيمة 1 فإن الكرة تسير‬ ‫إلى الأسفل. و كذلك الحال في الإتجاه الأفقي. أما بالنسبة لتصادم الكرة بالجدران فيمكن أن نختبر قيمة ‪ Bx‬و ‪ By‬لكي نعرف مكان الكرة ثم نقوم بتغيير إتجاهها عن طريق تغيير قيمة ‪ Dy‬و ‪ .Dx‬بالطبع فإن الكرة إذا إصطدمت بأعلى النافذة أو بإحدى‬ ‫جوانبها فسوف ترتد أما إذا نزلت إلى الأسفل فإنها تسقط و يخسر اللاعب. كما أنها إذا إصطدمت بالمضرب فإنها ترتد إلى‬ ‫الأعلى. بالنسبة للإصطدام بالجدران يمكن كتابة مثل هذا الكود:
if (Bx > 1)
Dx = 0;
else if (Bx < -1)
Dx = 1;
if (By > 1)
Dy = 0;
‫حيث أن القيمة 1 بالنسبة لمحور السينات ‪ X‬تعني أقصى يمين النافذة و القيمة -1 تعني أقصى يسار النافذة و نحن قد قمنا‬ ‫بإختبار القيمة فإذا تجاوزت تلك الحدود نقوم بتغيير الإتجاه. و كذلك الحال بالنسبة للحافة العليا للنافذة. أما عندما نقوم‬ ‫بعمل نفس الشئ عندما تصطدم الكرة مع المضرب فإنظر إلى هذا الرسم التوضيحي لقانون التصادم:

سنقوم في الكود بإختبار ما إذا كانت الكرة مصطدمة مع المضرب أم لا. عندما يكون هناك إصطدام للكرة مع المضرب فإن نقطة‬ ‫‪ Bx‬يجب أن تكون بين النقاط 0.1-‪ Mx‬و 0.1+‪ Mx‬و التي هي حدود المضرب. كما أن نقطة ‪ By‬و يجب أن تكون أعلى‬ ‫من نقطة -0.39 و أعلى من النقطة -0.78 و التي هي أسفل المضرب و سطحه. سنقوم بتحويل هذا الكلام إلى كود لكي نقوم‬ ‫بجعل الكرة ترتد بعد أن تكون قد ضربت بالمضرب. و سنتكلم في الفصول القادمة عن التصادم بشئ من التفصيل:
‪if (Bx < Mx+0.1 && Bx > Mx-0.1 && By < -0.87 && By > -0.93)
Dy = 1;
‫و بعد أن قمنا بحساب كل شئ يتعلق بحركة الكرة لنقوم برسم الكرة. و قد قلنا بأن مكانها عند المتغير ‪ Bx‬و ‪ .By‬و حتى لا ‫نشتت القارئ فإننا لن نتكلم عن رسم الكرة في هذا التطبيق بل سنؤجله إلى الفصل القادم حين نتحدث عن مكتبة الرسوم بشئ من‬ ‫التفصيل. لذلك سنرسم الكرة كأنها مربع عادي حول نقطة ‪ Bx‬و ‪ By‬هذه المرة:‬
glBegin(GL_QUADS);
glColor3f(1,0.8,0.9);
glVertex2f(Bx-0.02,By+0.02);
glVertex2f(Bx-0.02,By-0.02);
glVertex2f(Bx+0.02,By-0.02);
glVertex2f(Bx+0.02,By+0.02);
glEnd();
‫و الآن لنضيف كل هذا إلى الكود الأصلي و نرى ما التأثير الذي سيحدث. فكل ما سوف نضيفه إلى الكود هو أننا سنقوم بتعريف‬ ‫المتغيرات التي ذكرناها و هي ‪ Bx‬و ‪ By‬و ‪ Dx‬و ‪ Dy‬و هي متغيرات عمومية.‬
float Bx = -1, By = 0;
int Dx = 0, Dy = 0;
‫و أما عن القيم الفتراضية التي وضعناها لهذه المتغيرات فهي تعني أن الكرة في بداية اللعبة سوف تكون في منتصف يسار‬ ‫النافذة كما نرى في القيم الإفتراضية للمتغيرين ‪ Bx‬و ‪ .By‬و سوف يكون إتجاه سيرها إلى اليمين و إلى الأسفل كما نرى في‬ ‫القيم الفتراضية للمتغيرات ‪ Dx‬و ‪ .Dy‬ثم في دالة الرسم قمنا بإضافة السطور الخاصة بالتحريك و تغيير مكان الكرة و كل الإصطدامات بالضافة إلى كود رسم الكرة. و بعد التعديل يفترض أن تجد اللعبة بهذا الشكل:

‫و الآن لنقم بعمل الخطوة الأخيرة. ألا و هي الطوب. و بما أننا ربما نضع الكثير من الطوب فربما نحتاج إلى عمل حلقة‬ ‫تكرارية ‪ Loop‬على كل طوبة لرسمها و فحص ما إذا كانت الكرة قد إصطدمت بها أم لا. يجب أن يكون هناك متغير لكل طوبة‬ ‫يوضح هل هذه الطوبة قد تحطمت أم لا فإذا كانت قد تحطمت لا نقوم برسمها و أما إذا كانت مازالت لم تحطم فنقوم برسمها. و‬ ‫يجب كذلك أن يكون هناك متغير لمكان الطوبة. و مادام عندنا الكثير من المتغيرات فلنقم بعمل تركيب ‪ Structure‬نضع فيه‬ ‫بيانات الطوبة الواحدة. إنظر إلى هذا الكود:‬

typedef struct
{
float X,Y;
int Damaged;
} Brick;
‫ثم نعرف من هذا التركيب عدد من الطوب في مصفوفة ‪:Array
Brick B‬‬[28];
‫و يجب في بداية البرنامج أن نقوم بتعبئة هذه المصفوفة بالبيانات اللازمة. طبعاً سنجعل كل الطوب غير محطم و كذلك سنضع‬ ‫إحداثيات كل طوبة. نتخيل مثلًا أن عندنا ثلاث صفوف من الطوب ففي الصف العلوي لدينا على سبيل المثال عشر طوبات و الذي‬ ‫يليه فيه ثمان طوبات و الذي يليه فيه ست طوبات و الذي يليه فيه أربعة. إذاً فالمجموع الكلي للطوب هو ثمان و عشرين طوبة‬ ‫و الآن لنقم أولًا بعمل حلقة تكرارية لجعل كل الطوب غير محطم و لنفترض أن المتغير ‪ Damaged‬إذا كان يحمل قيمة 1‬ ‫فيدل على أن الطوبة محطمة و إذا كان يحمل القيمة صفر فيدل على أن الطوبة غير محطمة. ضع هذا الكود في الدالة ‪:main‬‬
for (int r = 0 ; r < 28 ; r++)
B[r].Damaged = 0;
‫هكذا قد جعلنا كل الطوب غير محطم في بداية البرنامج. و أما ما يتعلق بإحداثيات الطوب فإنظر إلى هذه الصورة للتوضيح:


‫كما ترى في الصورة فإننا قد جعلنا طول الطوبة هو 0.2 و عرضها 0.1 و الآن إنظر إلى الصف الأول من الطوب. سوف نقوم‬ ‫بعمل حلقة تكرارية على الصف الأول أي من أول الرقم 0 في المصفوفة إلى الرقم 9 و يكون معنا متغير يحمل نقطة ‪ X‬و‬ ‫يزيد هذا المتغير في كل مرة بمقدار 0.2 حتى نصل إلى إحداثيات الطوبة الأخيرة في الصف:‬

float tmpX = -1;
for (int r = 0 ; r < 10 ; r++)
{
B[r].X = tmpX;
B[r].Y = 1;
tmpX += 0.2;
}
‫و الآن بعد أن نصل إلى آخر طوبة سوف تأخذ إحداثيات 0.8 لمحور السينات و 1 لمحور الصادات. كذلك الحال في بقية الطوب‬ ‫و طبعاً سنقوم بعمل حلقة تكرارية لكل صف من الصفوف و نعطي المتغير ‪ tmpX‬القيمة المناسبة في كل مرة:‬
tmpX = -0.8;
for (int r = 10 ; r < 18 ; r++)
{
B[r].X = tmpX;
B[r].Y = 0.9;
tmpX += 0.2;
}
tmpX = -0.6;
for (int r = 18 ; r < 24 ; r++)
{
B[r].X = tmpX;
B[r].Y = 0.8;
tmpX += 0.2;
}
tmpX = -0.4;
for (int r = 24 ; r < 28 ; r++)
{
B[r].X = tmpX;
B[r].Y = 0.7;
tmpX += 0.2;
}
‫و بعد أن قمنا بملأ بيانات الطوب في بداية البرنامج لنقم الآن برسم الطوب ثم بعد ذلك نقوم بإكتشاف تصادم الطوب‬ ‫مع الكرة و كيف ستنعكس الكرة نتيجة لهذا التصادم. بالطبع سوف نقوم بعمل حلقة التكرارية على كل طوبة ثم نقوم بإختبار‬ ‫قيمة المتغير ‪ Damaged‬للطوبة فنحدد هل نرسمها أم لا. و سيكون الكود في دالة الرسم بهذا الشكل:‬
glBegin(GL_QUADS);
for (int r = 0 ; r < 28 ; r++)
{
if (B[r].Damaged == 0)
{
glColor3f(1,0,0);
glVertex2f(B[r].X,B[r].Y);
glColor3f(0,1,0);
glVertex2f(B[r].X,B[r].Y-0.1);
glColor3f(1,0,0);
glVertex2f(B[r].X+0.2,B[r].Y-0.1);
glColor3f(0,1,0);
glVertex2f(B[r].X+0.2,B[r].Y);
}
}
glEnd();
‫و هكذا قمنا برسم كل الطوب الغير محطم حيث أننا قد مررنا على كل الطوب و إختبرنا قيمة المتغير ‪Damaged‬‬ ‫لمعرفة هل هذه الطوبة محطمة أم لا ثم رسمناها و لاحظ أننا قد رسمناها باللون الأحمر يتدرج إلى الأخضر. و طبعاً قد‬ ‫تتسائل لماذا وضعنا دالة ‪ glBegin‬و ‪ glEnd‬خارج الحلقة التكرارية. و الجواب على ذلك باننا قد قلنا قبل ذلك أننا‬ ‫عندما نقوم برسم مضلع من أربع أضلع ‪ GL_QUADS‬فإن مكتبة الرسوم تأخذ كل أربع إستدعاءات لدالة ‪ glVertex‬على‬ ‫أنها مضلع منفرد. و كذلك الحال في ‪ GL_TRIANGLES‬فإن مكتبة الرسوم ستأخذ كل ثلث إستدعاءات على أنهم مثلث منفرد‬ ‫بإستثناء ‪ GL_POLYGON‬التي ترسم مضلعات بدون تحديد عدد معين فإنه يجب عليك أن تقوم في كل مرة ترسم فيها شكل‬ ‫خماسي على سبيل المثال أن تقوم بإستدعاء ‪ glBegin‬و ‪ glEnd‬في كل مرة لأن مكتبة الرسوم لا تدري كم مضلعاً تريد.‬
‫و الآن ندخل في كتابة أوامر إصطدام الطوب بالكرة. بالطبع سنقوم في الحلقة التكرارية السابقة -التي سبق أن جعلناها‬ ‫ترسم الطوب- بجعلها تقوم بإختبار هل الكرة مصطدمة مع الطوبة أم لا فإذا لم تكن مصطدمة فلن يحدث شئ و إن كانت مصطدمة‬ ‫فإنها تعلم على الطوبة بأنها مصطدمة فتقوم بتغيير قيمة المتغير ‪ Damaged‬لتكون 1 (و ذلك حتى لا نقوم برسمها مرة‬ ‫آخرى و حتى لا نقوم بإختبار التصادم معها في كل مرة) و بعد ذلك نقوم بجعل الكرة تنعكس. إنظر إلى الصورة التوضيحية‬ ‫لقانون تصادم المستطيلات و المربعات:



‫كيف نعرف أن هذين المربعين قد تقاطعا ؟؟ إذا توفرت الشروط الآتية: النقطة X1 في مربع 2 أقل من النقطة X2 ‬في مربع‬ ‫1 و النقطة X2 ‬في مربع 2 أكبر من النقطة X1 ‬في مربع 1. و النقطة Y1 ‬في مربع 2 فوق النقطة Y2 ‬في مربع 1 و‬ ‫النقطة Y2 ‬في مربع 2 تحت النقطة Y1 ‬في مربع 1. بالنسبة للكرة و الطوب فإن النقطة X1 ‬في الكرة هي 0.02-‪ Bx‬و‬ ‫النقطة X2 ‬هي 0.02+‪ Bx‬و النقطة Y1 ‬هي 0.02-‪ By‬و النقطة Y2 ‬هي 0.02+‪ By‬و في الطوب النقطة X1 ‬هي‬ ‫‪ B[r].X‬و النقطة X2 ‬هي 0.2+‪ B[r].X‬و النقطة Y1 ‬هي ‪ B[r].Y‬و النقطة Y2 ‬هي 0.1+‪ B[r].Y‬و هكذا‬ ‫نكون مستعدين لإختبار تصادم الطوبة بالكرة. إنظر إلى كود إكتشاف التصادم بعد أن أضفناه إلى الحلقة التكرارية:
glBegin(GL_QUADS);
for (int r = 0 ; r < 28 ; r++)
{
if (B[r].Damaged == 0)
{
if (B[r].Y > By-0.02 && B[r].Y-0.1 < By+0.02 && B[r].X < Bx+0.02 && B[r].X+0.2 > Bx-0.02)
{
if (Dx == 1)
Dx = 0;
else
Dx = 1;
if (Dy == 1)
Dy = 0;
else
Dy = 1;
B[r].Damaged = 1;
}
glColor3f(1,0,0);
glVertex2f(B[r].X,B[r].Y);
glColor3f(0,1,0);
glVertex2f(B[r].X,B[r].Y-0.1);
glColor3f(1,0,0);
glVertex2f(B[r].X+0.2,B[r].Y-0.1);
glColor3f(0,1,0);
glVertex2f(B[r].X+0.2,B[r].Y);
}
}
glEnd();
‫قمنا بإضافة كود إكتشاف التصادم كما شرحناه ثم قمنا بجعل الكرة تعكس إتجاهها تماماً فتعود من حيث جائت ثم يتم تغيير‬ ‫قيمة ‪ Damaged‬للطوبة لكي تصبح محطمة فلا ترسم بل لا يتم التعامل معها أصلًا. و هكذا نكون قد إنتهينا من كل شئ‬ ‫تقريباً في اللعبة‬. بعد التشغيل ستجد اللعبة كهذه الصورة:

‫بعد أن تقوم بتجريب اللعبة ستجد أن كل شئ يعمل كما ينبغي إلا أننا قد نسينا أن نضع اللمسة الأخيرة في اللعبة و هي‬ ‫الفوز و الخسارة. فبعد أن تقوم بتحطيم كل الطوب لا يحدث شئ و تظل اللعبة تعمل كما أن الكرة لو سقطت لا يحدث شئ لذلك‬ ‫نريد أن نضيف اللمسة الأخيرة على اللعبة. و الأمر بسيط لا يتعدى أكثر من أن نضع شرطاً واحداً في دالة ‪ DispTimer‬لكي‬ ‫نختبر مكان الكرة في محور الصادات هل تجاوز النقطة -1 أم لا فإن لم تتجاوزها لا يحدث شئ و أما إن تجاوزتها فإن اللعبة‬ ‫تنتهي و يتم إعلام اللاعب بطريقة أو بآخرى أنه قد خسر. و بالطبع فإننا لم ندخل بعد في الكلم عن تحميل الصور حتى نظهر‬ ‫للاعب رسالة تفيده بأنه قد خسر بل كل ما عرفناه في الرسم هو كيف نرسم أشكال هندسية أولية فقط. لذلك سنقوم بشئ بسيط و هو أن اللعبة سوف تبدأ من جديد فيعود كل الطوب غير محطم و يعود المضرب في منتصف الشاشة و تأتي الكرة في منتصف‬ ‫يسار الشاشة كما كان في البداية. و كذلك الحال في الفوز. و الفوز يتم حين يحطم اللعب كل الطوب. و لكي نعرف هل كل‬ ‫الطوب قد تم تحطيمه أم لا يمكننا أن نقوم بعمل عداد يزيد كلما ضربنا طوبة و حين يصل العداد إلى 28 يفوز اللعب. سنقوم أولًا بعمل دالة تقوم بإرجاع كل شئ في اللعبة إلى طبيعته. و هو أن يتم إستعادة الطوب و إرجاع الكرة إلى مكانها و وضع‬ ‫المضرب في منتصف النافذة. و قد أسميناها ‪ .ResetGame‬إنظر إلى هذه السطور من الكود:

void ResetGame()
{
for (int r = 0 ; r < 28 ; r++)
B[r].Damaged = 0;
Bx = -1;
By = 0;
Dx = 1;
Dy = 0;
Mx = 0;
glutWarpPointer(W/2,H/2);
}
‫و هكذا قمنا بإعادة كل شئ إلى طبيعته. و الآن لنقم بإستدعاء تلك الدالة عندما تنزل الكرة إلى أقل من -1 في محور الصادات‬ ‫أو إذا حطم اللعب كل الطوب. بالنسبة للكرة إذا نزلت سنكتب هذا الشرط في دالة ‪ispTimer‬‬
if (By < -1)
ResetGame();
‫أما عن إذا ما تحطم كل الطوب فإننا سوف نقوم بعمل عداد يزيد كلما حطمنا طوبة. سنقوم بتعريف متغير عمومي نعطيه قيمة‬ ‫صفر في البداية و يزيد كلما حطمنا طوبة فإذا وصل إلى 28 تأكدنا أن اللعب قد حطم كل الطوب:‬
int Toob=0;
و في دالة ‪ DispTimer‬سنقوم في الحلقة التكرارية على الطوب بإضافة هذا السطر بعد 1 = ‪:B[r].Damaged‬‬
‪Toob‬‬++;
‫و بالطبع سنقوم في دالة إعادة اللعبة إلى طبيعتها بتصفير هذا المتغير هو الآخر. و سنقوم بتعديل شرط إستدعاء دالة إعادة كل‬ ‫شئ إلى طبيعته إلى التي:‬
‪if (By < -1 || Toob‬‬ >= 28)
ResetGame‬‬();
‫و الآن قم بإجراء كل التغييرات التي وضعناها و جرب أن تلعب اللعبة. سترى أنها قد أصبحت لعبة متكاملة. و إذا نظرت‬ ‫في الكود ستجد أن كل شئ فيه قد تعلمناه في الدروس السابقة. إذاً فالمسآلة ليست بالتعقيد الذي ربما تصوره البعض في البداية. فبرمجة الألعاب لا تحتاج أكثر من أن تكون على علم بالرسم و الجرافكس بالإضافة إلى بعض الأفكار. يمكنك أن تقوم بعد ذلك بعمل بعض اللعاب الآخرى بناءاً على ما تعلمناه. يمكنك أن تقوم بعمل‬ ‫لعبة ‪ Snake‬الشهيرة مثلًا و عبارة عن ثعبان يمشي و يأكل التفاح و إذا آكل تفاحة يزداد طوله و يجب عليه ألا يصطدم‬ ‫بنفسه و طبعاً كلما طال كلما أصبحت العملية أكثر تعقيداً و صعوبة. فالأمر لا يحتاج أكثر من أن تكون قادر على توليد الأفكار‬ ‫الجديدة و التفكير في حل المشكلة التي أمامك. و لهذا الأمر قمنا بعمل هذه التطبيقات بعد كل مجموعة من الفصول حتى نعطي‬ ‫القارئ أفكار لبعض الأشياء التي يمكن أن يعملها بما قد تعلمه.