أنماط برمجة الألعاب › حلقة اللعبة (game loop) 14 / 26 السابق التالي

حلقة اللعبة

^title حلقة اللعبة ^section أنماط التسلسل

النية

افصل تقدّم زمن اللعبة عن إدخال المستخدم وسرعة المعالج.

الدافع

إن كان هناك نمط (pattern) واحد لا يستطيع هذا الكتاب الاستغناء عنه، فهو هذا. حلقات اللعبة (game loops) هي المثال النموذجي الكلاسيكي على «نمط في برمجة الألعاب». كل لعبة تقريباً تملك واحدة، ولا اثنتان متطابقتان تماماً، وقلّة من البرامج خارج نطاق الألعاب تستخدمها.

لنفهم كيف تكون مفيدة، فلنُعد قليلاً إلى الذاكرة. في أيام البرمجة القديمة حين كان الجميع لديه لحية، كانت البرامج تعمل كغسّالة الأطباق. تُدخل كومة من الشيفرة، تضغط زراً، تنتظر، ثم تخرج النتائج. انتهى. كانت برامج وضع الدفعات (batch mode) هذه -- بمجرد انتهاء العمل، يتوقف البرنامج.

ما زالت هذه البرامج موجودة اليوم، لكننا بحمد الله لم نضطر إلى كتابتها على بطاقات مثقّبة بعد الآن. نصوص الصدفة (shell scripts)، وبرامج سطر الأوامر، وحتى نصّ بايثون الصغير الذي يحوّل كومة من Markdown إلى هذا الكتاب، كلها برامج وضع دفعات.

مقابلة مع معالج مركزي

في نهاية المطاف، أدرك المبرمجون أن ترك دفعة من الشيفرة في مكتب الحوسبة ثم العودة بعد ساعات قليلة للحصول على النتائج طريقة بطيئة جداً لإخراج الأخطاء من البرنامج. أرادوا تغذية راجعة فورية. وُلدت البرامج التفاعلية (interactive). وكانت بعض أوائل البرامج التفاعلية ألعاباً:

YOU ARE STANDING AT THE END OF A ROAD BEFORE A SMALL BRICK
BUILDING . AROUND YOU IS A FOREST. A SMALL
STREAM FLOWS OUT OF THE BUILDING AND DOWN A GULLY.

> GO IN
YOU ARE INSIDE A BUILDING, A WELL HOUSE FOR A LARGE SPRING.

يمكنك أن تجري محادثة حيّة مع البرنامج. فهو ينتظر إدخالك، ثم يستجيب لك. وتردّ عليه بالتناوب تماماً كما تعلّمت في الروضة. وحين يكون دورك، يجلس هناك دون أن يفعل شيئاً. شيء من هذا النوع:

^code 1

حلقات الأحداث

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

^code 2

الفرق الرئيس هو أن البرنامج، بدلاً من انتظار أوامر نصية، ينتظر أحداث إدخال المستخدم -- نقرات الفأرة وضغطات المفاتيح. وما زال يعمل في جوهره كما تعمل ألعاب النص القديمة حيث يتوقّف البرنامج (blocks) منتظراً إدخال المستخدم، وهذه مشكلة.

على خلاف معظم البرمجيات الأخرى، تستمر الألعاب في الحركة حتى عندما لا يقدّم المستخدم أي إدخال. إن جلست تحدّق في الشاشة، فإن اللعبة لا تتجمّد. تستمر الحركات في التحريك. ترقص المؤثّرات البصرية وتتألق. وإن كنت محظوظاً بخفة، فذلك وحشٌ ما زال يقرض بطلك.

هذه هي الجزء الأساسي الأول من حلقة لعبة حقيقية: إنها تعالج إدخال المستخدم، لكن دون أن تنتظره. فالحلقة لا تكفّ عن الدوران أبداً:

^code 3

