World Wide 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 قيمًا تتطابق مع المواصفات. ومع ذلك، عندما يتعلق الأمر بالتنفيذ، من المنطقي أن يتم استيعاب القيم التي تظهر بشكل متكرّر. لذلك، تستخدم World Wide Maze قيم الإرجاع في نظام التشغيل iOS بشكلٍ تلقائي وتتكيّف مع أجهزة Android وفقًا لذلك.
if android and event.gamma > 180 then event.gamma -= 360
لا يتوافق هذا الإجراء مع جهاز Nexus 10. على الرغم من أنّ جهاز Nexus 10 يعرض النطاق نفسه من القيم مثل أجهزة Android الأخرى، إلا أنّ هناك خطأ يؤدي إلى عكس قيمتَي بيتا وغاما. يتمّ التعامل مع هذه المشكلة بشكل منفصل. (هل يتم ضبط الإعداد التلقائي على الوضع الأفقي؟)
كما يوضّح ذلك، حتى إذا كانت واجهات برمجة التطبيقات التي تتضمّن الأجهزة المادية تتضمّن مواصفات محدّدة، لا يمكن ضمان أن تتطابق القيم المعروضة مع تلك المواصفات. لذلك، من المهم اختبارها على جميع الأجهزة المحتملة. ويعني ذلك أيضًا أنّه قد يتم إدخال قيم غير متوقّعة، ما يتطلّب إنشاء حلول بديلة. تطلب لعبة World Wide Maze من اللاعبين لأول مرة معايرة أجهزتهم كخطوة أولى من البرنامج التعليمي، ولكن لن تتم معايرة القيمة الصفرية بشكل صحيح إذا تلقّت قيم إمالة غير متوقّعة. لذلك، يتم ضبط حدّ زمني داخلي ويُطلب من اللاعب التبديل إلى عناصر التحكّم في لوحة المفاتيح إذا تعذّرت المعايرة خلال هذا الحدّ الزمني.
WebSocket
في لعبة World Wide Maze، يتم ربط هاتفك الذكي بالكمبيوتر الشخصي من خلال WebSocket. بتعبير أدق، يتم ربطهما من خلال خادم إعادة توجيه بينهما، أي من الهاتف الذكي إلى الخادم إلى الكمبيوتر الشخصي. ويعود السبب في ذلك إلى أنّ WebSocket لا يمكنه ربط المتصفّحات ببعضها مباشرةً. (يسمح استخدام قنوات بيانات WebRTC بالاتصال من جهاز إلى آخر ويزيل الحاجة إلى خادم إعادة توجيه، ولكن في وقت التنفيذ، لا يمكن استخدام هذه الطريقة إلا مع Chrome Canary وFirefox Nightly).
اخترت تنفيذ ذلك باستخدام مكتبة تُسمى Socket.IO (الإصدار 0.9.11)، والتي تتضمّن ميزات لإعادة الاتصال في حال انتهاء مهلة الاتصال أو انقطاعه. لقد استخدمت هذه المكتبة مع NodeJS، لأنّ هذه المجموعة من NodeJS وSocket.IO أظهرت أفضل أداء من جهة الخادم في العديد من اختبارات تنفيذ WebSocket.
الإقران حسب الأرقام
- يتصل جهاز الكمبيوتر بالخادم.
- يمنح الخادم جهاز الكمبيوتر رقمًا يتم إنشاؤه عشوائيًا ويتذكر مجموعة الرقم والكمبيوتر.
- من جهازك الجوّال، حدِّد رقمًا واتصل بالخادم.
- إذا كان الرقم المحدَّد هو نفسه الرقم الوارد من جهاز كمبيوتر متصل، يعني ذلك أنّ جهازك الجوّال مقترن بهذا الكمبيوتر.
- إذا لم يكن هناك جهاز كمبيوتر شخصي مخصّص، سيحدث خطأ.
- عندما تأتي البيانات من جهازك الجوّال، يتم إرسالها إلى الكمبيوتر الذي تم إقرانه به، والعكس صحيح.
يمكنك أيضًا إجراء عملية الربط الأولية من جهازك الجوّال بدلاً من ذلك. في هذه الحالة، يتم عكس الأجهزة ببساطة.
مزامنة علامات التبويب
تسهِّل ميزة "مزامنة علامات التبويب" الخاصة بمتصفّح 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، ولكن حدثت مع WebSocket في WebKit المستخدَم في Chrome لنظام التشغيل iOS، والذي لا يتضمّن هذا الخيار مفعّلاً. (واجه أيضًا Safari، الذي يستخدم WebKit نفسه، هذه المشكلة. تم إبلاغ Apple بالمشكلة من خلال Google، ويبدو أنّه تم حلّها في إصدار التطوير من WebKit.
عند حدوث هذه المشكلة، يتم دمج بيانات الميل المُرسَلة كل 100 ملي ثانية في أجزاء لا تصل إلى الكمبيوتر الشخصي إلا كل 500 ملي ثانية. لا يمكن تشغيل اللعبة في ظل هذه الظروف، لذا يتم تجنُّب هذه المدة من خلال إرسال البيانات من جهة الخادم على فترات قصيرة (كل 50 ملي ثانية تقريبًا). أعتقد أنّ تلقّي ACK
على فترات قصيرة يخدع خوارزمية Nagle ليعتقد أنّه لا بأس بإرسال البيانات.
يعرض الرسم البياني أعلاه فواصل البيانات الفعلية التي تمّ تلقّيها. ويشير ذلك إلى الفواصل الزمنية بين الحِزم، ويمثّل اللون الأخضر فواصل الإخراج ويمثّل اللون الأحمر فواصل الإدخال. يبلغ الحد الأدنى 54 مللي ثانية والحد الأقصى 158 مللي ثانية، ويبلغ متوسط القيمة 100 مللي ثانية تقريبًا. لقد استخدمتُ هنا هاتف iPhone مع خادم إعادة توجيه مُستضاف في اليابان. تبلغ مدة الإخراج والإدخال حوالي 100 ملي ثانية، ويكون التشغيل سلسًا.
في المقابل، يعرض هذا الرسم البياني نتائج استخدام الخادم في الولايات المتحدة. في حين أنّ فواصل الإخراج الخضراء تظل ثابتة عند 100 ملي ثانية، تتراوح فواصل الإدخال بين 0 ملي ثانية و500 ملي ثانية، ما يشير إلى أنّ الكمبيوتر الشخصي يتلقّى البيانات على شكل أجزاء.
أخيرًا، يعرض هذا الرسم البياني نتائج تجنُّب وقت الاستجابة من خلال توجيه الخادم لإرسال بيانات العناصر النائبة. على الرغم من أنّ أدائه ليس جيدًا مثل استخدام الخادم الياباني، من الواضح أنّ فواصل الإدخال تظلّ ثابتة نسبيًا عند 100 ملي ثانية تقريبًا.
هل هناك خطأ؟
على الرغم من أنّ المتصفّح التلقائي في Android 4 (ICS) يتضمّن واجهة برمجة التطبيقات WebSocket API، لا يمكنه الاتصال، ما يؤدي إلى حدوث حدث Socket.IO connect_failed. تنتهي مهلة الاتصال داخل التطبيق، ولا يمكن لخادم التطبيق أيضًا إثبات صحة الاتصال. (لم أختبر ذلك باستخدام WebSocket وحده، لذا قد تكون هناك مشكلة في Socket.IO).
توسيع نطاق خوادم الإرسال
بما أنّ دور خادم الإعادة ليس معقّدًا، من المفترض ألا يكون من الصعب توسيع نطاق الخوادم وزيادة عددها ما دام جهاز الكمبيوتر والجهاز الجوّال متّصلَين دائمًا بالخادم نفسه.
فيزياء
يتم تنفيذ جميع عمليات حركة الكرة داخل اللعبة (الكرة التي تتدحرج إلى أسفل التل، وتصطدم بالأرض، وتصطدم بالجدران، وتجمع العناصر، وما إلى ذلك) باستخدام محاكي فيزيائي ثلاثي الأبعاد. لقد استخدمت Ammo.js، وهو إصدار متوافق مع JavaScript من محرك الفيزياء Bullet المستخدَم على نطاق واسع باستخدام Emscripten، إلى جانب Physijs لاستخدامه كـ "Web Worker".
عمال الويب
Web Workers هي واجهة برمجة تطبيقات لتشغيل JavaScript في سلاسل محادثات منفصلة. يتم تشغيل JavaScript الذي تم إطلاقه كعامل Web Worker كسلسلة مهام منفصلة عن السلسلة التي تم استدعاؤه منها في الأصل، ما يتيح تنفيذ المهام الثقيلة مع الحفاظ على سرعة استجابة الصفحة. يستخدم Physijs Web Workers بكفاءة لمساعدة محرك الفيزياء الثلاثي الأبعاد المكثّف عادةً على العمل بسلاسة. يعالج World Wide Maze محرك الفيزياء وعرض صور WebGL بمعدّلات لقطات مختلفة تمامًا، لذا حتى إذا انخفض معدّل اللقطات على جهاز منخفض المواصفات بسبب كثافة عرض WebGL، سيحافظ محرك الفيزياء نفسه على معدّل 60 لقطة في الثانية تقريبًا ولن يعرقل عناصر التحكّم في اللعبة.
تعرض هذه الصورة معدّلات اللقطات الناتجة على جهاز Lenovo G570. يعرض المربّع العلوي عدد اللقطات في الثانية لبرنامج WebGL (عرض الصور)، ويعرض المربّع السفلي عدد اللقطات في الثانية لمحرك الفيزياء. وحدة معالجة الرسومات هي شريحة Intel HD Graphics 3000 مدمجة، لذا لم يصل معدل عرض اللقطات لعرض الصور إلى المعدل المتوقّع الذي يبلغ 60 لقطة في الثانية. ومع ذلك، بما أنّ محرّك الفيزياء حقّق معدّل عرض اللقطات المتوقّع، لا يختلف أداء اللعب كثيرًا عن الأداء على جهاز عالي المواصفات.
بما أنّ سلاسل المهام التي تتضمّن Web Workers نشطة لا تحتوي على عناصر وحدة تحكّم، يجب إرسال البيانات إلى سلسلة المهام الرئيسية من خلال postMessage لإنشاء سجلّات تصحيح الأخطاء. يؤدي استخدام console4Worker إلى إنشاء عنصر وحدة تحكّم مكافئ في Worker، ما يسهّل عملية تصحيح الأخطاء بشكل كبير.
تتيح لك الإصدارات الحديثة من Chrome ضبط نقاط توقّف عند تشغيل Web Workers، وهو أمر مفيد أيضًا لتصحيح الأخطاء. ويمكن العثور عليه في لوحة "العمال" في "أدوات المطوّرين".
الأداء
في بعض الأحيان، تتجاوز عدد المضلّعات في المراحل 100,000 مضلّع، ولكنّ الأداء لم يتأثر بشكل خاص حتى عند إنشائها بالكامل على شكل Physijs.ConcaveMesh
(btBvhTriangleMeshShape
في Bullet).
في البداية، انخفض معدّل عرض اللقطات مع زيادة عدد الأجسام التي تتطلّب رصد الاصطدامات، ولكن ساهم إيقاف المعالجة غير الضرورية في Physijs في تحسين الأداء. تم إجراء هذا التحسين على نسخة مُعدَّلة من مكتبة Physijs الأصلية.
الأجسام الوهمية
تُعرف "العناصر الشبحية" في Bullet بأنها العناصر التي تتضمّن ميزة رصد التصادم ولكنّها لا تتأثر عند التصادم وبالتالي لا تؤثّر في العناصر الأخرى. على الرغم من أنّ Physijs لا تتيح رسميًا استخدام الأجسام الشبحية، من الممكن إنشاؤها من خلال تعديل العلامات بعد إنشاء Physijs.Mesh
. يستخدم تطبيق World Wide 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 أو مستندات Bullet للحصول على مزيد من المعلومات. بما أنّ Physijs هو حزمة لـ Ammo.js وأنّ Ammo.js مطابق بشكل أساسي لـ Bullet، يمكن تنفيذ معظم الإجراءات التي يمكن تنفيذها في Bullet في Physijs أيضًا.
مشكلة Firefox 18
أدّى تحديث Firefox من الإصدار 17 إلى الإصدار 18 إلى تغيير طريقة تبادل Web Workers للبيانات، ونتيجةً لذلك توقّف Physijs عن العمل. تم الإبلاغ عن المشكلة على GitHub وتم حلّها بعد بضعة أيام. على الرغم من أنّ كفاءة البرامج مفتوحة المصدر هذه أبهرتُني، إلا أنّ الحادثة ذكّرتني أيضًا بأنّ World Wide Maze تتألف من عدة إطارات عمل مختلفة مفتوحة المصدر. أكتب هذه المقالة على أمل تقديم نوع من الملاحظات.
asm.js
على الرغم من أنّ هذا لا يرتبط بلعبة World Wide 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 MGF. ومع ذلك، في حين أنّ طريقة Kawase تُبرز جميع المناطق الساطعة، تُنشئ World Wide Maze أهدافًا منفصلة للعرض للمناطق التي تحتاج إلى توهج. ويعود السبب في ذلك إلى أنّه يجب استخدام لقطة شاشة لموقع إلكتروني لإنشاء مواد عرض المسرح، وسيؤدي استخراج جميع المناطق الساطعة إلى توهج الموقع الإلكتروني بالكامل إذا كان يحتوي على خلفية بيضاء مثلاً. لقد فكّرت أيضًا في معالجة كل شيء بتنسيق HDR، ولكنني قررت عدم تنفيذ ذلك هذه المرة لأنّ التنفيذ كان سيتّسم بالتعقيد الشديد.
يعرض الجزء العلوي الأيسر المرور الأول، حيث تم عرض مناطق التوهج بشكل منفصل ثم تم تطبيق تمويه. يعرض أسفل يمين الشاشة الخطوة الثانية، حيث تم تقليل حجم الصورة بنسبة% 50 ثم تم تطبيق التمويه. يعرض أعلى يسار الشاشة المرور الثالث، حيث تم تصغير الصورة مرة أخرى بنسبة% 50 ثم تم تمويهها. بعد ذلك، تمّت تداخل الصور الثلاث لإنشاء الصورة المركبة النهائية المعروضة في أسفل يمين الصفحة. بالنسبة إلى التمويه، استخدمت VerticalBlurShader
وHorizontalBlurShader
، المضمّنين في three.js، لذلك لا يزال هناك مجال لمزيد من التحسين.
كرة عاكسة
يستند الانعكاس على الكرة إلى عيّنة من three.js. يتم عرض جميع الاتجاهات من موضع الكرة ويتم استخدامها كخرائط للبيئة. يجب تعديل خرائط البيئة في كل مرة تتحرك فيها الكرة، ولكن بما أنّ عملية التعديل بمعدّل 60 لقطة في الثانية تكون كثيفة، يتم تعديلها كل ثلاثة لقطات بدلاً من ذلك. لا تكون النتيجة سلسة تمامًا مثل تعديل كل لقطة، ولكن لا يمكن ملاحظة الفرق تقريبًا ما لم يتم الإشارة إليه.
أداة تظليل، أداة تظليل، أداة تظليل…
تتطلّب WebGL استخدام برامج معالجة الصور (برامج معالجة رؤوس المضلّعات وبرامج معالجة أجزاء المضلّعات) لإجراء جميع عمليات العرض. على الرغم من أنّ أدوات التظليل المضمّنة في three.js تتيح مجموعة كبيرة من التأثيرات، لا مفر من كتابة أدواتك الخاصة للحصول على تظليل وتحسين أكثر تفصيلاً. بما أنّ لعبة World Wide Maze تشغّل وحدة المعالجة المركزية (CPU) من خلال محرّك الفيزياء، حاولت استخدام وحدة معالجة الرسومات بدلاً من ذلك من خلال كتابة أكبر قدر ممكن من الرموز البرمجية بلغة التظليل (GLSL)، حتى عندما كان من الأسهل معالجة وحدة المعالجة المركزية (من خلال JavaScript). تعتمد تأثيرات أمواج المحيط على أدوات تظليل الصور، كما هو الحال مع الألعاب النارية عند نقاط الأهداف وتأثير الشبكة المستخدَم عند ظهور الكرة.
تم الحصول على الصور أعلاه من اختبارات تأثير الشبكة المستخدَم عند ظهور الكرة. الشكل على يمين الصفحة هو الشكل المستخدَم في اللعبة، وهو مكوّن من 320 مضلّعًا. يستخدم التصميم في الوسط حوالي 5,000 مضلّع، ويستخدم التصميم على اليمين حوالي 300,000 مضلّع. حتى مع هذا العدد الكبير من المضلّعات، يمكن أن تحافظ المعالجة باستخدام مواد التشويش على معدّل ثابت للإطارات يبلغ 30 لقطة في الثانية.
يتم دمج العناصر الصغيرة المنتشرة في جميع أنحاء المسرح في شبكة واحدة، وتعتمد الحركة الفردية على مخطّطات التظليل التي تحرّك كلّ من رؤوس المضلّعات. هذا الاختبار يهدف إلى معرفة ما إذا كان الأداء سيتأثر بوجود أعداد كبيرة من العناصر. تم عرض حوالي 5,000 عنصر هنا، مكوّن من 20,000 مضلّع تقريبًا. لم يتأثّر الأداء على الإطلاق.
poly2tri
يتم إنشاء المراحل استنادًا إلى معلومات المخططات المستلَمة من الخادم، ثم يتم تحويلها إلى مضلّعات باستخدام JavaScript. إنّ عملية تحديد الموقع بالاستناد إلى المثلثات، وهي جزء أساسي من هذه العملية، لا يتم تنفيذها بشكل جيد باستخدام three.js وعادةً ما تؤدي إلى خطأ. لذلك قررتُ دمج مكتبة مختلفة للمثلثات تُسمى poly2tri بنفسي. تبيّن أنّ مكتبة three.js حاولت إجراء الشيء نفسه في السابق، لذا تمكّنت من تشغيلها ببساطة عن طريق تعليق جزء منها. نتيجةً لذلك، انخفض عدد الأخطاء بشكل كبير، ما سمح بإضافة المزيد من المراحل القابلة للعب. يستمر الخطأ في بعض الأحيان، ولأسباب غير معروفة، يعالج poly2tri الأخطاء من خلال إصدار تنبيهات، لذلك عدّلته لطرح استثناءات بدلاً من ذلك.
يوضّح المثال أعلاه كيفية تقسيم المخطط الأزرق إلى مثلثات وإنشاء المضلّعات الحمراء.
التصفية المتباينة الخواص
بما أنّ ميزة "تعيين MIP متماثل" العادية تُقلّل حجم الصور على كل من محورَي العرض والارتفاع، فإنّ عرض المضلّعات من زوايا مائلة يجعل النسيج في الطرف البعيد من مراحل World Wide Maze يبدو مثل نسيج منخفض الدقة وممدّد أفقيًا. تعرض الصورة في أعلى يسار صفحة ويكيبيديا هذه مثالاً جيدًا على ذلك. من الناحية العملية، يلزم الحصول على درجة دقة أكبر في الاتجاه الأفقي، ما يحلّه WebGL (OpenGL) باستخدام طريقة تُعرف باسم الفلترة غير المتجانسة. في three.js، يؤدي ضبط قيمة أكبر من 1 لـ THREE.Texture.anisotropy
إلى تفعيل الفلترة غير المتجانسة. ومع ذلك، هذه الميزة هي إضافة وقد لا تكون متاحة في بعض وحدات معالجة الرسومات.
تحسين
كما تشير مقالة أفضل الممارسات المتعلّقة بWebGL هذه، فإنّ الطريقة الأكثر أهمية لتحسين أداء WebGL (OpenGL) هي تقليل عدد طلبات الرسم. خلال المرحلة الأولى من تطوير World Wide Maze، كانت جميع الجزر والجسور وحواجز الحماية داخل اللعبة عناصر منفصلة. وقد أدّى ذلك أحيانًا إلى أكثر من 2,000 طلب رسم، ما جعل المراحل المعقدة غير قابلة للاستخدام. ومع ذلك، بعد تجميع الأنواع نفسها من العناصر في شبكة واحدة، انخفض عدد طلبات الرسم إلى خمسين طلبًا تقريبًا، ما أدّى إلى تحسين الأداء بشكلٍ كبير.
لقد استخدمت ميزة تتبُّع 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
لتقليل التأثيرات وفقًا للتقلبات في معدّل عرض اللقطات، ما يساعد في ضمان تشغيل سلس. تستند لعبة World Wide Maze إلى فئة IFLAutomaticPerformanceAdjust
نفسها وتُقلِّل من تأثيراتها بالترتيب التالي لتسهيل تجربة اللعب قدر الإمكان:
- إذا انخفض عدد اللقطات في الثانية عن 45 لقطة في الثانية، سيتوقف تحديث خرائط البيئة.
- إذا استمر عدد اللقطات في الثانية أقل من 40 لقطة، يتم تقليل درجة دقة العرض إلى %70 (%50 من نسبة السطح).
- إذا استمرت معدّلات اللقطات في الثانية بالانخفاض إلى أقل من 40 لقطة في الثانية، يتم إيقاف ميزة FXAA (التمويه).
- إذا استمرت السرعة في الانخفاض إلى ما دون 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 على بيئة تطوير متكاملة على الأقل، ولكنّ openFrameworks صعبة إذا لم تكن معتادًا عليها (قد يسهّل استخدام أداة مثل Cocos2D هذه العملية). من ناحية أخرى، يسمح HTML بالتحكم الدقيق في جميع جوانب تصميم الواجهة الأمامية باستخدام CSS، تمامًا كما هو الحال عند إنشاء المواقع الإلكترونية. على الرغم من أنّ التأثيرات المعقدة، مثل تكثيف الجسيمات في شعار، مستحيلة، إلا أنّ بعض التأثيرات الثلاثية الأبعاد ضمن إمكانات CSS Transforms ممكنة. تمّت إضافة تأثيرات متحركة للنصّين "GOAL" و"TIME IS UP" في لعبة World Wide Maze باستخدام مقياس الانتقال في CSS (تمّ تنفيذه باستخدام Transit). (من الواضح أنّ تدرجات الخلفية تستخدم WebGL).
تحتوي كل صفحة في اللعبة (العنوان وRESULT وRANKING وما إلى ذلك) على ملف HTML خاص بها، وبعد تحميلها كنموذج، يتم استدعاء $(document.body).append()
بالقيم المناسبة في الوقت المناسب. كان من بين المشاكل التي واجهناها أنّه لا يمكن ضبط أحداث الماوس ولوحة المفاتيح قبل الإضافة، لذا لم تنجح محاولة el.click (e) -> console.log(e)
قبل الإضافة.
النشر على نطاق عالمي (i18n)
وكان من السهل أيضًا استخدام HTML لإنشاء النسخة باللغة الإنجليزية. اخترت استخدام i18next، وهي مكتبة تدويل على الويب، لتلبية احتياجاتي المتعلّقة بالترجمة والنشر في بلدان متعددة، وتمكّنت من استخدامها كما هي بدون أي تعديل.
تم تعديل النص داخل اللعبة وترجمته في جدول بيانات "مستندات Google". بما أنّ i18next تتطلّب ملفّات JSON، تصدّرت جداول البيانات إلى TSV ثمّ حوّلتها باستخدام محوِّل مخصّص. أجريتُ الكثير من التعديلات قبل الإصدار مباشرةً، لذا كان من الأسهل كثيرًا أن أبرمِج عملية التصدير من جدول بيانات "مستندات Google".
تعمل أيضًا ميزة الترجمة التلقائية في Chrome بشكلٍ طبيعي لأنّ الصفحات مبنية باستخدام HTML. ومع ذلك، لا يتمكّن في بعض الأحيان من رصد اللغة بشكل صحيح، بل يخطئ في تحديدها على أنّها لغة مختلفة تمامًا (مثلاً، الفيتنامية)، لذا هذه الميزة غير مفعّلة حاليًا. (يمكن إيقاف هذه الميزة باستخدام العلامات الوصفية).
RequireJS
اخترتُ RequireJS كنظام وحدات JavaScript. يتم تقسيم 10,000 سطر من رمز المصدر الخاص باللعبة إلى 60 فئة تقريبًا (= ملفات coffee) ويتم تجميعها في ملفات 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 الرئيسي مع جميع ملفات JavaScript التابعة في ملف واحد، ثم تصغيره باستخدام UglifyJS (أو Closure Compiler). ويؤدي ذلك إلى تقليل عدد الملفات والكمية الإجمالية للبيانات التي يحتاج المتصفّح إلى تحميلها. يبلغ إجمالي حجم ملف JavaScript للعبة World Wide Maze حوالي 2 ميغابايت، ويمكن تقليل هذا الحجم إلى حوالي 1 ميغابايت من خلال تحسين r.js. وإذا كان من الممكن توزيع اللعبة باستخدام gzip، سيتم تقليل هذا الحجم إلى 250 كيلوبايت. (تواجه Google App Engine مشكلة تمنع نقل ملفات gzip التي تبلغ 1 ميغابايت أو أكثر، لذا يتم حاليًا توزيع اللعبة بدون ضغط بحجم 1 ميغابايت من النص العادي).
أداة إنشاء العروض
يتم إنشاء بيانات المرحلة على النحو التالي، ويتم تنفيذها بالكامل على خادم GCE في الولايات المتحدة:
- يتم إرسال عنوان URL للموقع الإلكتروني المطلوب تحويله إلى مرحلة من خلال WebSocket.
- يلتقط PhantomJS لقطة شاشة، ويتم استرداد مواضع علامتَي div وimg وعرضها بتنسيق JSON.
- استنادًا إلى لقطة الشاشة من الخطوة 2 وبيانات تحديد موضع عناصر HTML، يحذف برنامج C++ (OpenCV وBoost) المخصّص المناطق غير الضرورية وينشئ جزرًا ويربط الجزر بجسور ويحسب موضع حاجز الحماية والعناصر ويضبط نقطة الهدف وما إلى ذلك. ويتم عرض النتائج بتنسيق JSON وإعادتها إلى المتصفّح.
PhantomJS
PhantomJS هو متصفّح لا يتطلّب شاشة. ويمكنه تحميل صفحات الويب بدون فتح النوافذ، لذا يمكن استخدامه في الاختبارات المبرمَجة أو لالتقاط لقطات شاشة من جهة الخادم. يستخدم المتصفّح محرك WebKit نفسه المستخدَم في Chrome وSafari، لذا فإنّ تنسيقه ونتائج تنفيذ JavaScript متطابقة تقريبًا مع المتصفّحات العادية.
باستخدام PhantomJS، يتم استخدام JavaScript أو CoffeeScript لكتابة العمليات التي تريد تنفيذها. من السهل جدًا التقاط لقطات شاشة، كما هو موضّح في هذا العيّنة. كنت أعمل على خادم Linux (CentOS)، لذلك كنت بحاجة إلى تثبيت خطوط لعرض اللغة اليابانية (M+ FONTS). وحتى في هذه الحالة، يتم التعامل مع عرض الخط بشكل مختلف عن نظام التشغيل Windows أو Mac OS، لذا يمكن أن يبدو الخط نفسه مختلفًا على الأجهزة الأخرى (على الرغم من أنّ الفرق بسيط).
يتم التعامل مع استرداد مواضع علامة img وdiv بشكل أساسي بالطريقة نفسها المتّبعة في الصفحات العادية. ويمكن أيضًا استخدام jQuery بدون أي مشاكل.
stage_builder
في البداية، فكّرتُ في استخدام نهج يستند إلى DOM بشكل أكبر لإنشاء المراحل (على غرار Firefox 3D Inspector) وحاولتُ إجراء تحليل DOM في PhantomJS. في النهاية، اخترتُ نهج معالجة الصور. لهذا الغرض، كتبتُ برنامجًا بلغة C++ يستخدم OpenCV وBoost ويُسمى "stage_builder". وينفِّذ ما يلي:
- تحميل لقطة الشاشة وملفات JSON
- تحويل الصور والنصوص إلى "جزر"
- إنشاء جسور لربط الجزر
- إزالة الجسور غير الضرورية لإنشاء متاهة
- وضع العناصر الكبيرة
- أماكن وضع العناصر الصغيرة
- أماكن حواجز الحماية
- إخراج بيانات تحديد الموقع الجغرافي بتنسيق JSON
في ما يلي تفاصيل كل خطوة.
تحميل لقطة الشاشة وملفات JSON
يتم استخدام الرمز cv::imread
المعتاد لتحميل لقطات الشاشة. لقد اختبرت عدة مكتبات لملفات JSON، ولكن picojson بدت هي الأسهل في الاستخدام.
تحويل الصور والنصوص إلى "جزر"
لقطة الشاشة أعلاه هي لقسم "الأخبار" في الموقع الإلكتروني aid-dcc.com (انقر لعرض الحجم الفعلي). يجب تحويل عناصر الصور والنصوص إلى جزر. لعزل هذه الأقسام، علينا حذف لون الخلفية الأبيض، أي اللون الأكثر شيوعًا في لقطة الشاشة. إليك الشكل الذي سيظهر به المحتوى بعد إجراء ذلك:
الأقسام البيضاء هي الجزر المحتملة.
النص رقيق جدًا وحاد، لذا سنزيد من سمكه باستخدام cv::dilate
وcv::GaussianBlur
وcv::threshold
. لا يتوفّر أيضًا محتوى الصورة، لذا سنملؤها باللون الأبيض استنادًا إلى بيانات علامة img التي يتم عرضها من PhantomJS. تظهر الصورة الناتجة على النحو التالي:
يشكّل النص الآن مجموعات مناسبة، وتكون كل صورة جزيرة مناسبة.
إنشاء جسور لربط الجزر
بعد أن تصبح الجزر جاهزة، يتم ربطها بجسور. تبحث كل جزيرة عن الجزر المجاورة على يمينها ويسارها وأعلىها وأسفلها، ثم تربط جسرًا بأقرب نقطة في أقرب جزيرة، ما يؤدي إلى ظهور ما يلي:
إزالة الجسور غير الضرورية لإنشاء متاهة
سيؤدي الاحتفاظ بجميع الجسور إلى تسهيل التنقل في المرحلة، لذا يجب إزالة بعضها لإنشاء متاهة. يتم اختيار جزيرة واحدة (مثل الجزيرة في أعلى يمين الشاشة) كنقطة بداية، ويتم حذف جميع الجسور التي تربط هذه الجزيرة باستثناء جسر واحد (يتم اختياره عشوائيًا). بعد ذلك، يتم إجراء الشيء نفسه للجزيرة التالية المرتبطة بالجسر المتبقّي. بعد أن يصل المسار إلى طريق مسدود أو يعود إلى جزيرة سبق زيارتها، يعود إلى نقطة تسمح بالوصول إلى جزيرة جديدة. تكتمل المتاهة بعد معالجة جميع الجزر بهذه الطريقة.
وضع العناصر الكبيرة
يتم وضع عنصر واحد أو أكثر كبير على كل جزيرة استنادًا إلى أبعادها، ويتم الاختيار من بين النقاط الأبعد عن حواف الجزر. على الرغم من أنّها غير واضحة جدًا، تظهر هذه النقاط باللون الأحمر أدناه:
من بين كل هذه النقاط المحتملة، يتم ضبط النقطة في أعلى يمين الصفحة على أنّها نقطة البداية (الدائرة الحمراء)، ويتم ضبط النقطة في أسفل يمين الصفحة على أنّها الهدف (الدائرة الخضراء)، ويتم اختيار ست نقاط كحد أقصى من النقاط المتبقية لموضع العنصر الكبير (الدائرة البنفسجية).
وضع عناصر صغيرة
يتم وضع أعداد مناسبة من العناصر الصغيرة على طول خطوط على مسافات محددة من حواف الجزيرة. تعرض الصورة أعلاه (التي ليست من aid-dcc.com) خطوط مواضع الإعلانات المعروضة باللون الرمادي، وهي مُعدَّلة ومُثبَّتة على فترات منتظمة من حواف الجزيرة. تشير النقاط الحمراء إلى مواضع وضع العناصر الصغيرة. بما أنّ هذه الصورة من إصدار في مرحلة التطوير، يتم ترتيب العناصر في خطوط مستقيمة، ولكن في الإصدار النهائي يتم توزيع العناصر بشكل غير منتظم إلى حد ما على جانبي الخطوط الرمادية.
وضع حواجز
يتم وضع حواجز الحماية بشكل أساسي على طول الحدود الخارجية للجزر، ولكن يجب قطعها عند الجسور للسماح بالوصول. أثبتت مكتبة Geometry من Boost أنّها مفيدة لهذا الغرض، إذ سهّلت العمليات الحسابية الهندسية، مثل تحديد مكان تقاطع بيانات حدود الجزيرة مع الخطوط على جانبَي الجسر.
الخطوط الخضراء التي تحدد الجزر هي حواجز الحماية. قد يكون من الصعب الرؤية في هذه الصورة، ولكن لا تظهر خطوط خضراء في أماكن الجسور. هذه هي الصورة النهائية المستخدَمة لتصحيح الأخطاء، حيث يتم تضمين جميع العناصر التي يجب إخراجها إلى ملف JSON. النقاط الزرقاء الفاتحة هي عناصر صغيرة، والنقاط الرمادية هي نقاط إعادة تشغيل مقترَحة. عندما تسقط الكرة في المحيط، يتم استئناف اللعبة من أقرب نقطة إعادة تشغيل. يتم ترتيب نقاط إعادة البدء بالطريقة نفسها تقريبًا التي يتم بها ترتيب العناصر الصغيرة، وذلك على فترات منتظمة وبمسافة محددة من حافة الجزيرة.
إخراج بيانات تحديد الموقع الجغرافي بتنسيق JSON
لقد استخدمتُ أيضًا picojson للإخراج. ويُكتب البيانات في الإخراج العادي، الذي يتلقّاه المُتصل (Node.js) بعد ذلك.
إنشاء برنامج C++ على جهاز Mac ليتم تشغيله في Linux
تم تطوير اللعبة على جهاز Mac ونشرها على Linux، ولكن بما أنّ OpenCV وBoost متوفّران لكلا نظامَي التشغيل، لم تكن عملية التطوير نفسها صعبة بعد إنشاء بيئة الترجمة. لقد استخدمتُ "أدوات سطر الأوامر" في Xcode لتصحيح أخطاء عملية الإنشاء على جهاز Mac، ثم أنشأتُ ملف إعداد باستخدام automake/autoconf حتى يمكن تجميع عملية الإنشاء في Linux. بعد ذلك، كان عليّ ببساطة استخدام "configure && make" في Linux لإنشاء الملف القابل للتنفيذ. واجهت بعض الأخطاء المتعلّقة بنظام التشغيل Linux بسبب الاختلافات في إصدار المُجمِّع، ولكن تمكّنت من حلّها بسهولة نسبية باستخدام gdb.
الخاتمة
يمكن إنشاء لعبة مماثلة باستخدام Flash أو Unity، ما سيتيح لك الاستفادة من مزايا عديدة. ومع ذلك، لا يتطلّب هذا الإصدار أيّ إضافات، وقد تبيّن أنّ ميزات التنسيق في HTML5 وCSS3 فعّالة للغاية. من المهم بالتأكيد توفُّر الأدوات المناسبة لكل مهمة. لقد فاجأني شخصيًا مستوى الأداء الذي حقّقته اللعبة، علمًا بأنّها تم إنشاؤها بالكامل باستخدام HTML5. وعلى الرغم من أنّها لا تزال غير مكتملة في العديد من الجوانب، إلا أنّني أتطلع إلى رؤية مستوى تطورها في المستقبل.