دراسة حالة - Inside Worldwide Maze

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

متاهة عالمية

تتمحور اللعبة حول استخدام ميزات HTML5 بشكل كبير. على سبيل المثال، يسترجع حدث DeviceOrientation بيانات الإمالة من الهاتف الذكي، والتي يتم إرسالها بعد ذلك إلى الكمبيوتر الشخصي عبر WebSocket، حيث يمكن للّاعبين طريقهم من خلال المساحات الثلاثية الأبعاد التي تم إنشاؤها بواسطة WebGL وWeb Workers.

في هذه المقالة، سأشرح لك بدقة كيفية استخدام هذه الميزات وعملية التطوير بشكل عام والنقاط الرئيسية للتحسين.

DeviceOrientation

يُستخدم حدث DeviceOrientation (مثال) لاسترداد بيانات الإمالة من الهاتف الذكي. عند استخدام addEventListener مع الحدث DeviceOrientation، يتم استدعاء استدعاء يتضمّن الكائن DeviceOrientationEvent كوسيطة على فترات زمنية منتظمة. تختلف الفواصل الزمنية نفسها باختلاف الجهاز المستخدم. على سبيل المثال، في iOS + Chrome وiOS + Safari، يتم استدعاء معاودة الاتصال كل 1/20 من الثانية تقريبًا، بينما في Android 4 + Chrome يتم استدعاءها كل 1/10 من الثانية تقريبًا.

window.addEventListener('deviceorientation', function (e) {
  // do something here..
});

يحتوي الكائن DeviceOrientationEvent على بيانات إمالة لكل من المحاور X وY وZ بالدرجات (وليس وحدات الراديان) (مزيد من المعلومات على HTML5Rocks). ومع ذلك، تختلف القيم المعروضة أيضًا باختلاف الجهاز والمتصفّح المستخدَم. يتم توضيح نطاقات القيم المعروضة الفعلية في الجدول أدناه:

اتجاه الجهاز.

القيم في أعلى الصفحة المميزة باللون الأزرق هي تلك المحددة في مواصفات W3C. وتتطابق تلك المميزة باللون الأخضر مع هذه المواصفات، بينما تنحرف المواصفات المميزة باللون الأحمر. للمفاجأة، لم تعرض سوى مجموعة Android-Firefox القيم التي تطابق المواصفات. ومع ذلك، عندما يتعلق الأمر بالتنفيذ، من المنطقي استيعاب القيم التي تحدث بشكلٍ متكرر. وبالتالي، تستخدم "متاهة Worldwide Maze" قيم إرجاع نظام التشغيل iOS كمعيار ويتم تعديلها بما يتناسب مع أجهزة Android وفقًا لذلك.

if android and event.gamma > 180 then event.gamma -= 360

ومع ذلك، لا يزال ذلك غير متوافق مع جهاز Nexus 10. على الرغم من أن جهاز Nexus 10 يعرض نفس نطاق القيم كأجهزة Android الأخرى، فإن هناك خطأ يعكس قيم بيتا وغاما. تتم معالجة هذه المشكلة بشكل منفصل. (ربما يتم الضبط افتراضيًا على الاتجاه الأفقي؟)

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

WebSocket

في متاهة Worldwide Maze، يتم توصيل هاتفك الذكي وجهاز الكمبيوتر الشخصي عبر WebSocket. وبشكل أكثر دقة، يتم ربط هذه الأجهزة عن طريق خادم ترحيل بينها، أي من الهاتف الذكي إلى الخادم إلى الكمبيوتر الشخصي. وهذا لأن WebSocket يفتقر إلى القدرة على ربط المتصفحات ببعضها البعض مباشرةً. (يتيح استخدام قنوات بيانات WebRTC إمكانية الاتصال من نظير إلى نظير ويلغي الحاجة إلى خادم إرسال، ولكن في وقت التنفيذ، لا يمكن استخدام هذه الطريقة إلا مع Chrome Canary وFirefox Nightly).

لقد اخترتُ تنفيذ هذه البيانات باستخدام مكتبة اسمها Socket.IO (v0.9.11)، وتتضمّن ميزات لإعادة الاتصال في حال انتهاء مهلة الاتصال أو انقطاع الاتصال به. لقد استخدمته مع NodeJS، حيث أظهرت تركيبة NodeJS + Socket.IO أفضل أداء من جانب الخادم في العديد من اختبارات تنفيذ WebSocket.