سنصقل هذا لاحقاً، لكن القطع الأساسية موجودة هنا. فدالة processInput() تتعامل مع أي إدخال من المستخدم حدث منذ الاستدعاء الأخير. ثم إن update() تقدّم محاكاة اللعبة خطوة واحدة، فهي تشغّل الذكاء الاصطناعي والفيزياء (عادةً بذلك الترتيب). وأخيراً، ترسم render() اللعبة ليُرى اللاعب ما حدث.

عالم بلا زمن

إذا كانت هذه الحلقة لا تتوقّف عند الإدخال، فذلك يقودنا إلى السؤال البديهي: ما مدى سرعتها في الدوران؟ كل دورة عبر حلقة اللعبة تقدّم حالة اللعبة بمقدار معيّن. ومن منظور ساكن في عالم اللعبة، فقد تقدّم عقرب الساعة ساعة إلى الأمام.

وفي الوقت نفسه، ساعة اللاعب الحقيقية تمرّ. إن قِسنا مدى سرعة دوران حلقة اللعبة بمقياس الزمن الحقيقي، نحصل على «عدد الإطارات في الثانية» (FPS) للعبة. إن دار الحلقة بسرعة، يكون عدد الإطارات في الثانية مرتفعاً وتتحرّك اللعبة بسلاسة وسرعة. وإن دار ببطء، تتحرّك اللعبة وكأنها فيلم تصوير بطيء (stop motion).

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

الثاني هو سرعة المنصّة الأساسية. فالرقائق الأسرع تعالج شيفرة أكثر في الزمن نفسه. والأنوية المتعددة، ووحدات معالجة الرسوميات، وعتاد الصوت المخصّص، ومُجدوِل نظام التشغيل، كلها تؤثر في كم العمل الذي تنجزه في نبضة واحدة.

الثواني في الثانية

في ألعاب الفيديو المبكّرة، كان العامل الثاني ثابتاً. فإذا كتبت لعبة لـNES أو Apple IIe، كنت تعرف تماماً أي معالج مركزي تعمل عليه لعبتك، واستطعت (وقد فعلت) أن تكتب الشيفرة خصيصاً له. كل ما عليك أن تقلق بشأنه هو كم العمل الذي تؤديه في كل نبضة.

كانت الألعاب الأقدم مكتوبة بعناية لتؤدي القدر المناسب من العمل في كل إطار بحيث تعمل اللعبة بالسرعة التي أرادها المطوّرون. لكن إن حاولت لعب تلك اللعبة نفسها على آلة أسرع أو أبطأ، فإن اللعبة نفسها ستسرع أو تبطئ.

أما اليوم، فقلّة من المطوّرين يتمتّعون برفاهية معرفة العتاد الذي ستعمل عليه لعبتهم بالضبط. بدلاً من ذلك، يجب أن تتكيّف ألعابنا بذكاء مع مجموعة متنوعة من الأجهزة.

وهذا هو العمل الأساسي الثاني لحلقة اللعبة: إنها تشغّل اللعبة بسرعة ثابتة على الرغم من اختلاف العتاد الأساسي.

النمط

تعمل حلقة اللعبة (game loop) باستمرار أثناء اللعب. وفي كل دورة من الحلقة، تتح معالجة إدخال المستخدم دون أن تتوقّف، وتحدّث حالة اللعبة، وترسم اللعبة. وهي تتتبّع مرور الزمن لتضبط معدّل اللعب.

متى تستخدمه

قد يكون استخدام النمط الخطأ أسوأ من عدم استخدام أي نمط على الإطلاق، لذلك يوجد هذا القسم عادةً للتحذير من الإفراط في الحماس. فهدف أنماط التصميم ليس إدراج أكبر عدد ممكن منها في قاعدة شيفرتك.

لكن هذا النمط مختلف قليلاً. أستطيع أن أقول بثقة جيدة إلى حدٍّ كبير إنك سوف تستخدم هذا النمط. فإذا كنت تستخدم محرّك ألعاب، فلن تكتبه بنفسك، لكنه سيكون هناك على أي حال.

