مزيد من المعلومات عن العرض في حلقات الألعاب

إحدى الطرق الشائعة جدًا لتنفيذ حلقة الألعاب تبدو كما يلي:

while (playing) {
    advance state by one frame
    render the new frame
    sleep until it’s time to do the next frame
}

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

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

  • استخدام مكتبة Android Frame Pacing (إجراء يُنصح به)
  • ملء قائمة انتظار التخزين المؤقت بالكامل والاعتماد على الضغط الخلفي لـ "تبديل الموارد الاحتياطية"
  • استخدام مصمم الرقصات (واجهة برمجة التطبيقات 16+)

مكتبة ميزة "تتبُّع سرعة الإطارات" في Android

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

ملء قائمة الانتظار

يسهل تنفيذ هذا الإجراء: ما عليك سوى تبديل الموارد الاحتياطية بأسرع ما يمكن. في الإصدارات الأولى من نظام التشغيل Android، قد يؤدي هذا الإجراء إلى فرض عقوبة، حيث تصبح SurfaceView#lockCanvas() في وضع السكون لمدة 100 ملي ثانية. يتم الآن تعديل سرعتها من خلال BufferQueue، ويتم إفراغ BufferQueue بالسرعة نفسها التي يمكن بها SurfaceFlinger.

يمكنك الاطّلاع على أحد الأمثلة على هذا النهج في صفحة Android Breakout. ويستخدم GLSurfaceView، التي تعمل في حلقة مفرغة تستدعي استدعاء onDrawFrame() الخاص بالتطبيق ثم تبدّل المخزن المؤقت. إذا كانت قائمة الانتظار المؤقت ممتلئة، فسينتظر eglSwapBuffers() الاتصال حتى يتوفر مخزن مؤقت. وتصبح المخازن الاحتياطية متاحة عند إطلاق SurfaceFlinger لها، وهو ما يحدث بعد الحصول على مساحة جديدة لعرضها. ولأنّ هذا يحدث في VSYNC، فإن توقيت حلقة الرسم يتطابق مع معدل التحديث. في الغالب.

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

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

مُصمِّم رقصات

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

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

يقدم نشاط "تسجيل تطبيق GL" في Grafika مثالاً على ذلك. في بعض الأجهزة (مثل Nexus 4 وNexus 5)، سيبدأ النشاط في إسقاط الإطارات إذا كنت تجلس وتشاهد فقط. يعد عرض GL أمرًا بسيطًا، ولكن في بعض الأحيان تتم إعادة رسم عناصر العرض، ويمكن أن يستغرق تمرير القياس/التخطيط وقتًا طويلاً جدًا إذا دخل الجهاز في وضع الطاقة المخفّضة. (وفقًا لموقع systrace، يستغرق الأمر 28 ملي ثانية بدلاً من 6 ملي ثانية بعد بطء الساعات في الإصدار Android 4.4. إذا سحبت إصبعك حول الشاشة، فهذا يعني أنك تتفاعل مع النشاط، وبذلك تظل سرعات الساعة عالية ولن تسقط أي إطار أبدًا.)

كان الإصلاح البسيط هو إسقاط إطار في استدعاء مصمم الرقصات إذا كان الوقت الحالي أكثر من N ملي ثانية بعد وقت VSYNC. من الناحية المثالية، يتم تحديد قيمة N بناءً على فواصل VSYNC المرصودة سابقًا. على سبيل المثال، إذا كانت مدة التحديث 16.7 ملي ثانية (60 لقطة في الثانية)، يمكنك إسقاط إطار إذا كان التأخر متأخرًا أكثر من 15 ملي ثانية.

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

إدارة سلسلة المحادثات

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

يستعين كل من Breakout و Record GL بسلاسل عارض العرض المخصصة، ويتم أيضًا تحديث حالة الرسوم المتحركة في سلسلة التعليمات هذه. وهذه طريقة معقولة طالما أنه يمكن تحديث حالة اللعبة بسرعة.

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

run() {
    Thread.sleep(100);
    synchronized (mLock) {
        moveBlock();
    }
}

(قد ترغب في تحديد وقت النوم من ساعة ثابتة لمنع الانجراف -- sleep() غير متسق تمامًا، وmoveBlock() يستغرق وقتًا غير صفري -- ولكن عليك فهمت.)

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

بالنسبة إلى أي مشهد بأي تعقيد، من الأفضل إنشاء قائمة بالأحداث القادمة مرتبة حسب وقت الاستيقاظ، والنوم حتى موعد الحدث التالي، لكنها الفكرة نفسها.