الإقران حسب الأرقام

  1. يتصل جهاز الكمبيوتر بالخادم.
  2. يعطي الخادم جهاز الكمبيوتر الشخصي رقمًا عشوائيًا ويتذكر مجموعة الرقم والكمبيوتر الشخصي.
  3. من جهازك الجوّال، حدِّد رقمًا واتصل بالخادم.
  4. إذا كان الرقم المحدد هو نفسه الرقم الذي يظهر على جهاز كمبيوتر متصل، سيتم إقران جهازك الجوّال بجهاز الكمبيوتر هذا.
  5. يحدث خطأ إذا لم يكن هناك جهاز كمبيوتر مخصّص.
  6. وعندما تأتي البيانات من جهازك الجوّال، يتم إرسالها إلى جهاز الكمبيوتر الشخصي الذي تم إقرانها معه، والعكس صحيح.

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

مزامنة علامات التبويب

تعمل ميزة "مزامنة علامات التبويب" الخاصة بمتصفِّح Chrome على تسهيل عملية الإقران بشكلٍ أكبر. باستخدامه، يمكن فتح الصفحات المفتوحة على جهاز الكمبيوتر على جهاز جوّال بسهولة (والعكس صحيح). ويضيف جهاز الكمبيوتر رقم الاتصال الصادر عن الخادم إلى عنوان URL للصفحة باستخدام history.replaceState.

history.replaceState(null, null, '/maze/' + connectionNumber)

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

وقت الاستجابة

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

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

مشكلة خوارزمية Nagle

عادةً ما يتم دمج خوارزمية Nagle في أنظمة التشغيل لتوفير اتصال فعّال عن طريق التخزين المؤقت على مستوى بروتوكول التحكم بالنقل (TCP)، لكنني اكتشفت أنّه لا يمكنني إرسال البيانات في الوقت الفعلي عندما كانت هذه الخوارزمية مفعَّلة. (على وجه التحديد عند الدمج مع الإقرار المتأخر في بروتوكول TCP؟ حتى بدون تأخير ACK، تحدث المشكلة نفسها في حال تأخّر ACK إلى درجة معيّنة بسبب عوامل مثل توفُّر الخادم في الخارج.)