قد تظن أنك لن تحتاج هذا إذا كنت تصنع لعبة بالأدوار. لكن حتى هناك، وإن كانت حالة اللعبة لن تتقدّم حتى يأخذ المستخدم دوره، فإن الحالة المرئية والسمعية للعبة تتقدّم عادةً. فالحركة والموسيقى تستمران حتى عندما تكون اللعبة «تنتظر» منك أن تأخذ دورك.

في بالك

الحلقة التي نتحدث عنها هنا هي بعض أهم شيفرة في لعبتك. يُقال إن البرنامج يقضي 90% من وقته في 10% من شيفرته. وستكون حلقة لعبتك ثابتة في ذلك العُشر. تعامل مع هذه الشيفرة بعناية، وكن واعياً لكفاءتها.

قد تحتاج إلى التنسيق مع حلقة أحداث المنصّة

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

أحياناً، يمكنك أن تستولي على السيطرة وتجعل حلقتك هي الحلقة الوحيدة. مثلاً، إن كنت تكتب لعبة على واجهة Windows API، فيمكن لدالة main() أن تحتوي على حلقة لعبة فحسب. وفي داخلها يمكنك استدعاء PeekMessage() للتعامل مع أحداث نظام التشغيل وتوزيعها. وعلى خلاف GetMessage()، فإن PeekMessage() لا تتوقّف منتظرةً إدخال المستخدم، فستستمر حلقة لعبتك في الدوران.

لا تسمح لك منصّات أخرى بالانسحاب من حلقة الأحداث بسهولة. فإذا كنت تستهدف متصفّح ويب، فإن حلقة الأحداث مبنية بعمق في نموذج تنفيذ المتصفّح. هناك، تكون حلقة الأحداث هي التي تسيّر المسرح، وستستخدمها أنت أيضاً كحلقة لعبتك. ستستدعي شيئاً مثل requestAnimationFrame() وستستدعي هي بدورها شيفرتك لإبقاء اللعبة تعمل.

شيفرة نموذجية

على رغم طول هذه المقدمة، فإن شيفرة حلقة اللعبة في الواقع مباشرة إلى حدٍّ كبير. سنتنقّل عبر بعض التنويعات ونمرّ على مزاياها وعيوبها.

تقود حلقة اللعبة الذكاء الاصطناعي والعرض وأنظمة اللعبة الأخرى، لكن هذه ليست جوهر النمط نفسه، لذا سنكتفي هنا بالاستدعاء إلى توابع وهمية. أما تنفيذ render() وupdate() وغيرها فعلياً فيترك تمريناً (صعباً!) للقارئ.

اركض، اركض بأقصى ما تستطيع

لقد رأينا أبسط حلقة لعبة ممكنة:

^code 3

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

خذ قيلولة قصيرة

التنويع الأول الذي سننظر إليه يضيف إصلاحاً بسيطاً. لنفترض أنك تريد للعبتك أن تعمل بـ60 إطاراً في الثانية. هذا يمنحك نحو 16 مللي ثانية لكل إطار. ما دمت تستطيع بثبات إنجاز كل معالجات اللعبة ورسمها في أقل من ذلك الوقت، فيمكنك العمل بمعدّل إطارات ثابت. كل ما عليك فعله هو معالجة الإطار ثم الانتظار حين يحين وقت الإطار التالي، كالتالي:

مخطط تدفّق بسيط لحلقة اللعبة. معالجة المدخلات → تحديث اللعبة → الرسم → الانتظار، ثم العودة إلى البداية.

تبدو الشيفرة على النحو التالي تقريباً:

^code 4

