تحدثنا في الفصل السابق عن بعض الأوامر البسيطة التي نستخدمها في الرسم. و لكننا قمنا بعمل رسومات تبقى كما هي منذ بداية البرنامج إلى نهايته أي لا تتحرك. و في هذا الفصل بإذن الله تعالى سنتناول كيفية عمل رسوم متحركة. إن الألعاب عبارة عن كائنات تستجيب لمدخلات اللعبة و تتفاعل معها. و يتم رسم كل التحديثات كل جزء محدد من الثانية. إن عمل التحريكات له العديد من الطرق المتنوعة و المختلفة و التي يربطها أصل واحد فقط ألا و هو الوقت. و لكي نتعامل مع عامل الوقت ينبغي أن نذكر أولًا بعض الأساسيات.
يجب أن يكون هناك مؤقت Timer ـيقوم بإستدعاء دالة الرسم كل فترة محددة من الزمن. فمثلًا نريد أن نحدث الرسمة الموجودة كل 30 ميلي ثانية (الميلي ثانية هو جزء من ألف جزء من الثانية). تخيل معي أن هناك دالة يتم إستدعاؤها كل 30 ميلي ثانية و تقوم برسم كل التحديثات. ربما يكون قد تغير مكان مؤشر الفأرة أو قد تغير مكان وحش من الوحوش في اللعبة و المفترض أن تقوم بمسح الرسمة القديمة و رسم آخرى جديدة. و تذكر معي ماذا كنا نفعل حتى تقوم مكتبة جلوت بإستدعاء دالة الرسم. قمنا بإنشاء دالة منفصلة و أسميناها بأي إسم ثم كتبنا فيها أوامر الرسم التي نريدها. ثم عندما كنا في الدالة الرئيسية main قمنا بإخبار مكتبة جلوت بأن تستدعي هذه الدالة إذا ما لزم الأمر. سوف نقوم بإنشاء دالة عادية جداً و نقوم بوضع فيها ما نريد ثم نقوم بإخبار مكتبة جلوت بأن تستدعي تلك الدالة بعد 30 ميلي ثانية (أو طبعاً بعد المدة التي نريدها .. ليس شرطاً أن تكون 30). إنظر لهذا السطر:
و هكذا ستقوم مكتبة جلوت بإستعداء هذه الدالة بعد 30 ميلي ثانية. و عندما تأتي الدالة بعد 30 ميلي ثانية ستقوم بإستعداء دالة الرسم. أولًا جرب أن تقول لمكتبة جلوت أن تستدعي الدالة بعد عشر ثواني حتى ترى نتيجة محسوسة أمامك. أي بدلًا من السطر السابق إكتب:
إن المتغير الذي وضعناه في تعريف دالة TimerXXهذا يتم تمريره في دالة glutTimerFuncفي البارامتر الثالث الذي أخذ قيمة صفر كما ترون. و هذا يعني أننا يمكن أن نمرر قيمةً ما للمؤقت حتى يستعملها في أي شئ في الكود. الأمر مماثل تماماً لعملية إستدعاء دالة عادية حيث نممر لها بارامتر. و لكن الفرق هنا أن الدالة التي إستدعيناها ستنفذ بعد المدة التي حددناها و ليس في الحال. و المتاح فقط لتمريره إلى هذه الدالة هو متغير واحد فقط من نوع عدد صحيح .integerولنجرب الآن البرنامج الذي قمنا بكتابته في الفصل السابق الذي كان ينشئ نافذة و عليها مثلث ملون. إلا أننا سوف نضع فيه بعض التغييرات فسوف يقوم البرنامج في البداية بدون أي رسوم. ثم ستظهر الرسوم (المثلث الملون) بعد عشر ثواني. سيكون الكودبهذا الشكل:
#include <GL/gl.h>
#include <GL/glut.h>
void Display()
{
glClear(GL_COLOR_BUFFER_BIT);
glFlush();
glutSwapBuffers();
}
void Timer1(int i)
{
glBegin(GL_TRIANGLES);
glColor3f(1.0,0.0,0.0);
glVertex2f(0.8, 0.8);
glColor3f(0.0,0.5,1.0);
glVertex2f(0.6, -0.5);
glColor3f(1.0,1.0,0.0);
glVertex2f(-0.5, 0.0);
glEnd();
glFlush();
glutSwapBuffers();
}
int main(int argc , char** argv)
{
glutInit(&argc,argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(200,200);
glutInitWindowSize(320,320);
glutCreateWindow(“Timer Test”);
glClearColor(0,0,0,1);
glutDisplayFunc(Display);
glutTimerFunc(10000,Timer1,0);
glutMainLoop();
return 0;
}
قم بتشغيل الكود و شاهد التغيرات التي ستحدث. ستجد أنه في البداية كانت الشاشة سوداء تماماً. طبعاً نعلم لماذا. لأننا حددنا في البداية في دالة glClearColorأننا نريد صفر و صفر و صفر أي اللون الأسود و قد تم إستدعاء دالة Display لننا عندما أنشأنا النافذة كنا بحاجة إلى رسمها. و قد قلنا لمكتبة جلوت بأن تستدعي دالة إذا ما إحتجنا لعادة الرسم. و قد ذكرنا في دالة Display أننا نريد أن ننظف النافذة ثم أنهينا الرسم و لم نرسم أي شئ. و لكننا أخبرنا مكتبة جلوت أن تقوم بإستدعاء دالة Timer1 بعد 10000 ميلي ثانية (10 ثواني). و قد تم بالفعل أننا وجدنا الرسمة قد ظهرت بعد عشر ثواني. في الحقيقة هناك ملحوظة كنت أود أن ألفت نظر القارئ إليها حتى ندخل في كلم أكثر تفصيلً عن التحكم بالزمن في البرنامج أو اللعبة. بعد أن ظهرت الرسمة بعد عشرة ثواني قم مثلًا بتغيير حجم النافذة أو تكبيرها حتى تقوم مكتبة جلوت بإستدعاء دالة Display و يتم تنظيف النافذة. ستجد أن المثلث قد تم مسحه بالطبع. و لكن إنتظر عشرة ثواني آخرى. ترى هل تم إظهار المثلث مرة آخرى ؟؟ لن تجده و السبب هو أننا قد أخبرنا مكتب جلوت بأن تستدعي دالة Timer1 بعد عشرة ثواني و قد تم ذلك و لم نخبرها أننا نريد تكرار هذا الأمر. لمزيد من التوضيح نحتاج في كل مرة أن نقوم بإخبار مكتبة جلوت أننا نريد إستدعاء دالة Timer1 بعد 10000 ميلي ثانية. سنقوم بالتعديل على دالة Timer1 لتكون كالتي :
و هذا أشبه بعملية ال Recursionأي عندما تستدعي الدالة نفسها فتكون شبيهه بحلقة تكرارية يتم تنفيذ فيها أوامر الدالة و لكن هنا سيتم تنفيذ أوامر الدالة ثم إخبار مكتبة جلوت بإستدعاء نفس الدالة بعد عشرة ثواني. و بهذه الطريقة نجد أن الرسمة ستظهر أو سيتم تجديدها كل عشرة ثواني. و إذا ما قمنا بعمل أي تغيير يستدعي إعادة الرسم ستقوم مكتبة جلوت بإستدعاء Displayو يتم محو الرسمة ثم بعد إتمام العشرة ثواني سيتم إظهار المثلث من جديد. طبعاً هذه ليست الطريقة المثلى للتحكم بالزمن في اللعبة فهناك طرق أفضل سنذكرها لحقاً.
عادةً في الألعاب (أو على القل فيما قمت ببرجمته) لا نحتاج أن نخبر مكتبة جلوت بأننا نريد إستدعاء دالة معينة إذا ما حدث تغيير على النافذة مثل التكبير أو تغيير الحجم أو نحو ذلك لن الرسمه سيتم إعادة رسمها تلقائياً كل مدة معينة نقوم نحن بتحديدها لذلك لسنا بحاجة لمعرفة ما إذا كان هناك تغير أم ل فلسنا بحاجة لستخدام دالة glutDisplayFunc و لسنا بحاجة لعمل دالة مثل .Displayو ستكون كل أوامر الرسم في دالة المؤقت مثل Timer1 في المثال السابق. و لكن بعض المترجمات تتعسف في هذا الأمر و تأبى إلا أن تضع دالة تقوم بعمل ال .Displayفي gcc في اللينوكس لا يطلب مني ذلك فلو لم تقم بإستعمال دالة glutDisplayFuncسيسمح لك. لكن في مترجم CodeBlocks يأبى إل أن تقوم بذلك فنحن نخدعه كما نخدع الطفل الصغير و نقوم بعمل دالة فارغة يقوم بإستدعائها حتى يسكت !!
و هكذا قمنا بتعريف دالة فارغة لا قيمة لها حتى إننا أسميناها DummyFunction و سنقوم بإخبار مكتبة جلوت أن تستدعيها حتى لا توقف عمل البرنامج بسبب عدم وجودها:
و لكي ترى بعينك تحريكاً على النافذة يستعمل هذه الطريقة في عمل العديد من اللقطات في دالة التوقيت. إنظر معي إلى البرنامج التالي الذي يرسم مثلث متحرك من أسفل الشاشة إلى أعلها. سيقوم هذا البرنامج برسم مثلث و هذا المثلث سيتغير مكانه و يسير إلى العلى. سنقوم في البداية بجعل الكود عادي و في مرة آخرى سنضع أوامر تبطئ المعالج حتى ندرك الفرق. قم بكتابة الكود التالي:
#include <GL/gl.h>
#include <GL/glut.h>
float YY = -1;
void Timer(int I)
{
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_TRIANGLES);
glColor3f(1,1,1);
glVertex2f(0.5,YY);
glVertex2f(-0.5,YY);
glColor3f(0,1,0);
glVertex2f(0,YY+0.2);
glEnd();
glFlush();
glutSwapBuffers();
YY += 0.01;
glutTimerFunc(2,Timer,0);
}
void Dummy() {};
int main (int argc , char** argv)
{
glutInit(&argc,argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(0,0);
glutInitWindowSize(300,300);
glutCreateWindow(“Motion Test 1”);
glutDisplayFunc(Dummy);
glutTimerFunc(2,Timer,0);
glutMainLoop();
return 0;
}
في هذا البرنامج قمنا بعمل متغير عام و أسميناه YY و قمنا بإعطائه قيمة -1 في البداية. ثم في دالة التوقيت قمنا برسم مثلث و قمنا بوضعه حول نقطة YY ثم قمنا بإضافة 0.01 على قيمة المتغير .YY ثم في النهاية قمنا بإستدعاء الدالة Timerمرة آخرى عن طريق إخبار مكتبة جلوت بأن تستدعيها بعد 2 ميلي ثانية. و في كل مرة ستتغير قيمة YY
لأننا في كل مرة نزيد عليها 0.01 و لهذا ستجد أن المثلث يتحرك للعلى كما في الشكل التالي.
ثم نعود مرة آخرى للحديث عن التحكم في الزمن. و لقد قمت بعمل رسم توضيحي أو خريطة زمنية توضح كيفية عمل هذه المؤقتات لنعرف كيف نتحكم بالزمن (و من ثم السرعة). أولًا يجب عليك أن تعلم أن الطريقة المثلى لتنظيم الوقت في اللعبة هي التي تجعل اللعبة تعمل بنفس السرعة على المعالجات البطيئة نسبياً و بنفس السرعة على المعالجات السريعة و ما يأتي بعدها. فمثلً إذا عملت لعبة تستطيع أن تعمل على معالجات بنتيوم2. يجب أن تعمل بنفس السرعة على المعالجات التي سوف تكون بعد مائة عام !! و هذا الرسم يوضح المقصود من هذا الأمر. إنظر للصورة:
فكما ترى فإن اللون الأسود هو الوقت الذي يأخذه المعالج المتوسط العادي لكي يقوم بكل عمليات المعالجة و الرسم في لعبة من الألعاب أي أن مكتبة جلوت تقوم بإستدعاء الدالة التي فيها أوامر تحديث الرسم و معالجة تغيرات اللعبة. و الوضع الطبيعي أن تكون هناك فترة ما بين إنتهاء الأوامر و ما بين إبتداء الأوامر مرة آخرى بعد تمام 30 ميلي ثانية الآخرى. و بهذا ستجد أن في المعالجات البطيئة ستعمل اللعبة هكذا:

ستجد أن الوقت الذي إستغرقه الحاسوب لإتمام عمليات اللعبة أكبر نتيجة للبطء. و لكننا لن نلحظ هذا الأمر لننا مازلنا نرى أن السرعة ثابتة و أنه يتم تحديث الصورة كل 30 ميلي ثانية. رغم أن العمليات أبطأ و أخذت وقت أطول. و كذلك في معالجات السريعة:
هنا سنجد أن هذا الحاسوب قام بكل العمليات في سرعة كبيرة و في وقت وجيز. إلا أن سرعة اللعبة مازالت ثابتة لأننا نقوم بتكرار العمليات كل 30 ميلي ثانية. الآن علمنا أن الطريقة المثلى للتحكم في الزمن و السرعة في اللعبة (أو البرنامج) هي أن نحدد فترة زمنية معينة و نكون متأكدين أن تلك الفترة سوف تكفي لكي تجعل الحاسوب ينهي كل العمليات المطلوبة ليدخل فيها مرة آخرى حتى يتم تحديث اللعبة بإستمرار.
إلى الآن ذكرنا الطريقة المثلى التي نريد أن نقوم بها في عملية ضبط الزمن و لكننا لم نحولها إلى كود بعد. إنظر إلى دالة التوقيت تلك و حاول أن تعرف ما الخطأ فيها.
void TT(int X)
{
/* كل أوامر اللعبة هنا */
glutTimerFunc(10,TT,0);
}
طبعاً هذه الطريقة بها العيب الآتي:
ربما تلاحظ أإنها تقوم بتنفيذ العمليات جيداً. و في وقت جيد حسب تجربتك للعبة على حاسوبك. و لكنك ستفاجئ بمشكلة عندما تختبر اللعبة على حاسب آخر. لنتتبع الكود , إنه يقوم بتنفيذ العمليات كلها و لنفرض أنه قام بها كلها في 20 ميلي ثانية على حاسوبك. ثم إنتهى و جاء دور دالة glutTimerFunc حيث قالت لمكتبة جلوت أن تستدعي نفس الدالة بعد 10 ميلي ثانية. سنجد أنه يتم تحديث اللعبة كل 30 ميلي ثانية. و لكن ما العمل إذا ما إختبرنا اللعبة على حاسب أسرع ؟؟

سنجد أن اللعبة قد أسرعت بطريقة جنونية. و السبب هو أن الحاسوب قام بتنفيذ الأوامر كلها على سبيل المثال في 10 ميلي ثانية ثم أخبرنا مكتبة جلوت بأننا نريد أن نستدعي نفس الدالة بعد 10 ميلي ثانية. و هكذا جعلنا التحديث يتم كل 20 ميلي ثانية و ليس 30 و هذا ما ل نريده. و كذلك سنجد نفس الأمر في الحواسيب البطيئة إذا ما إستعملنا تلك الطريقة:

سنجد أن الحاسوب قد قام بتنفيذ الأوامر في 28 ميلي ثانية ثم أخبرنا مكتبة جلوت بأننا نريد إستدعاء نفس الدالة بعد 10 ميلي ثانية و هكذا سيتم تحديث اللعبة كل 38 ميلي ثانية. و حتى تتأكد من هذا الأمر سنقوم في البرنامج الذي كتبناه في البداية الذي كان يقوم بتحريك مثلث إلى العلى بتعديل بسيط يثبت صدق هذا الكلام. و هذا التعديل هو أننا سنقوم في دالة
Timerبوضع عملية حسابية ثقيلة تجعل الحاسب يستغرق بعض الوقت للنتهاء منها و لهذا ستتغير سرعة المثلث. و لنأخذ على سبيل المثال العديد من العمليات الحسابية من الدوال المثلثية مثل sin و cos الموجودان في الملف الرأسي .math.hقم بتضمين ملف math.h ثم غير دالة Timer لتكون:
void Timer(int I)
{
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_TRIANGLES);
glColor3f(1,1,1);
glVertex2f(0.5,YY);
glVertex2f(-0.5,YY);
glColor3f(0,1,0);
glVertex2f(0,YY+0.2);
glEnd();
glFlush();
glutSwapBuffers();
YY += 0.01;
for (int i = 0 ; i < 9999999 ; i++)
cos(3.5);
glutTimerFunc(2,Timer,0);
}
حيث قمنا بجعل دالة cosتتكرر عشرة ملايين من المرات !! قم بتجربة البرنامج في هذه الحالة و لاحظ أن سرعة سير المثلث أصبحت أبطأ بكثير و هذا للسبب الذي ذكرناه. أن عملية إستدعاء دالة glutTimerFunc في آخر الدالة ليست أكثر من مجرد أمر مثل Delayأو Sleepالذي ينتظر مدة محددة يحددها مستدعي الدالة. ما العمل إذاً لحل تلك المشكلة ؟؟ و كيف نصل للطريقة التي ذكرناها في البداية التي تجعل اللعبة (أو البرنامج) يعمل بنفس السرعة في كل الأحوال ؟؟ الأمر بسيط جداً , ما رأيك لو أننا وضعنا إستدعاء دالة glutTimerFunc في بداية الدالة و ليس في نهايتها ؟؟ عندها سيتم إخبار مكتبة جلوت أننا نريد مثلًا إستدعاء تلك الدالة بعد 20 ميلي ثانية, ثم مثلًا لو إفترضنا أن المعالج إستغرق 15 ميلي ثانية في معالجة تلك البيانات. فإنه يتبقى 5 ميلي ثانية حتى يتم إستدعاء الدالة مرة آخرى و لو مثلً إستغرق 10 ميلي ثانية فإنه يتبقى 10 ميلي ثانية آخرى لكي يتم إستدعاء الدالة مرة آخرى و هكذا نقوم بحل المشكلة. الآن سوف نقوم بعمل برنامج كتجربة لهذه المسألة. سنقوم بعمل برنامج به نفس المثلث الذي يسير من أسفل إلى أعلى. في مرة سنضع أوامر رسم المثلث فقط و في مرة آخرى سنضع أوامر ثقيلة جداً لتبطيء المعالج و سنرى أن سرعة سير المثلث لم تقل قط. نحن بحاجة إلى أن نعرف الوقت الذي أخذته دالة المؤقت التي أسميناها . Timer
سوف نقوم بإستعمال دالة ftimeالموجودة في المكتبة timeb لنقيس الفرق بين فترتين من فترات تحديث صورة أو لقطة من اللعبة. مثلًا عندما نستدعي دالة التوقيت بعد 50 ميلي ثانية فإن الفرق بين كل تحديث للعبة هو في حدود ال 50 ميلي ثانية. سنقوم بقياس هذا الزمن الآن. قم بكتابة هذا الكود:
#include <GL/gl.h>
#include <GL/glut.h>
#include <math.h>
#include <stdio.h>
#include <sys/timeb.h>
float YY = -1;
timeb t1,t2;
void Timer(int I)
{
glutTimerFunc(50,Timer,0);
ftime(&t2);
printf(“%d\n”,(int)(1000*(t2.time-t1.time)+(t2.millitm-t1.millitm)));
t1 = t2;
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_TRIANGLES);
glColor3f(1,1,1);
glVertex2f(0.5,YY);
glVertex2f(-0.5,YY);
glColor3f(0,1,0);
glVertex2f(0,YY+0.2);
glEnd();
glFlush();
glutSwapBuffers();
YY += 0.01;
for (int i = 0 ; i < 999999 ; i++)
cos(3.6);
}
void Dummy() {};
int main (int argc , char** argv)
{
glutInit(&argc,argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(0,0);
glutInitWindowSize(300,300);
glutCreateWindow(“Motion Test 1”);
glutDisplayFunc(Dummy);
ftime(&t1);
glutTimerFunc(50,Timer,0);
glutMainLoop();
return 0;
}
لاحظ أننا قد إستعملنا دالة printfفي هذا البرنامج و التي تعمل على شاشة سطر الأوامر و ليس على النوافذ الرسومية التي ننشئها بمكتبة جلوت. طبعاً تذكر أننا في الفصل السابق قلنا بأننا سوف نحتاج إلى إستعمال شاشة سطر الأوامر Consoleو لذلك إذا كنت تستخدم monodevelopقم بتفعيل ظهور شاشة ال Consoleحتى تقوم بمشاهدة النتائج. أو ربما يكون للبعض سبيل آخر في هذا حيث يفضلون أن يقوموا بإخراج النتائج التي يريدون طباعتها على ملف نصي بدلً من سطور على Consoleو لكل منهم وجهة نظر صحيحة. ليس عليك أن تتعرف على تفاصيل دالة ftimeو مكتبة timebو ليس عليك أن تتقنها. فقط كل ما فعلت هو أنني عرفت كائنين من المركب struct timebالذي يحمل بيانات الوقت بدقة الميلي ثانية. ثم قمت في بداية البرنامج بأخذ قيمة الوقت كما تلحظون في هذا السطر:
ثم قمت في دالة Timerالتي يجب أن تتكرر كل 50 ميلي ثانية بقياس الزمن و وضعه في الكائن الثاني t2 و طباعة الفرق بين الزمنين ثم وضعت قيمة t2 في الكائن t1 حتى يحسب الفرق الجديد بين هذا الإستدعاء و الإستدعاء الذي يليه و هكذا. قم بتشغيل البرنامج و لكن تابع ما يحدث داخل نافذة سطر الأوامر التي في العادة تكون خلف النافذة التي أنشئناها. و أنظر إلى النتيجة. ستجد فعلً أن الفرق بين كل تحديث و تحديث حوالي 50 ميلي ثانية بالفعل. قم بعد ذلك بحذف السطرين الأخرين في دالة Timerالذان يثقلان الدالة ثم شغل البرنامج و ستجد أن الوقت بين كل تحديث و تحديث أيضاً حوالي 50 ميلي ثانية. و هذا ما أريد أن أصل إليه في النهاية. هو أن يعمل البرنامج بنفس السرعة سواء كان هناك ما يبطئه أم لا.

كما تلاحظ فإن المثلث في كلتا الحالتين يسير بنفس السرعة سواء في وجود بطء أو في عدم وجوده. و كذلك الحال عندما تجرب البرنامج على حاسوب بطيء. ستجد أن السرعة في كل الحالات ثابتة. و هذا ما أردت أن أصل إليه في هذا الفصل. ليس فقط عمل رسوم متحركة على الشاشة و لكن أردت أن أجعل سرعة الحركة ثابتة في جميع الحالت. و طبعاً هذا ينطبق فقط على سرعة معينة للمعالجات. فمثلًا لا يمكن أن تعمل لعبة قوية على حاسوب صنع في الثمانينيات مثلًا أو في أوائل التسعينيات. لأنه ربما يكون هناك حاسوب قديم يأخذ وقت أكبر من الذي حددته في معالجات أوامر اللعبة. و حينئذ فإن دالة glutTimerFuncإذا وجدت أن الدالة التي سوف تستدعيها قد جاء وقت إستدعائها و لكن مازال هناك إستدعاء آخر لم ينتهي فإنها ستنتظر حتى ينتهي ثم تستدعيها مرة آخرى و لهذا ربما يحدث بعض البطء و إختلف السرعة في بعض الجهزة القديمة. و لكن في الغالب إذا إتبعت هذه الطريقة التي وصلنا إليها في النهاية سيضبط سرعة اللعبة جيداً إن شاء الله
المفضلات