لم تحدث مشكلة وقت استجابة Nagle مع WebSocket في Chrome لنظام التشغيل Android، والذي يتضمن الخيار TCP_NODELAY لإيقاف Nagle، ولكنها حدثت مع WebKit WebSocket المستخدم في متصفح Chrome لنظام التشغيل iOS، والذي لم يتم تفعيل هذا الخيار به. (واجهت شركة Safari، التي تستخدم مجموعة WebKit نفسها، هذه المشكلة أيضًا. تم إبلاغ شركة Apple بهذه المشكلة من خلال Google ويبدو أنّه تم حلها في الإصدار التطويري من WebKit.

عند حدوث هذه المشكلة، يتم دمج بيانات الإمالة المُرسَلة كل 100 ملي ثانية في أجزاء لا تصل إلا إلى جهاز الكمبيوتر الشخصي كل 500 ملي ثانية. لا يمكن أن تعمل اللعبة في ظلّ هذه الشروط، لذلك تتجنّب وقت الاستجابة هذا من خلال جعل جهة الخادم يرسل البيانات على فترات زمنية قصيرة (كل 50 ملي ثانية تقريبًا). أعتقد أنّ تلقّي ACK على فترات زمنية قصيرة يخدع خوارزمية Nagle وتفكر في إمكانية إرسال البيانات.

خوارزمية Nagle 1

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

خوارزمية Nagle 2

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

ALT_TEXT_HERE

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

هل هناك خطأ؟

على الرغم من أن المتصفح الافتراضي في نظام Android 4 (ICS) يحتوي على واجهة برمجة تطبيقات WebSocket، لا يمكن الاتصال به، مما يؤدي إلى حدوث حدث Socket.IOconnect_failed. تنتهي مهلة الاتصال داخليًا ولا يمكن لجهة الخادم التحقق من الاتصال أيضًا. (لم أختبر ذلك باستخدام WebSocket وحده، لذا قد تكون هناك مشكلة في Socket.IO).

توسيع نطاق خوادم الإرسال

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

فيزياء

من خلال لعبة محاكاة الفيزياء الثلاثية الأبعاد، يمكنك استخدام ألعاب محاكاة الفيزياء الثلاثية الأبعاد لتحريك الكرة داخل اللعبة (مثل دحرجة الهبوط أو الاصطدام بالأرض أو الاصطدام بالجدران أو جمع العناصر وغيرها). لقد استخدمتُ Ammo.js، وهو منفذ لمحرك الرموز الفيزيائية Bullet المستخدم على نطاق واسع في JavaScript باستخدام Emscripten، إلى جانب Physijs لاستخدامه كـ "عامل على الويب".

عمال الويب

Web Workers هي واجهة برمجة تطبيقات لتشغيل JavaScript في سلاسل محادثات منفصلة. عند إطلاق JavaScript كـ Web Worker، يتم تشغيلها كسلسلة محادثات منفصلة عن تلك التي استدعتها في الأصل، لذا يمكن تنفيذ المهام الصعبة مع الحفاظ على استجابة الصفحة. تستخدم Physijs عمال الويب بكفاءة لمساعدة محرك الفيزياء ثلاثي الأبعاد في العمل بسلاسة. تتعامل "متاهة Worldwide Maze" مع المحرّك الفيزيائي وعرض صور WebGL عند معدّل عرض إطارات مختلف تمامًا، وبالتالي حتى إذا انخفض عدد اللقطات في الثانية على جهاز ذي مواصفات منخفضة بسبب الحِمل الثقيل على عرض WebGL، سيحافظ المحرّك الفيزيائي نفسه على 60 لقطة في الثانية بدون أن يعيق عناصر التحكّم في اللعبة.

لقطات في الثانية

تعرض هذه الصورة عدد اللقطات في الثانية الناتج على جهاز Lenovo G570. يعرض المربع العلوي عدد اللقطات في الثانية لـ WebGL (عرض الصور)، ويعرض المربع السفلي عدد اللقطات في الثانية لمحرّك البحث الفيزيائي. وحدة معالجة الرسومات هي شريحة Intel HD Graphics 3000 مدمجة، لذا لم يصل عدد اللقطات في الثانية لعرض الصورة إلى 60 لقطة في الثانية المتوقَّعة. بما أنّ المحرّك الفيزيائي حقّق عدد اللقطات المتوقّع في الثانية، لا يكون أسلوب اللعب مختلفًا كثيرًا عن الأداء على آلة عالية المواصفات.

بما أنّ سلاسل المحادثات التي تتضمّن عمال الويب النشِطين لا تحتوي على كائنات لوحدة التحكُّم، يجب إرسال البيانات إلى سلسلة التعليمات الرئيسية من خلال postMessage لإنشاء سجلّات تصحيح الأخطاء. يؤدي استخدام console4Worker إلى إنشاء مكافئ لكائن وحدة التحكّم في Worker، ما يجعل عملية تصحيح الأخطاء أسهل بكثير.

مشغِّلو الخدمات

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

عروض أداء

أحيانًا تتجاوز المراحل التي تحتوي على أعداد مضلّعات عالية 100000 مضلّع، ولكن لم يتأثر الأداء بشكل خاص حتى إذا تم إنشاؤها بالكامل كـ Physijs.ConcaveMesh (btBvhTriangleMeshShape في نقطية).

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

عناصر شبح

يُطلق على العناصر التي ترصد الاصطدامات ولكن لا تؤثر على الاصطدامات، وبالتالي ليس لها أي تأثير على أجسام أخرى اسم "كائنات الشبح" في القائمة النقطية. على الرغم من أنّ أداة Physijs لا تتيح رسميًا كائنات الشبح، يمكن إنشاؤها هناك من خلال تعديل العلامات بعد إنشاء Physijs.Mesh. تستخدم متاهة Worldwide Maze كائنات الأشباح لرصد تصادم العناصر ونقاط الهدف.

hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)

بالنسبة إلى collision_flags، القيمة 1 هي CF_STATIC_OBJECT والقيمة 4 هي CF_NO_CONTACT_RESPONSE. جرِّب البحث في منتدى Bullet أو Stack Overflow أو وثائق نقطية للحصول على مزيد من المعلومات. نظرًا لأن Pysijs هو برنامج تضمين لـ Ammo.js في حين أن Ammo.js متطابقتان في الأساس مع Bullet، فيمكن أن تتم معظم الأشياء التي يمكن تنفيذها في Bullet في Physijs أيضًا.