دالة sleep() هنا تضمن ألّا تعمل اللعبة بسرعة مفرطة إذا عالجت إطاراً بسرعة. لكنها لا تساعد إن كانت لعبتك تعمل ببطء مفرط. إن استغرق تحديث الإطار ورسمه أكثر من 16 مللي ثانية، فسيصبح زمن نومك سالباً. لو كانت لدينا حواسيب تستطيع السفر إلى الوراء في الزمن، لكانت أمور كثيرة أسهل، لكننا لسنا كذلك.

بدلاً من ذلك، تتباطأ اللعبة. ويمكنك الالتفاف على ذلك بأن تؤدي عملاً أقل في كل إطار -- قلّل الرسوميات والتأثيرات البهرجة أو بسّط الذكاء الاصطناعي. لكن ذلك يؤثّر في جودة اللعب لدى جميع المستخدمين، حتى أولئك الذين يستخدمون آلات سريعة.

خطوة صغيرة، وخطوة عملاقة

لنجرّب شيئاً أكثر تعقيداً قليلاً. فالمشكلة التي لدينا تنحصر في الأساس فيما يلي:

  1. كل تحديث يقدّم زمن اللعبة بمقدار معيّن.

  2. معالجة ذلك تستغرق مقداراً معيّناً من الزمن الحقيقي.

إن استغرقت الخطوة الثانية وقتاً أطول من الأولى، فسيتمهّل اللعب. إن استغرقت المعالجة أكثر من 16 مللي ثانية من الزمن الحقيقي لتقدّم زمن اللعبة بمقدار 16 مللي ثانية، فعجز اللعبة عن اللحاق بالركب مستحيل. لكن إن استطعنا تقديم اللعبة بمقدار أكبر من 16 مللي ثانية من زمن اللعبة في خطوة واحدة، فيمكننا أن نحدّث اللعبة أقل تكراراً ونظل نلحق بالركب.

الفكرة إذن هي اختيار خطوة زمنية للتقدّم بناءً على مقدار الزمن الحقيقي الذي مرّ منذ الإطار الأخير. كلما طال زمن الإطار، كانت خطوات اللعبة أكبر. وهي تلحق دوماً بالزمن الحقيقي لأنها تحتاج إلى خطوات أكبر فأكبر للوصول إليه. وياسمي هذا خطوة زمنية متغيّرة (variable time step) أو سائلة (fluid). يبدو هكذا:

^code 5

في كل إطار، نحدّد مقدار الزمن الحقيقي الذي مرّ منذ آخر تحديث للعبة (elapsed). وعندما نحدّث حالة اللعبة، نمرّر تلك القيمة. ثم يكون المحرّك مسؤولاً عن تقديم عالم اللعبة إلى الأمام بمقدار ذلك الزمن.

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

  • تعمل اللعبة بمعدّل ثابت على عتاد مختلف.

  • يحصل اللاعبون ذوو الآلات الأسرع على لعب أنعم كمكافأة.

لكن، للأسف، هناك مشكلة خطيرة تكمن في الأفق: لقد جعلنا اللعبة غير حتمية (non-deterministic) وغير مستقرة. إليك أحد أمثلة الفخ الذي نصبنه لأنفسنا:

لنفترض لدينا لعبة شبكية للاعبين اثنين، ولدى فريد آلة ألعاب وحشية، بينما يستخدم جورج حاسوبه الشخصي القديم الذي ورثه عن جدّته. تلك الرصاصة المذكورة أعلاه تحلّق عبر شاشتيهما. فعلى آلة فريد تعمل اللعبة بسرعة خارقة، لذا تكون كل خطوة زمنية صغيرة جداً. نُكدِّس، تقريباً، 50 إطاراً في الثانية التي تحتاجها الرصاصة لقطع الشاشة. أما آلة جورج المسكينة فتتسع لخمسة إطارات فقط تقريباً.

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

