معمارية اللعبة Game Architectur ملاحظـة: نظراً لطول الموضوع فقد قسمتـه إلى أربع أجزاء حتى أسهل لنفسي من عـملية التنسيق التي جلست فيها أكثر من ربع ساعـة ... شكراً. +تمهيد +التطبيقات الفورية. +القسم المنطقي للعبة. +قسم العرض للعبة. +عـملية البرمجـة. +الخلاصـة. تمهيد: يركز هذا المقال حـول كيفية بناء اللعبـة ، سنبدأ رحلتنا مع دراسة الألعاب كتطبيقات فورية real-time software، وبعد ذلك نحلل ما يكون داخل أي لعبة بشكل عام ، لذا فإننا سنحدد كتل البناء الأساسية الرئيسيـة للسورس كـود (سنتعرف على هذه الكتل في الفصول اللاحقة من الكتاب) ؛ بالإضافة إلى ذلك فإننا سنركز على بعض المفاهيم الإدارية للمشاريع الخاصة بتطوير الألعاب .سندرس أيضاً التخطيط الكـودي إلى الجدولة إلى الاختبار إلى الصيانـة ؛ بوضع كل هذا معاً فإنك ستكون قادراً على فهـم كيف تبنى اللعبة. التطبيقات الفوريـة: Real-Time Software ألعاب الفيديـو هي تطبيقات برمجيـة؛ بشكل أدق هي تصنف على أنها تطبيق فوري real-time software. هذا تصنيف مهـم لأنه سيساعدك على فهـم كيف تتصرف ولماذا بعض القرارات الكـوديـة يتم اتخاذها. لهـؤلاء الذين لا يعرفون مثل هذه المفاهيم فإنني سأتوقف لشرح مثل هذه الخصائص. في التعريف الرسمي التطبيقات الفوريـة تعـني تطبيقات الحاسب التي لها طبيعـة حساسة بالزمن حيث يتم جمع البيانات والاستجابة لها في وقت زمني محدد جداً. على سبيل المثال ، برنامج يعرض معلومات عـن القادمين على شاشة كبيرة في محطـة مطار ، عدة سطور ستكون عـن عدد الطلعات ، الحالة ، وقت الهبوط وهكذا .... من الواضح أن هذا التطبيق يستجيب للأحداث الزمنيـة : هـبوط طائرة، التأخيرات وهكذا ...... إن وصول هذه المعلومات متقلبة جداً، والتطبيق يجب أن يعالج ويتعامل معها وفقاً لذلك؛ علاوة على ذلك فإن هذه المعلومات المعتمدة على الوقت يجب أن تعرض في شاشة لتزويد عرض مرئي عـن البيانات المعتمدة على الوقت .. هذا بشكل أساسي بسيط ما تعنيه التطبيقات الفورية. لنأخذ مثالاً أعقد من المثال السابق بقليل ؛ برنامج تطبيقي تم تصميمـه للمساعدة في السيطرة على الملاحـة الجويـة أنظر الشكل التالي: يتحسس التطبيق الفضاء أو الجـو عـن طريق الرادار ، يعرض معلومات الطائرات ومساراتها على الشاشـة ، ويسمح للمراقب الأرضي بمساعدة الطياريـن للوصول إلى أهدافهـم في الوقت المحدد وبشكل آمن عـن طريق إرسال رسائل إليهـم ؛ بالنظر إلى ما داخل النظام فإنك ستجد أنه يتكون من: + وحدة جمع البيانات ؛ في هذه الحالة هي الرادار . + وحدة حساب\ عرض والتي تساعد المراقب الأرضي بعرض البيانات مرئياً. + وحدة تفاعل لإرسال إشارات إلى الطائرات حتى تعلم ما عليها فعله. هانحن نتحرك خطوة إلى الأمام في مثالنا السابق . نحن الآن نشاهد تطبيق فوري ، تطبيق تفاعلي ، تطبيق يستجيب إلى الأحداث التي تحدث في أي وقت كان ، يعرض معلومات تتعلق بهذه الأحداث ويسمح للمشغل بالتفاعل معهـم. الألعاب ليست مختلفـة جداً عـن هذه المعماريـة . تخيل أننا استبعدنا الرادار وأضفنا ملاحـة جوية "افتراضية" عـن طريق تطبيق محاكي ، وأخبرنا المستخدم بأن عليه العـمل على هبوط الطائرات بسلام ، أضفنا إلى هذه اللعبـة لوحـة نتائج على الشاشـة. كل الألعاب في الحقيقـة هي عبارة عـن تطبيقات تفاعلية فوريـة ، المشغل (والذي يدعـى اللاعب player ) يستطيع الاتصال بعالم اللعبـة والذي هـو في الحقيقـة عبارة عـن محاكاة للعالم الحقيقي باستخدام مكونات برمجيـة. عدو يطاردنا ، مصعد يرتفع ويهبط بنا ، والرد على إطلاق النيران هي في الحقيقـة عبارة عـن أمثلة افتراضية للعوالم الحقيقيـة. لكن هـناك الكثير من الوقت (التوقيتات) في الألعاب أكثر مما تعتقد ، الألعاب مقيدة بزمن ، يجب عليك عرض معلومات بشكل سريع( في العادة أكثر من 25 إطاراً في الثانية الواحدة) وذلك للسماح بالتفاعل أن يكون مستمر. على كل فإن الألعاب أشبه ما تكون بالخيال ، الخدعـة هـنا هي جعل المستحيل ممكناً أي صناعـة عـوالم تبدو أنها أكبر من إمكانيات الهاردوير عـن طريق تقديم العروض المتعددة الوسائط بشكل جيد. بشكل ملخص الألعاب هي تطبيقات تفاعلية معتمدة على الوقت ، تحتوي على محاكي عالم حقيقي والذي يتغذى من بيانات العالم الواقعي ، ونموذج تقديمي يقوم بعرضـه ، آليات سيطرة تسمح للاعب بالتفاعل مع هذا العالم. بسبب أن معدل التفاعل يكون سريعاً فإن هـناك حدود حول ما يمكن محاكاته ؛ ولكن برمجـة الألعاب تحاول تحدي مثل هذه الحدود وتحاول صناعـة شيء أقوى مما تقبله البيئـة في كل من التقديم أو العرض presentation والمحاكاة simulation . هذا هـو مفتاح برمجـة الألعاب وموضوع هذا الكتاب. الجزء الأول "Gameplay Programming " يتعامل مع تكويد محاكيات العالم الحقيقي التي تعالج عالم اللعبـة، الجزء الثاني "برمجـة المحرك" يغطي طبقة التقديـم أو العرض. ملاحظـة: المحاكاة هي تقليد العالم الحقيقي ، التقديم هـو عرض هذه المحاكاة على أي شكل كان على وحدة إخراج شاشـة ، طابعـة ، أي شيء.... الحلقات الفورية Real-Time Loops كما هـو مذكور في وقت سابق ، جميع التطبيقات الفورية التفاعلية تشمل ثلاث مهـام تعمل في نفس الوقت؛ أولاً: حالة العالم يجب أن تكون محسوبة بشكل ثابت ( على الرادار أن يشعر بوجود الطائرة في الفضاء ) ، الثانية: يجب أن يسمح للمشغل أو اللاعب بالتفاعل مع العالم ، الثالثة: حالة العالم الناتجـة يجب أن تقدم إلى اللاعب على الشاشـة ، السماعات .. وأي وحدة إخراج متوفرة ؛ في أي لعبة فإن كلتا مهمتي محاكاة العالم وإدخالات المستخدم تعتبران من المهام التي تحدث هذا العالم ؛ في النهـاية فإن اللاعب هـو عبارة عـن مجرد كيـان خاص باللعبـة. للبساطـة ، فإنني سأتبع هذه القاعدة وسأشير إلى الألعاب على أنها تطبيقات تشمل جزأين: التحديث و روتين التصيير render routine . حالما نحاول أن نكتب هذين الروتينين في كـود اللعبة الفعلي فإن المشاكل ستبدأ بالظهـور، كيف نضمن بأن كلا الروتينين يتم تنفيذهـما بشكل لحظي ، والذي سيعطي الوهـم بالفعل أننا ننظر إلى العالم الحقيقي من خلال نافذة (من خلال شاشة الحاسب) ؛ في العالم المثالي (الذي نطمح بالوصول إليه) ، فإن كلا الروتينين التحديث والتصيير تتنفذان بجهاز هاردوير قوي بشكل لانهائي وهذا الجهاز يشمل الكثير من المعالجات المتوازيـة ، لذا كلا الروتينين سيكون عـندها تداول غير محدود لمعالجات الهارودوير ؛ ولكن فإن تكنولوجيا العالم الحقيقي تفرض العديد من التقييدات: أغلب الحاسبات بشكل عام تحتوي على معالج واحد مع سرعـة وذاكرة محدودتين ، من الواضح أن المعالح يمكن فقد أن ينفذ واحدة أو اثنيتن من المهام المطلوبـة في أي وقت مطلوب ؛ لذا فإننا نحتاج هـنا إلى بعض الخطط الذكيـة...... أولى الطرق هي أن يتم تنفيذ كلا الروتينين في حلقة تكرارية كما هو واضح من الشكل بالأسفل ، أي أن التحديث سيتبعـه استدعاء التصيير وهلم جرا . هذا يضمن بأن كلا الروتينين لهـما أهـمية متساويـة . في هذه الطريقة فإن المنطق (والذي هـو في هذه الحالة التحديث) و التقديم presentation من المفترض اعتبارهـما مرتبطيـن بشكل كامل ؛ ولكن ماذا سيحدث لو كان معدل إطارات\ثانية يتغير عـند أي تغيير غير ملحوظ في مستوى التعقيد؟ (ملاحظة المترجم: يقصد هـنا بمستوى التعقيد تغير معدل إطارات\ثانيـة بين الأجهـزة المختلفـة)، تخيل أن 10% اختلاف في تعقيد المشهد الذي سيسبب بطأً في محرك اللعبـة ، عدد الدورات المنطقيـة يتفاوت طبقاً لذلك ؛ بشكل أسوأ ، مالذي سيحدث في لعبة كمبيوتر شخصي حيث الآلات السريعـة تخرج بيانات بشكل أسرع من الآلات البطيئـة بخمسة أضعاف ؛ هل سيكون الذكاء الصناعي AI أبطأ في هذه الآلات الغير قويـة ... من الواضح أن استعـمال الطريقة المتزاوجـة تنشيء بعض الأسئلة المثيرة حول كيف ستتأثر اللعبـة بسبب اختلافات الأداء.... لحل هذه المشاكل يجب علينا أن نحلل طبيعـة كلا الكـودين في الروتينين. بشكل عام فإن على روتين التصيير أن ينفذ غالباً كما تسمح بيئة الهاردوير ، الكمبيوترات السريعـة يجب عليهـا أن تنتج مشاهد أنعـم ، ومعدل إطارات\ثانية أسرع وهكذا ؛ ولكن العالم في اللعبة يجب ألا يكون متأثر بهذه السرعـة ، يجب على الشخصيات في اللعبـة أن تمشي بالسرعـة التي صممت بها اللعبـة أو أن اللعبـة سيكون ليس لها أي داعي أصلاً ، تخيل بأنك اشتريت لعبـة كرة قدم واللعب فيها إمـا أن يكون سريعاً جداً أو بطيئاً جداً حسب سرعـة الجهـاز ؛ من الواضح أن قسمي التصيير والتحديث متزامنين سيعقد التكويد ، لأن أحدهـما (التحديث) له تردد ثابت أساسي والآخر لأ. حل مختلف آخر لمشكلة التزامن سيستخدم طريقـة القناتين التوأمين ،أنظر إلى الشكل بالأسفل، لذا فسيكون بإمكان قناة واحدة أن تنفذ قسم التصيير والأخرى ستحرص على تحديث العالم ؛ بواسطـة التحكم بالتردد في أي من الروتينين سيتم استدعاؤهـما ، سنضمن بذلك بأن قسم التصيير سيتم استدعاؤه عدد من المرات حتى يبقى مستمر ؛ تنفيذ AI بين 10 إلى 25 مرة في الثانية أكبر من كفاية أغلب الألعاب. تخيل وجود لعبة أكشن يتم تنفيذها 60 تردد في الثانيـة مع تنفيذ AI في قناة ثانوية 15 تردد في الثانيـة ؛ من الواضح أن إطاراً واحد من الأربعة إطارات سيتم تحديثها ؛ بالرغـم من أن هذا الفعل جيد لضمـان سرعـة محددة فإن لها بعض الجوانب السلبيـة ؛ على سبيل المثال كيف نضمن بأن الأربعة إطارات التي تتشارك في دورة الـ AI مختلفة عـملياً ، عرض صور متحركـة ورسومات أنعـم . الكثير من الإطارات لا تعـني شيئاً إذا كانت متشابهـة في دورة الـ AI ؛ الصور المتحركـة ستعـمل بفعالية عـند 15 تردد \ثانيـة ؛ لحل هذه المشكلـة فإنه يتم تقسيم AIs إلى قسمين . الكـود الحقيقي AI سيتم تنفيذه في وقت زمني محدد بينما الروتينات البسيطـة مثل عرض الرسوم والمقذوفات سيتم معالجتها لكل إطار . بهذه الطريقة فإن الإطارات الإضافية لكل ثانيـة ستكون حقاً مختلفـة أثناء تجربة اللاعب لها. ولكن فإن طريقة القنوات لها قضايا حقيقية يجب التعامل معها ؛ أساساً فإن الفكرة جيدة ولكنها لا تنفذ جيداً على بعض بيئات الهاردوير ، بعض الآلات ذات single-CPU ليست جيدة بالقدر الكافي للتعامل مع القنوات خاصـة حينما يتم تطبيق دوال ذات توقيت دقيق جداً ، بعض الإختلافات في التردد تحدث ومستوى رضا اللاعب سينخفض ، المشكلة لا تظهر بشكل جلي في الدالة التي تستدعـى رأسياً حينما يتم خلق قنوات ؛ ولكن في دوال توقيت نظام التشغيل التي ليست بتلك الدقـة. هكذا ، يجب علينا إيجاد شيء يسمح لنا بتقليد القنوات على مكائن وحدة المعالجـة المركزية الأحاديـة. البديل الأكثر شعبيـة لهذه البيئات هـو معالجـة القنوات باستخدام حلقات برمجيـة منتظمـة ومؤقتات في برنامج ذو قناة واحدة. المفتاح هـنا هـو تنفيذ التحديث والتصيير بشكل متسلسل مع تخطي روتين التحديث وعـدم تنفيذه عـن طريق فصل روتين التصيير من روتين التحديث ، يتم تنفيذ روتين التصيير مهـما كان ممكناً مستدعاً بينما يتم مزامنـة روتين التحديث للعـمل في أوقات محددة. لإنجاز مثل هذه النتيجـة يجب علينا البدء بتخزين توقيت استدعاء دالة التحديث ؛ ثم وفي نفس الحلقة يجب علينا حساب الوقت منذ النداء الأخير ومقارنتـه بمعكوس التردد المطلوب ؛ عـن طريق فعل هذا فإننا نختبر إذا ما كنا نحتـاج استدعاء دالة التحديث ؛ على سبيل المثال إذا أردت تنفيذ AI عشرين مرة في الثانيـة فإنه يجب عليك استدعاء روتين التحديث مرة واحدة كل 50 ميلي ثانيـة ، ثم فإن كل ما عليك فعله هـو تخزين الوقت الذي تم استدعاء فيه الدالة ولا يتم تنفيذ الدالة مرة أخرى إلا حينما تنقضي 50 ميلي ثانيـة منذ آخر استدعاء ؛ هذه آلية شعبية جداً لأن كثيراً من الوقت يتم السيطرة عليه بشكل أفضل من القنوات ؛ لا يجب عليك الاهتمام بالذاكرة المشتركـة أو التزامن أو غير ذلك أنظر إلى الرسم بالأسفل. هذا هـو الكـود بلغـة السي لتطبيق مثل هذه الطريقة:
long timelastcall=timeGetTime();while (!end) { if ((timeGetTime()-timelastcall)>1000/frequency) { game_logic(); timelastcall=timeGetTime(); } presentation(); }
time0 = getTickCount();while (!bGameDone) { time1 = getTickCount(); frameTime = 0; int numLoops = 0; while ((time1 - time0) > TICK_TIME && numLoops < MAX_LOOPS) { GameTickRun(); time0 += TICK_TIME; frameTime += TICK_TIME; numLoops++; } IndependentTickRun(frameTime); // If playing solo and game logic takes way too long, discard // pending time. if (!bNetworkGame && (time1 - time0) > TICK_TIME) time0 = time1 - TICK_TIME; if (canRender) { // Account for numLoops overflow causing percent > 1. float percentWithinTick = Min(1.f, float(time1 - time0)/TICK_TIME); GameDrawWithInterpolation(percentWithinTick); } }
Player update Sense Player input Compute restrictions Update player stateWorld update Passive elements Pre-select active zone for engine use Logic-based elements Sort according to relevance Execute control mechanism Update state AI based elements Sort according to relevance Sense internal state and goals Sense restrictions Decision engine Update worldEnd
Game logic Player update Sense Player input (chapter 5) Compute restrictions (chapter 22) Update player state World update (chapters 6 to 9) Passive elements (chapter 4, spatial index) Pre-select active zone for engine use Logic-based elements Sort according to relevance Execute control mechanism Update state AI based elements Sort according to relevance Sense internal state and goals Sense restrictions Decision engine Update worldEndPresentation World presentation (chapters 11 to 14, 17 to 21) Select visible subset (graphics) Clip Cull Occlude Select resolution Pack geometry Render world geometry Select audible sound sources (sound) Pack audio data Send to audio hardware NPC presentation (chapter 15) Select visible subset Animate Pack Render NPC data Player presentation (chapter 15) Animate Pack RenderEnd