مشكلة في Firefox 18

أدى تحديث Firefox من الإصدار 17 إلى 18 إلى تغيير طريقة تبادل Web Workers للبيانات، وبالتالي توقف برنامج Physijs عن العمل. تم الإبلاغ عن المشكلة على GitHub وتم حلّها بعد بضعة أيام. على الرغم من أنّ هذه الكفاءة المفتوحة المصدر أثارت إعجابي، إلا أنّ الحادثة ذكّرتني أيضًا بأنّ المتاهة العالمية تتضمّن عدّة أطر عمل مختلفة مفتوحة المصدر. أكتب هذه المقالة على أمل تقديم نوع من الملاحظات.

asm.js

على الرغم من أنّ هذا الأمر لا يتعلق بـ Worldwide Maze مباشرةً، فإنّ Ammo.js يدعم حاليًا asm.js المُعلَن عنها مؤخرًا في Mozilla (ليس بمستغرب، فقد تم إنشاء ملف asm.js بشكل أساسي لتسريع محتوى JavaScript الذي تم إنشاؤه من خلال Emscripten، وأنّ منشئ Emscripten هو أيضًا منشئ ملف Ammo.js). إذا كان Chrome يدعم استخدام ملف asm.js أيضًا، فمن المفترض أن ينخفض حمل الحوسبة في محرك الفيزياء بشكل كبير. كانت السرعة أسرع بشكل ملحوظ عند اختباره باستخدام Firefox Nightly. ربما يكون من الأفضل كتابة الأقسام التي تتطلب سرعة أكبر بلغة C/C++ ثم نقلها إلى JavaScript باستخدام Emscripten؟

WebGL

في ما يتعلق بتنفيذ WebGL، استخدمت المكتبة الأكثر تطورًا، three.js (r53). على الرغم من إصدار المراجعة 57 بالفعل بواسطة المراحل الأخيرة من التطوير، فقد تم إجراء تغييرات رئيسية على واجهة برمجة التطبيقات، لذلك تمسكت بالإصدار الأصلي للإصدار.

تأثير اللمعان

ويتم تطبيق تأثير اللمعان المضاف إلى قلب الكرة وعلى العناصر باستخدام نسخة بسيطة مما يُطلق عليه اسم "Kawase Method MGF". في حين أنّ طريقة كاواس تُشعِر جميع المناطق المضيئة بالإضاءة، تنشئ متاهة Worldwide Maze أهداف عرض منفصلة للمناطق التي تحتاج إلى التوهج. وذلك لأنه يجب استخدام لقطة شاشة لموقع الويب في زخارف المسرح، وسيؤدي استخراج جميع المناطق الساطعة ببساطة إلى جعل موقع الويب بأكمله لامعًا إذا كان، على سبيل المثال، خلفية بيضاء. فكرتُ أيضًا في معالجة كل شيء في النطاق العالي الديناميكية، لكنني صرفت النظر عن ذلك هذه المرة لأنّ التنفيذ سيكون معقدًا للغاية.

لمعان

يعرض الجزء العلوي الأيمن الممر الأول، حيث تم عرض مناطق التوهج بشكل منفصل ثم تم تطبيق تمويه عليها. يعرض أسفل يسار الصفحة الممر الثاني، حيث تم تقليل حجم الصورة بنسبة 50%، ثم تم تطبيق تمويه. يعرض الجزء العلوي الأيمن الممر الثالث، حيث تم تقليل الصورة مرة أخرى بنسبة 50% ثم تم تعتيمها. بعد ذلك، تم تراكب الثلاثة لإنشاء الصورة المركّبة النهائية المعروضة في أسفل اليسار. بالنسبة إلى التمويه الذي استخدمتُ VerticalBlurShader وHorizontalBlurShader، تم تضمينهما في three.js، لذلك لا يزال هناك مجال لإجراء مزيد من التحسين.

كرة عاكسة

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

أداة التظليل، أداة التظليل، أداة التظليل...