هذه ليست سوى مشكلة قبيحة واحدة يمكن أن تسبّبها الخطوة الزمنية المتغيّرة، لكن هناك المزيد. فمن أجل العمل في الزمن الحقيقي، فإن محرّكات فيزياء الألعاب هي تقريبيات للقوانين الميكانيكية الحقيقية. ولمنع تلك التقريبات من الانفجار، يُطبَّق التخميد (damping). وهذا التخميد مضبوط بعناية على خطوة زمنية معيّنة. بدّل تلك الخطوة، تصبح الفيزياء غير مستقرة.

عدم الاستقرار هذا سيّئ إلى حدٍّ كافٍ بحيث لا وجود لهذا المثال إلا بوصفه tale تحذيرية يقودنا إلى ما هو أفضل...

اِلحق بالركب

أحد أجزاء المحرّك التي عادةً لا تتأثر بالخطوة الزمنية المتغيّرة هو العرض (rendering). فبما أن محرّك العرض يلتقط لحظة زمنية، فلا يمديه ما يساويه إن كان قد مضى وقت طويل منذ اللحظة السابقة. فهو يرسم الأشياء أينما كانت في تلك اللحظة بعينها.

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

الأمر على النحو التالي: لقد انقضى قدر معيّن من الزمن الحقيقي منذ آخر دورة لحلقة اللعبة. وهذا هو مقدار زمن اللعبة الذي نحتاج إلى محاكاته كي يلحق «الآن» عند اللعبة بـ«الآن» عند اللاعب. ونفعل ذلك باستخدام سلسلة من الخطوات الزمنية الثابتة. وتبدو الشيفرة على النحو التالي تقريباً:

^code 6

هناك عدّة قطع هنا. في بداية كل إطار، نحدّث lag بناءً على مقدار الزمن الحقيقي الذي مرّ. وهذا يقيس كم تتأخر ساعة اللعبة مقارنة بالعالم الحقيقي. ثم لدينا حلقة داخلية تحدّث اللعبة، بخطوة ثابتة في كل مرة، حتى تلحق بالركب. وحالما نلحق، نرسم ونبدأ من جديد. ويمكنك تخيّل الأمر على النحو التالي تقريباً:

مخطط تدفّق معدَّل. معالجة المدخلات → تحديث اللعبة → الانتظار، ثم العودة إلى هذه الخطوة، ثم → الرسم → العودة إلى البداية.

لاحظ أن الخطوة الزمنية هنا لم تعد معدّل الإطارات المرئي. فـMS_PER_UPDATE هو مجرد دقّة (granularity) نستخدمها لتحديث اللعبة. وكلما قصرت هذه الخطوة، استغرق لحاقنا بالزمن الحقيقي وقتاً أكبر في المعالجة. وكلما طالت، صار اللعب متقطّعاً أكثر. في الحالة المثلى، تريدها قصيرة إلى حدٍّ كبير، وغالباً أسرع من 60 إطاراً في الثانية، كي تحاكي اللعبة بدقة عالية على الآلات السريعة.

لكن احذر من جعلها قصيرة جداً. عليك التأكّد من أن الخطوة الزمنية أكبر من الوقت اللازم لمعالجة update()، حتى على أبطأ العتاد. وإلا فستكون لعبتك عاجزة ببساطة عن اللحاق بالركب.

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

عالق في المنتصف

تبقى لدينا مشكلة واحدة، وهي التأخّر المتبقّي (residual lag). نحن نحدّث اللعبة بخطوة زمنية ثابتة، لكننا نرسم عند نقاط زمنية اعتباطية. وهذا يعني أن اللعبة ستعرض غالباً، من منظور المستخدم، عند لحظة زمنية تقع بين تحديثين.

إليك خطاً زمنياً:

خط زمني يحوي تحديثات (Updates) متباعدة بالتساوي وعمليات رسم متقطعة.