تتطلب WebGL أدوات تظليل (أدوات تظليل الرأس وعوامل تظليل الأجزاء) لجميع عمليات العرض. في حين أن أدوات التظليل المضمنة في three.js تسمح بالفعل بمجموعة واسعة من التأثيرات، فإن كتابة عناصرك الخاصة لا مفر منها للحصول على تظليل وتحسين أكثر تفصيلاً. نظرًا لأن Worldwide Maze تُبقي وحدة المعالجة المركزية مشغولة بمحركها الفيزيائي، حاولت استخدام وحدة معالجة الرسومات بدلاً من ذلك عن طريق الكتابة قدر الإمكان بلغة التظليل (GLSL)، حتى عندما تكون معالجة وحدة المعالجة المركزية (CPU) (عبر JavaScript) أسهل. تعتمد تأثيرات موجة المحيط على التظليل، بطبيعة الحال، كما هو الحال في الألعاب النارية عند نقاط الهدف وتأثير الشبكة المتداخلة عند ظهور الكرة.

كرات التظليل

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

شبكة التظليل

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

poly2tri

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

poly2tri

يوضح ما سبق كيفية مثلث المخطط الأزرق وإنشاء مضلعات حمراء.

تصفية متباينة الخواص

نظرًا لأن تخطيط MIP القياسي الخواص يقلل حجم الصور على كل من المحاور الأفقية والرأسية، فإن عرض المضلّعات من الزوايا المائلة يجعل الزخارف على أقصى مراحل المتاهة العالمية مثل زخارف طويلة أفقيًا ومنخفضة الدقة. وتعرض الصورة العلوية اليمنى في صفحة ويكيبيديا مثالاً جيدًا على ذلك. من الناحية العملية، يجب توفير المزيد من الدقة الأفقية، والتي يتم حلها WebGL (OpenGL) باستخدام طريقة تُدعى التصفية متباينة الخواص. في3.js، يؤدي تعيين قيمة أكبر من 1 لـ THREE.Texture.anisotropy إلى تفعيل التصفية متباينة الخواص. ومع ذلك، يُرجى العلم أنّ هذه الميزة إضافة وقد لا تكون متاحة في بعض وحدات معالجة الرسومات.

تحسين

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

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

التحسين

ما سبق هو نتائج تتبُّع ناتجة عن إنشاء خرائط بيئة لانعكاس الكرة. إنّ إدراج console.time وconsole.timeEnd في موقعَين يبدو أنّهما مناسبان في three.js يمنحنا رسمًا بيانيًا يشبه هذه الطريقة. يتدفق الوقت من اليسار إلى اليمين، وكل طبقة تشبه طبقة الاتصال. يسمح تضمين console.time ضمن console.time بالقياسات الإضافية. يظهر الرسم البياني العلوي بعد التحسين المسبق، بينما يظهر الجزء السفلي منه بعد التحسين. كما يوضّح الرسم البياني العلوي، تم استدعاء updateMatrix (على الرغم من اقتطاع الكلمة) لكل عرض من 0 إلى 5 أثناء التحسين المسبق. لقد عدّلتُها بحيث يتم طلبها مرة واحدة فقط، ومع ذلك، لأنّ هذه العملية تكون مطلوبة فقط عند تغيير موضع العناصر أو اتجاهها.

وبطبيعة الحال، فإنّ عملية التتبُّع نفسها تتطلّب الكثير من الموارد، لذا فإنّ إدراج console.time بشكل مفرط يمكن أن يؤدي إلى انحراف شديد عن الأداء الفعلي، ما يجعل من الصعب تحديد الجوانب التي يمكن تحسينها.

أداة تعديل الأداء

نظرًا إلى طبيعة الإنترنت، من المرجّح أن يتم تشغيل اللعبة على أنظمة بمواصفات متفاوتة على نطاق واسع. تم إطلاق لعبة Find Your Way to Oz في أوائل شباط (فبراير) باستخدام فئة تُسمى IFLAutomaticPerformanceAdjust لتقليل التأثيرات وفقًا للتقلبات في عدد اللقطات في الثانية، ما يضمن تشغيل المحتوى بسلاسة. تعتمد متاهة Worldwide Maze على الفئة نفسها في IFLAutomaticPerformanceAdjust، وتُعدّل المؤثرات بالشكل التالي لتسهيل اللعب قدر الإمكان:

  1. إذا انخفض عدد اللقطات في الثانية عن 45 لقطة في الثانية، سيتوقّف تحديث خرائط البيئة.
  2. وإذا ظل أقل من 40 لقطة في الثانية، سيتم تقليل دقة العرض إلى %70 (أي% 50 من نسبة السطح).
  3. وإذا ظل أقل من 40 لقطة في الثانية، سيتم التخلص من FXAA (التنقيط).
  4. وإذا ظل أقل من 30 لقطة في الثانية، سيتم التخلص من تأثيرات اللمعان.