وكما ترى، نحن نحدّث على فاصل ثابت محكم. وفي الوقت نفسه، نرسم كلما أتاح لنا ذلك. فالتكرار أقل من التحديث، وليس ثابتاً أيضاً. وكلاهما مقبول. الجزء المؤسف هو أننا لا نرسم دائماً في نقطة التحديث بالذات. انظر إلى وقت الرسم الثالث. إنه يقع بالضبط بين تحديثين:

لقطة مقرَّبة من الخط الزمني تُظهر عمليات الرسم واقعة بين خطوات التحديث.

تخيّل رصاصة تعبر الشاشة. عند التحديث الأول، تكون على الجهة اليسرى. والتحديث الثاني ينقلها إلى الجهة اليمنى. وتُرسم اللعبة عند لحظة زمنية بين التحديثين، لذا يتوقّع المستخدم أن يرى تلك الرصاصة في منتصف الشاشة. ومع تنفيذنا الحالي، ستبقى على الجهة اليسرى. وهذا يعني أن الحركة تبدو مسنّنة أو متقطّعة.

ولحسن الحظ، نحن نعرف تماماً بكم بعد نقع عن إطارات التحديث لحظة الرسم: فالأمر مخزَّن في lag. ونحن نخرج من حلقة التحديث عندما يصير أقل من الخطوة الزمنية للتحديث، لا عندما يصير صفراً. فما تبقّى من قيمة؟ إنه بُعدنا داخل الإطار التالي.

وحين نذهب إلى الرسم، سنمرّر تلك القيمة:

^code 7

العارض يعرف كل كائن من كائنات اللعبة و سرعته الحالية. لنفترض أن تلك الرصاصة على بُعد 20 بكسل من الجهة اليسرى من الشاشة وتتحرّك يميناً بمقدار 400 بكسل في الإطار. وإن كنا في منتصف المسافة بين إطارين، فسنمرّر 0.5 إلى render(). أي أنه يرسم الرصاصة نصف إطار إلى الأمام، عند 220 بكسل. وها هي ذي، حركة ناعمة.

بالطبع، قد يتبيّن أن هذا الاستقراء (extrapolation) خطأ. فعندما نحسب الإطار التالي، قد نكتشف أن الرصاصة ارتطمت بعائق أو تباطأت أو ما شابه. لقد رسمنا موضعها بالاستيفاء بين موضعها في الإطار الأخير وموضعها في الإطار التالي كما نظنّ. لكننا لا نعرف ذلك حتى ننفّذ التحديث الكامل بالفيزياء والذكاء الاصطناعي.

إذن الاستقراء هنا شيء من التخمين، وقد يتبيّن خطأ أحياناً. لكن لحسن الحظ، لا تكون تصحيحات من هذا النوع ملحوظة عادةً. وعلى الأقل فهي أقل ملحوظة من التقطّع الذي تحصل عليه إن لم تستقرئ أصلاً.

قرارات التصميم

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

هل تملك حلقة اللعبة، أم المنصّة؟