تسرُّب الذاكرة

تمثل التخلص من العناصر بدقة نوعًا من المتاعب باستخدام three.js. لكن تركها وحدها ستؤدي بوضوح إلى تسرب الذاكرة، لذلك ابتكرت الطريقة أدناه. تشير السمة @renderer إلى السمة THREE.WebGLRenderer. (تستخدم النسخة الأخيرة من three.js طريقة مختلفة قليلاً في تخصيص الموقع، لذا ربما لن تعمل هذه الطريقة مع الأمر كما هي).

destructObjects: (object) =>
  switch true
    when object instanceof THREE.Object3D
      @destructObjects(child) for child in object.children
      object.parent?.remove(object)
      object.deallocate()
      object.geometry?.deallocate()
      @renderer.deallocateObject(object)
      object.destruct?(this)

    when object instanceof THREE.Material
      object.deallocate()
      @renderer.deallocateMaterial(object)

    when object instanceof THREE.Texture
      object.deallocate()
      @renderer.deallocateTexture(object)

    when object instanceof THREE.EffectComposer
      @destructObjects(object.copyPass.material)
      object.passes.forEach (pass) =>
        @destructObjects(pass.material) if pass.material
        @renderer.deallocateRenderTarget(pass.renderTarget) if pass.renderTarget
        @renderer.deallocateRenderTarget(pass.renderTarget1) if pass.renderTarget1
        @renderer.deallocateRenderTarget(pass.renderTarget2) if pass.renderTarget2

HTML

أنا شخصيًا أعتقد أن أفضل شيء في تطبيق WebGL هو القدرة على تصميم تخطيط الصفحة بتنسيق HTML. يُعد إنشاء واجهات ثنائية الأبعاد مثل عرض النتيجة أو النص في Flash أو openFrameworks (OpenGL) نوعًا من المشكلات. يحتوي Flash على الأقل على بيئة تطوير IDE، لكن يكون openFrameworks صعوبةً إذا لم تكن معتادًا على ذلك (قد يؤدي استخدام شيء مثل Cocos2D إلى تسهيل الأمر). ومن ناحية أخرى، يتيح HTML التحكم الدقيق في جميع جوانب تصميم الواجهة الأمامية باستخدام CSS، تمامًا كما هو الحال عند إنشاء مواقع الويب. وعلى الرغم من استحالة استخدام تأثيرات معقدة مثل تكثيف الجزيئات في شعار، إلا أنّه من الممكن استخدام بعض التأثيرات الثلاثية الأبعاد ضمن إمكانات "عمليات تحويل CSS". تتحرك تأثيرات النص "GOAL" و"TIME IS UP" في Worldwide Maze باستخدام مقياس في "نقل البيانات في CSS" (يتم تنفيذه باستخدام النقل العام). (من الواضح أن تدرجات الخلفية تستخدم WebGL.)

يكون لكل صفحة في اللعبة (العنوان أو RESULT أو RANKING أو غير ذلك) ملف HTML الخاص بها، وبعد تحميلها كنماذج، يتم استدعاء $(document.body).append() بالقيم المناسبة في الوقت المناسب. واجهنا عطلاً في تعذُّر ضبط أحداث الماوس ولوحة المفاتيح قبل الإلحاق، لذا لم تنجح محاولة el.click (e) -> console.log(e) قبل الإلحاق.

النشر على نطاق عالمي (i18n)

كان العمل باستخدام ترميز HTML سهلاً أيضًا لإنشاء نسخة اللغة الإنجليزية. اخترتُ استخدام i18next، وهي مكتبة i18n على الويب، لتلبية احتياجات نشر المحتوى على نطاق عالمي، والتي تمكنت من استخدامها بدون تعديل.

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

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

RequireJS

اخترتُ RequireJS كنظام لوحدة JavaScript. وتضم اللعبة 10,000 سطر من رمز المصدر مقسّمة إلى 60 فئة تقريبًا (= ملفات القهوة) ويتم تجميعها في ملفات js فردية. تحمِّل RequireJS هذه الملفات الفردية بالترتيب المناسب بناءً على التبعية.

define ->
  class Hoge
    hogeMethod: ->

يمكن استخدام الفئة المحددة أعلاه (hoge.coffee) على النحو التالي:

define ['hoge'], (Hoge) ->
  class Moge
    constructor: ->
      @hoge = new Hoge()
      @hoge.hogeMethod()

للعمل، يجب تحميل hoge.js قبل moge.js، وبما أن "hoge" قد تم تعيينه كوسيطة أولى لـ "define"، يتم دائمًا تحميل hoge.js أولاً (يتم استدعاؤه عند الانتهاء من تحميل hoge.js). تُسمى هذه الآلية AMD، ويمكن استخدام أي مكتبة تابعة لجهة خارجية مع النوع نفسه من معاودة الاتصال طالما أنها متوافقة مع AMD. وحتى المواقع الأخرى التي لا تعمل (على سبيل المثال، three.js)، ستحقّق أداءً مشابهًا طالما تم تحديد الإضافات مسبقًا.

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

r.js

يتضمّن RequireJS محسّنًا يسمى r.js. يؤدي ذلك إلى ضم مقتطف JavaScript الرئيسي مع جميع ملفات js التابعة في ملف واحد، ثم تصغيره باستخدام أداة UglifyJS (أو أداة تجميع كلمات المرور). ومن شأن هذا تقليل عدد الملفات وإجمالي مقدار البيانات التي يحتاج المتصفح إلى تحميلها. يبلغ الحجم الإجمالي لملف JavaScript في متاهة Worldwide Maze 2 ميغابايت تقريبًا، ويمكن تقليله إلى حوالي 1 ميغابايت باستخدام تحسين r.js. إذا كان من الممكن توزيع اللعبة باستخدام برنامج gzip، سيتم تقليل هذا الرقم إلى 250 كيلوبايت. (توجد مشكلة في GAE تؤدي إلى عدم السماح بنقل ملفات gzip بحجم 1 ميغابايت أو أكبر، لذا يتم توزيع اللعبة حاليًا بدون ضغط كحجم 1 ميغابايت من النص العادي).

أداة إنشاء المسرح

يتم إنشاء بيانات المرحلة على النحو التالي، ويتم إجراؤها بالكامل على خادم GCE في الولايات المتحدة:

  1. يتم إرسال عنوان URL للموقع الإلكتروني المطلوب تحويله إلى مرحلة عبر WebSocket.
  2. تأخذ خدمة PhantomJS لقطة شاشة، ويتم استرداد موضعَي علامات div وimg وإخراجهما بتنسيق JSON.
  3. استنادًا إلى لقطة الشاشة من الخطوة 2 وبيانات تحديد مواضع عناصر HTML، يحذف برنامج C++ المخصّص (OpenCV، Boost) المخصص المناطق غير الضرورية، وينشئ جُزرًا، ويربط الجزر بالجسور، ويحسب مواضع أشرطة الحماية، ومواضع العناصر، ويضبط نقطة الهدف، وما إلى ذلك. ويتم عرض النتائج بتنسيق JSON ويتم إرجاعها إلى المتصفح.

PhantomJS

PhantomJS هو متصفّح لا يتطلب أي شاشة. ويمكنها تحميل صفحات الويب بدون فتح النوافذ، وبالتالي يمكن استخدامها في الاختبارات الآلية أو لالتقاط لقطات شاشة من جهة الخادم. ومحرك المتصفح الخاص بها هو WebKit، وهو المحرك نفسه المستخدم في كل من Chrome وSafari، لذا فإن نتائج التنسيق وعملية تنفيذ JavaScript هي نفسها إلى حد كبير أو أقل من النتائج في المتصفحات القياسية.

باستخدام PhantomJS، يتم استخدام JavaScript أو CoffeeScript لكتابة العمليات التي تريد تنفيذها. إنّ الحصول على لقطات الشاشة أمر سهل جدًا، كما هو موضّح في هذا النموذج. كنت أعمل على خادم Linux (CentOS)، لذا كنت بحاجة إلى تثبيت خطوط لعرض الخطوط اليابانية (M+ fontS). ومع ذلك، يتم التعامل مع عرض الخط بشكل مختلف عن نظام التشغيل Windows أو Mac، وبالتالي قد يبدو الخط نفسه مختلفًا على الأجهزة الأخرى (الفرق بسيط على الرغم من ذلك).