هذا أقل ما يكون خياراً تختاره وأكثر ما يكون خياراً فُرض عليك. فإذا كنت تصنع لعبة تعمل في متصفّح ويب، فأنت لا تستطيع عملياً أن تكتب حلقة لعبتك الكلاسيكية الخاصة. فطابع المتصفّح القائم على الأحداث يمنع ذلك. وعلى حدٍّ سواء، إن كنت تستخدم محرّك ألعاب موجوداً، فمن المحتمل أن تعتمد على حلقته بدلاً من أن تكتب حلقتك الخاصة.

  • استخدم حلقة أحداث المنصّة:

    • بسيطة. لا عليك أن تقلق بشأن كتابة الحلقة الرئيسة للعبة وتحسينها.

    • تتعاون جيداً مع المنصّة. لا عليك أن تقلق بشأن إتاحة وقت صراحةً للمضيف ليعالج أحداثه الخاصة، ولا بشأن تخزين الأحداث مؤقتاً (caching)، ولا بشأن إدارة عدم التطابق (impedance mismatch) بين نموذج إدخال المنصّة ونموذجك على أي حال.

    • تفقد التحكّم في التوقيت. ستستدعي المنصّة شيفرتك كما ترى مناسباً. وإن لم يكن ذلك بتلك الكثرة أو تلك السلاسة التي تريدها، فلا حيلة لديك. والأسوأ أن معظم حلقات أحداث التطبيقات لم تُصمَّم والمgames في ذهنها، وهي عادةً بطيئة ومتقطّعة.

  • استخدم حلقة محرّك ألعاب:

    • لست مضطراً لكتابتها. فكتابة حلقة لعبة قد تصبح معقّدة جداً. ولأن تلك الشيفرة الرئيسة تُنفَّذ في كل إطار، فإن الأخطاء الطفيفة أو مشكلات الأداء قد يكون لها أثر كبير على لعبتك. وحلقةُ لعبةٍ محكمةُ المبرمَجات سبب وجيه للنظر في استخدام محرّك موجود.

    • لن تتمكّن من كتابتها. وبالطبع، فإن الجانب الآخر من هذه العملة هو فقدان التحكّم إن كنت فعلاً لديك احتياجات لا تناسب المحرّك تماماً.

  • اكتبها بنفسك:

    • تحكّم كامل. يمكنك أن تفعل بها ما تشاء. ويمكنك أن تصمّمها خصيصاً لاحتياجات لعبتك.

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

كيف تدير استهلاك الطاقة؟

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

والآن، قد تحتاج إلى أن تفكّر ليس في جعل لعبتك تبدو رائعة فحسب، بل أيضاً في استهلاك أقل قدر ممكن من المعالج. وهناك على الأرجح حدّ أعلى للأداء عنده تدع المعالج ينام إذا كنت قد أنجزت كل العمل الذي تحتاج إلى إنجازه في الإطار.

  • الركض بأقصى ما يستطيع:

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

    هذا يمنحك أفضل تجربة لعب ممكنة، لكنه سيستهلك أكبر قدر ممكن من الطاقة. فإذا كان اللاعب على حاسوب محمول، فسيكون لديه مُدفّأ للركبة جميل.

  • تقييد معدّل الإطارات:

    تركّز ألعاب الهاتف غالباً على جودة اللعب أكثر مما تركّز على تعظيم تفاصيل الرسوميات. وكثير من هذه الألعاب تضع حداً أعلى لمعدّل الإطارات (عادةً 30 أو 60 إطاراً في الثانية). فإذا انتهت حلقة اللعبة من المعالجة قبل انقضاء تلك الشريحة الزمنية، فإنها ستنام simply لبقية الوقت.

    هذا يمنح اللاعب تجربة «جيدة بما يكفي»، ثم يترك بطاريته بهدوء بعد ذلك.

كيف تتحكّم في سرعة اللعب؟

تتركّب حلقة اللعبة من قطعتين أساسيتين: إدخال مستخدم غير حاجب، والتكيّف مع مرور الزمن. أما الإدخال فبسيط. والسحر في كيفية تعاملك مع الزمن. وهناك عدد شبه لا نهائي من المنصّات التي يمكن أن تعمل عليها الألعاب، وقد تعمل لعبة واحدة على عدد كبير منها. وكيف تستوعب اللعبة هذا التنوّع هو جوهر الأمر.

  • خطوة زمنية ثابتة بلا مزامنة:

    هذه كانت أول شيفرة نموذجية لدينا. فأنت فقط تشغّل حلقة اللعبة بأقصى ما تستطيع.

    • بسيطة. وهذه هي فضيلتها الرئيسة (أي فضيلتها الوحيدة في الواقع).

    • سرعة اللعبة تتأثر مباشرةً بالعتاد وتعقيد اللعبة. وخطيئتها الرئيسة هي أن أي تنوّع سيؤثّر مباشرةً في سرعة اللعبة. فهي المحضّلة الثابتة لحلقة اللعبة.

  • خطوة زمنية ثابتة مع مزامنة:

    الخطوة التالية على سلّم التعقيد هي تشغيل اللعبة بخطوة زمنية ثابتة، مع إضافة تأخير أو نقطة مزامنة في نهاية الحلقة لمنع اللعبة من العمل بسرعة مفرطة.

    • لا تزال بسيطة إلى حدٍّ كبير. فهي سطر واحد فقط من الشيفرة أكثر من المثال «البسيط جداً على نحو يمنعه من العمل» على الأرجح. وفي معظم حلقات الألعاب، من المحتمل أن تقوم بالمزامنة على أي حال. ومن المرجّح أنك ستستخدم المخزن المزدوج لرسومياتك وتزامن قلب المخزن مع معدّل تحديث الشاشة.

    • صديقة للطاقة. وهذه اعتبار مهم بشكل مفاجئ للألعاب على الهاتف. فأنت لا تريد أن تستنزف بطارية المستخدم بلا داعٍ. فبالنوم لبضع مللي ثانية فحسب بدلاً من محاولة حشر مزيد من المعالجة في كل نبضة، أنت توفّر الطاقة.

    • اللعبة لا تعمل بسرعة مفرطة. هذا يعالج نصف مخاوف السرعة في الحلقة الثابتة.

    • قد تعمل اللعبة ببطء مفرط. إن استغرق تحديث إطار اللعبة ورسمه وقتاً طويلاً فإن سرعة اللعب ستبطئ. ولأن هذا الأسلوب لا يفصل التحديث عن الرسم، فمن المرجّح أن يصطدم بهذه المشكلة في وقت أسرع من الخيارات الأكثر تقدّماً. فبدلاً من مجرد إسقاط إطارات الرسم للحاق بالركب، ستتباطأ اللعبة.

  • خطوة زمنية متغيّرة:

    سأضعها هنا بوصفها خياراً في فضاء الحلول، مع التنبيه بأن معظم مطوّري الألعاب الذين أعرفهم ينصحون بعدم استخدامها. لكن من الجيّد أن نتذكّر لماذا فهي فكرة سيّئة.

    • تتكيف مع البطء المفرط والسرعة المفرطة معاً. إن عجزت اللعبة عن اللحاق بالزمن الحقيقي، فستأخذ ببساطة خطوات زمنية أكبر فأكبر حتى تفعل.

    • تجعل اللعب غير حتمي وغير مستقر. وهذه بالطبع هي المشكلة الحقيقية. فالفيزياء والشبكات تحديداً تصبحان أصعب بكثير مع خطوة زمنية متغيّرة.

  • خطوة زمنية ثابتة للتحديث، ورسم متغيّر:

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

    • تتكيف مع البطء المفرط والسرعة المفرطة معاً. ما دامت اللعبة قادرة على التحديث في الزمن الحقيقي، فلن تتأخر اللعبة عن الركب. فإذا كانت آلة اللاعب من الطراز الأول، فستستجيب بتجربة لعب أنعم.

    • أكثر تعقيداً. والعيب الرئيس هو أن التنفيذ فيه أمور أكثر قليلاً. فعليك ضبط الخطوة الزمنية للتحديث بحيث تكون أصغر ما يمكن للأجهزة عالية المواصفات، في الوقت نفسه الذي لا تكون فيه بطيئة أكثر مما ينبغي للأجهزة متدنية المواصفات.

انظر أيضاً

  • المقالة الكلاسيكية عن حلقات اللعبة هي مقالة غلين فيدلر «أصلح خطوتك الزمنية». ولولاها لما كان هذا الفصل كما هو.

  • وتأتي مقالة ويتر عن حلقات اللعبة في المرتبة التالية مباشرة.

  • إطار عمل Unity يملك حلقة لعبة معقّدة مشروحة برسم توضيحي رائع هنا.