يتم التعامل مع استرداد مواضع علامة img وdiv بالطريقة نفسها التي يتم التعامل بها مع الصفحات العادية. ويمكن أيضًا استخدام jQuery بدون أي مشاكل.

stage_builder

فكّرت في استخدام منهج يعتمد على نموذج العناصر في المستند (DOM) لإنشاء مراحل (على غرار أداة فحص العناصر الثلاثية الأبعاد في Firefox) وحاولت إجراء شيء مثل تحليل DOM في PhantomJS. ومع ذلك، فقد استندت في النهاية على منهج معالجة الصور. وتحقيقًا لهذه الغاية، كتبت برنامجًا يستخدم لغة C++ يستخدم OpenCV وBoost تحت اسم "stage_builder". وهي تنفذ ما يلي:

  1. لتحميل لقطة الشاشة وملفات JSON.
  2. لتحويل الصور والنصوص إلى "جزر".
  3. إنشاء الجسور لربط الجزر.
  4. التخلص من الجسور غير الضرورية لإنشاء متاهة.
  5. يضع عناصر كبيرة.
  6. يضع عناصر صغيرة.
  7. يتم وضع حواجز حماية.
  8. تنتج بيانات تحديد الموضع بتنسيق JSON.

في ما يلي شرح مفصّل لكل خطوة.

جارٍ تحميل لقطة الشاشة وملفات JSON

يتم استخدام cv::imread المعتاد لتحميل لقطات الشاشة. لقد اختبرتُ العديد من المكتبات لملفات JSON، ولكن بدا أنّ استخدام ملف picojson هو الأسهل في الاستخدام.

تحويل الصور والنصوص إلى "جزر"

إصدار المسرح

المعلومات الواردة أعلاه هي لقطة شاشة لقسم "الأخبار" على الموقع الإلكتروني aid-dcc.com (انقر لعرض الحجم الفعلي). يجب تحويل الصور والعناصر النصية إلى جزر. لعزل هذه الأقسام، يجب أن نحذف لون الخلفية الأبيض، بمعنى آخر، اللون الأكثر شيوعًا في لقطة الشاشة. إليك ما يبدو عليه الأمر بعد الانتهاء من ذلك:

إصدار المسرح

الأقسام البيضاء هي الجزر المحتملة.

النص دقيق جدًا وحادة، لذلك سنزيده باستخدام cv::dilate وcv::GaussianBlur وcv::threshold. محتوى الصورة غير متوفر أيضًا، لذا سنملأ هذه المناطق باللون الأبيض، استنادًا إلى إخراج بيانات علامة الصورة من PhantomJS. تبدو الصورة الناتجة على النحو التالي:

إصدار المسرح

يشكل النص الآن كتلاً مناسبة، وكل صورة تمثل جزيرة مناسبة.

إنشاء الجسور لربط الجزر

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

إصدار المسرح

القضاء على الجسور غير الضرورية لإنشاء متاهة

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

إصدار المسرح

وضع عناصر كبيرة

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

إصدار المسرح

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

وضع العناصر الصغيرة

إصدار المسرح

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

وضع حواجز للحماية

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

إصدار المسرح

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

إخراج بيانات تحديد الموضع بتنسيق JSON

استخدمت picojson للمخرجات أيضًا. وتكتب البيانات إلى إخراج قياسي، والذي يتلقاه المتصل (Node.js).

إنشاء برنامج C++ على جهاز Mac لتشغيله في Linux

تم تطوير اللعبة على نظام التشغيل Mac وتم نشرها في Linux، ولكن نظرًا لوجود OpenCV وBoost لكل من نظامي التشغيل، لم يكن التطوير نفسه صعبًا بعد إنشاء بيئة التجميع. لقد استخدمت أدوات سطر الأوامر في Xcode لتصحيح أخطاء الإصدار في نظام التشغيل Mac، ثم أنشأت ملف تهيئة باستخدام automake/autoconf بحيث يمكن تجميع الإصدار في Linux. ثم كان عليّ استخدام "تهيئة && make" في Linux لإنشاء الملف التنفيذي. واجهتُ بعض الأخطاء الخاصة بنظام التشغيل Linux بسبب الاختلافات في إصدار برنامج التحويل البرمجي ولكن تمكّنت من حلّها بسهولة نسبيًا باستخدام gdb.

الخلاصة

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