دراسة حالة - Bouncy Mouse

مقدمة

ماوس نطاط

بعد نشر تطبيق Boungy Mouse على نظامي التشغيل iOS وAndroid في نهاية العام الماضي، تعلّمت بعض الدروس المهمة جدًا. وكان من بين هذه العوامل أن الدخول إلى سوق راسخ هو أمر صعب. كان اكتساب زخم شديد صعوبة في سوق Android المليء بدرجة كبيرة، وكان ذلك أمرًا سهلاً، وعلى الرغم من قلة انتشار البورصة، كان ذلك أمرًا سهلاً. في ظل هذه التجربة، أرى فرصة مثيرة للاهتمام على "سوق Chrome الإلكتروني". على الرغم من أن السوق الإلكتروني ليس فارغًا بأي شكل من الأشكال، فقد بدأ كتالوج الألعاب العالية الجودة المستندة إلى HTML5 في النضوج. بالنسبة إلى مطوّر التطبيقات الجديد، يعني هذا أنّه من السهل جدًا إنشاء مخططات الترتيب واكتساب فرص ظهور جديدة. مع وضع هذه الفرصة في الاعتبار، بدأتُ بنقل لعبة Boungy Mouse إلى HTML5 على أمل أن أتمكّن من تقديم أحدث تجربة لعب لي لقاعدة جديدة ومثيرة من المستخدمين. في دراسة الحالة هذه، سأتحدّث قليلاً عن العملية العامة لنقل لعبة Boouncey Mouse إلى HTML5، ثم سنتعمق أكثر في ثلاثة جوانب مثيرة للاهتمام: الصوت والأداء وتحقيق الربح.

نقل لعبة C++ إلى HTML5

يتوفر تطبيق Boungy Mouse حاليًا على أنظمة Android(C++ ) وiOS (C++ ) وWindows Phone 7 (C#) وChrome (Javascript). ويؤدي ذلك أحيانًا إلى طرح السؤال التالي: كيف تكتب لعبة يمكن نقلها بسهولة إلى منصات متعددة؟ أشعر أنّ الناس يأملون في الحصول على نقطة سحرية يمكنهم استخدامها لتحقيق هذا المستوى من سهولة التنقل دون اللجوء إلى استخدام منفذ يد. للأسف، لست متأكدًا من توفّر مثل هذا الحل حتى الآن (أقرب شيء هو على الأرجح إطار عمل PlayN من Google أو محرّك Unity، ولكن لا يحقّق أي من هذين الحلَّين جميع الأهداف التي كنت تهمّني. كان نهجي في الواقع من خلال منفذ اليد. لقد كتبت إصدار iOS/Android لأول مرة في C++ ، ثم نقلت هذا الرمز إلى كل نظام أساسي جديد. ورغم أنّ ذلك قد يبدو عملاً كبيرًا، لم يستغرق إكمال كلّ من إصدارَي WP7 وChrome أكثر من أسبوعَين. والآن، هناك سؤالٌ مُلح هو، هل يمكن تنفيذ أي إجراء لتسهيل نقل قاعدة الرموز البرمجية يدويًا؟ ساعدتني بعض الأشياء في ذلك:

إبقاء قاعدة التعليمات البرمجية صغيرة

قد يبدو ذلك واضحًا، إلا أنّ السبب الرئيسي هو تمكّني من نقل اللعبة بسرعة كبيرة. يبلغ رمز عميل Boungy Mouse حوالي 7000 سطر فقط من C++. 7000 سطر من الرمز ليس شيئًا، ولكنه صغير بما يكفي لإدارته. انتهى الأمر بكلا إصداري C# وJavascript من رمز العميل بنفس الحجم تقريبًا. إنّ الإبقاء على قاعدة الرموز البرمجية صغيرةً تصل إلى ممارستَين أساسيتَين: عدم كتابة أي رموز زائدة وتوفير أكبر قدر ممكن من الميزات في مرحلة ما قبل المعالجة (غير وقت التشغيل) للرمز البرمجي. قد يبدو عدم كتابة أي تعليمة برمجية زائدة واضحًا، ولكنه شيء واحد دائمًا أكافحه مع نفسي. غالبًا ما أرغب في كتابة فئة/دالة مساعد لأي شيء يمكن تضمينه في المساعد. ومع ذلك، إذا كنت تخطط بالفعل لاستخدام مساعد عدة مرات، فعادة ما ينتهي الأمر بتضخم التعليمات البرمجية الخاصة بك. مع تطبيق Boungy Mouse، كنت حريصًا على عدم كتابة مساعد ما لم أستخدمه ثلاث مرات على الأقل. عندما كتبت درسًا مساعدًا، حاولت أن أجعله نظيفًا وسهل الاستخدام وقابلاً لإعادة الاستخدام لمشروعاتي المستقبلية. من ناحية أخرى، عند كتابة التعليمات البرمجية لتطبيق Boungy Mouse فقط، مع احتمال ضئيل بإعادة الاستخدام، كان تركيزي كان على إنجاز مهمة الترميز بأسرع وقت ممكن، حتى لو لم تكن هذه هي الطريقة "الأجمل" لكتابة التعليمات البرمجية. كان الجزء الثاني والأكثر أهمية من إبقاء قاعدة التعليمات البرمجية صغيرًا هو الدفع قدر الإمكان إلى خطوات المعالجة المسبقة. إذا كان بإمكانك تنفيذ مهمة وقت التشغيل ونقلها إلى إحدى مهام المعالجة المسبقة، لن تعمل اللعبة بشكل أسرع فحسب، بل لن يكون عليك نقل الرمز إلى كل منصة جديدة. ولتقديم مثال على ذلك، خزّنتُ في الأصل بيانات الأشكال الهندسية للمستوى كتنسيق لم تتم معالجته إلى حدّ ما، حيث أتمّ تجميع مخازن رأس OpenGL/WebGL الفعلية في وقت التشغيل. استغرق هذا الإجراء بعض الإعدادات بالإضافة إلى بضع مئات من الأسطر من رمز وقت التشغيل. وفي وقت لاحق، نقلتُ هذا الرمز إلى خطوة المعالجة المسبقة، وكتبت مخازن رأسية معبأة بالكامل OpenGL/WebGL في وقت التجميع. وكان المقدار الفعلي من الرموز البرمجية مماثلاً تقريبًا، ولكن تم نقل هذه المئات من الأسطر إلى خطوة المعالجة المسبقة، ما يعني أنني لم أضطر أبدًا إلى نقلها إلى أي منصات جديدة. يتوفر الكثير من الأمثلة على ذلك في تطبيق Boungy Mouse، وسيختلف ممكن ما يمكن من لعبة إلى أخرى، ولكن ترقَّبوا ما سيحدث في وقت التشغيل.

لا تأخذ التبعيات التي لا تحتاجها

سبب آخر لسهولة نقل Boungy Mouse هو أنه لا يحتوي على تبعيات تقريبًا. يلخص المخطط التالي تبعيات المكتبة الرئيسية لتطبيق Boungy Mouse لكل نظام أساسي:

Android iOS HTML5 WP7
الرسومات OpenGL ES OpenGL ES WebGL خوارزمية XNA
صوت OpenSL ES OpenAL محتوى صوتي على الويب خوارزمية XNA
فيزياء Box2D Box2D Box2D.js Box2D.xna

هذا كل شيء تقريبًا. لم يتم استخدام مكتبات كبيرة تابعة لجهات خارجية، باستثناء Box2D، وهي قابلة للنقل على جميع الأنظمة الأساسية. بالنسبة إلى الرسومات، يعيّن كل من WebGL وXNA بنسبة 1:1 تقريبًا باستخدام OpenGL، لذلك لم تكن هذه مشكلة كبيرة. كانت المكتبات الفعلية مختلفة في مجال الصوت فقط. ومع ذلك، كان الرمز الصوتي في Boungy Mouse صغيرًا (حوالي مئات السطور من التعليمات البرمجية الخاصة بالنظام الأساسي)، لذلك لم تكن هذه مشكلة كبيرة. يعني عدم وجود Boungy Mouse خالية من المكتبات الكبيرة غير المحمولة أن منطق رمز وقت التشغيل يمكن أن يكون هو نفسه تقريبًا بين الإصدارات (على الرغم من تغيير اللغة). بالإضافة إلى ذلك، يساعدنا ذلك في تجنُّب الوقوع في سلسلة أدوات غير محمولة. تم سؤالي ما إذا كان الترميز باستخدام OpenGL/WebGL يتسبب بشكل مباشر في زيادة التعقيد مقارنةً باستخدام مكتبة مثل Cocos2D أو Unity (هناك أيضًا بعض أدوات مساعدة WebGL). في الواقع، أعتقد العكس تمامًا. تتميز معظم ألعاب الهواتف الجوّالة / HTML5 (على الأقل في ألعاب مثل Boouncey Mouse) بالبساطة للغاية. في معظم الأحيان، لا ترسم اللعبة سوى بعض النقوش، وقد ترسم بعض الأشكال الهندسية المزخرفة. من المحتمل أن يكون إجمالي الرمز الخاص ببرنامج OpenGL في تطبيق Boungy Mouse أقل من 1000 سطر. سأفاجئ إذا كان استخدام مكتبة مساعِدة سيؤدي إلى تقليل هذا العدد. حتى لو تم خفض هذا العدد إلى نصفين، كنت أحتاج إلى قضاء وقت كبير في تعلم مكتبات/أدوات جديدة لتوفير 500 سطر من الرمز فقط. علاوة على ذلك، لم أجد حتى الآن مكتبة مساعد قابلة للنقل عبر جميع المنصات التي أهتم بها، لذا فإن التعامل مع مثل هذه التبعية سيضر بشكل كبير بقابلية النقل. لو كنت أكتب لعبة ثلاثية الأبعاد تحتاج إلى خرائط ضوئية أو محتوى ديناميكي متحرك أو صور متحرّكة ذات مظاهر بشرية وما إلى ذلك، كانت إجابتي ستتغير بالتأكيد. وفي هذه الحالة، سأعيد ابتكار العجلة لمحاولة ترميز المحرك بالكامل يدويًا باستخدام OpenGL. أعتقد أنّ معظم ألعاب الأجهزة الجوّالة/HTML5 غير مضمَّنة بعد في هذه الفئة، لذا لا حاجة إلى تعقيد الأمور قبل أن يكون الأمر ضروريًا.

لا تقلل من أوجه التشابه بين اللغات

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

استنتاجات النقل

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

الصوت

وقد تسببت لي هذه المشكلة (وعلى ما يبدو أمام الجميع) في حدوث مشكلة في الصوت. يتوفر عدد من خيارات الصوت القوية على نظامي التشغيل iOS وAndroid (OpenSL وOpenAL)، ولكن في عالم HTML5، تبدو الأمور أسوأ. أثناء توفر HTML5 Audio، وجدت أن هناك بعض المشكلات التي تؤدي إلى خرق الصفقات عند استخدامها في الألعاب. حتى مع أحدث المتصفحات، كثيرًا ما أواجه سلوكًا غريبًا. يبدو أنّ متصفِّح Chrome مثلاً يفرض حدًا لعدد العناصر الصوتية المتزامنة (المصدر) التي يمكنك إنشاؤها. بالإضافة إلى ذلك، حتى عندما يتم تشغيل الصوت، ينتهي الأمر أحيانًا بتشويه لسبب غير مفهوم. بشكل عام، كنت قلقًا بعض الشيء. وقد كشف البحث عبر الإنترنت أن الجميع تقريبًا يواجه نفس المشكلة. كان الحل الذي توصلتُ إليه في البداية هو واجهة برمجة التطبيقات SoundManager2. تستخدم واجهة برمجة التطبيقات هذه صوت HTML5 Audio عند توفرها، لالعودة إلى Flash في المواقف الصعبة. وبينما نجح هذا الحل، كان لا يزال بعض الأخطاء غير متوقعة ويمكن توقعه (أقل من تأثير الصوت HTML5 الخالص). بعد أسبوع من إطلاق التطبيق، تحدثت مع بعض العاملين في Google الذين قدّموا لي إرشادات إلى Webkit's Web Audio API. لقد فكرت في استخدام واجهة برمجة التطبيقات هذه في البداية، ولكنني ابتعدت عنها بسبب مقدار التعقيد غير الضروري (بالنسبة لي) الذي بدا أن واجهة برمجة التطبيقات موجودة. أردت فقط تشغيل بعض الأصوات: باستخدام صوت HTML5، يصل هذا إلى سطرين من JavaScript. ومع ذلك، في لمحة موجزة عن Web Audio، أدهشتني مواصفاته الضخمة (المكوّنة من 70 صفحة)، والعدد القليل من النماذج على الويب (وهو ما يُستخدَم عادةً في واجهة برمجة تطبيقات جديدة)، وحذف وظيفة "التشغيل" أو "الإيقاف المؤقت" أو "الإيقاف" في أي مكان في المواصفات. وبعد تأكيدات Google بأنّ مخاوفي لم تكن مستحيلة بالنسبة لي، بحثت في واجهة برمجة التطبيقات مرة أخرى. بعد الاطّلاع على بعض الأمثلة الإضافية وإجراء المزيد من الأبحاث، وجدت أنّ Google كانت على صواب، إذ بإمكان واجهة برمجة التطبيقات تلبية احتياجاتي بالتأكيد، ويمكن إجراء ذلك بدون الأخطاء التي تعاني منها واجهات برمجة التطبيقات الأخرى. تُعد المقالة بدء استخدام واجهة برمجة تطبيقات Web Audio مفيدة بشكل خاص إذا كنت تريد التعرّف على هذه الواجهة بشكل أفضل. مشكلتي الحقيقية هي أنّه حتى بعد فهم واجهة برمجة التطبيقات واستخدامها، يبدو أنّ واجهة برمجة تطبيقات غير مصمَّمة بهدف "تشغيل بعض الأصوات" فحسب. للتغلب على هذا الخطأ، كتبت درسًا مساعدًا صغيرًا يتيح لي استخدام واجهة برمجة التطبيقات بالطريقة التي أردتُ استخدامها، وهي تشغيل الصوت وإيقافه مؤقتًا والتوقف عنه والاستعلام عن حالة الصوت. اتصلت بالمقطع الصوتي لصف المساعد. المصدر الكامل متاح على GitHub بموجب ترخيص Apache 2.0، وسأناقش تفاصيل الفصل أدناه. لكن أولاً، إليك بعض المعلومات الأساسية عن Web Audio API:

رسوم بيانية صوتية على الويب

أوّل ما يجعل Web Audio API أكثر تعقيدًا (وأقوى) من عنصر HTML5 Audio هو قدرته على معالجة / مزج الصوت قبل إخراجه للمستخدم. وعلى الرغم من أهميتها، فإن حقيقة أن أي تشغيل صوتي يتضمن رسمًا بياني يجعل الأمور أكثر تعقيدًا في السيناريوهات البسيطة. لتوضيح فعالية Web Audio API، اطّلِع على الرسم البياني التالي:

الرسم البياني للصوت الأساسي على الويب
الرسم البياني الصوتي الأساسي على الويب

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

يمكن أن تكون الرسوم البيانية بسيطة

أوّل ما يجعل Web Audio API أكثر تعقيدًا (وأقوى) من عنصر HTML5 Audio هو قدرته على معالجة / مزج الصوت قبل إخراجه للمستخدم. وعلى الرغم من أهميتها، فإن حقيقة أن أي تشغيل صوتي يتضمن رسمًا بياني يجعل الأمور أكثر تعقيدًا في السيناريوهات البسيطة. لتوضيح فعالية Web Audio API، اطّلِع على الرسم البياني التالي:

رسم بياني لمحتوى صوتي غير متكلّف على الويب
رسم بياني بسيط للصوت على الويب

يوضّح الرسم البياني البسيط أعلاه جميع الخطوات المطلوبة لتشغيل صوت أو إيقافه مؤقتًا أو إيقافه.

لكن لا تقلق بشأن الرسم البياني

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

AudioClip
AudioClip

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

// At startup time
var sound = new AudioClip("ping.wav");

// Later
sound.play();

تفاصيل التنفيذ

لنلقِ نظرة سريعة على الرمز البرمجي لفئة المساعد: المنشئ – الدالة الإنشائية تعالج تحميل البيانات الصوتية باستخدام XHR. على الرغم من عدم ظهوره هنا (بهدف بساطة المثال)، يمكن أيضًا استخدام عنصر HTML5 Audio كعقدة مصدر. وهذا مفيد بشكل خاص للعينات الكبيرة. تجدر الإشارة إلى أنّ واجهة برمجة تطبيقات Web Audio تتطلب أن نجلب هذه البيانات بصفتها "مصفوفة مخزن مؤقت". وبعد تلقّي البيانات، ننشئ مخزنًا مؤقتًا للصوت على الويب من هذه البيانات (فك ترميزها من تنسيقها الأصلي إلى تنسيق PCM في وقت التشغيل).

/**
* Create a new AudioClip object from a source URL. This object can be played,
* paused, stopped, and resumed, like the HTML5 Audio element.
*
* @constructor
* @param {DOMString} src
* @param {boolean=} opt_autoplay
* @param {boolean=} opt_loop
*/
AudioClip = function(src, opt_autoplay, opt_loop) {
// At construction time, the AudioClip is not playing (stopped),
// and has no offset recorded.
this.playing_ = false;
this.startTime_ = 0;
this.loop_ = opt_loop ? true : false;

// State to handle pause/resume, and some of the intricacies of looping.
this.resetTimout_ = null;
this.pauseTime_ = 0;

// Create an XHR to load the audio data.
var request = new XMLHttpRequest();
request.open("GET", src, true);
request.responseType = "arraybuffer";

var sfx = this;
request.onload = function() {
// When audio data is ready, we create a WebAudio buffer from the data.
// Using decodeAudioData allows for async audio loading, which is useful
// when loading longer audio tracks (music).
AudioClip.context.decodeAudioData(request.response, function(buffer) {
    sfx.buffer_ = buffer;
    
    if (opt_autoplay) {
    sfx.play();
    }
});
}

request.send();
}

التشغيل - يتضمن تشغيل الصوت خطوتين: إعداد الرسم البياني للتشغيل، واستدعاء نسخة من "noteOn" في مصدر الرسم البياني. يمكن تشغيل المصدر مرة واحدة فقط، لذا يجب إعادة إنشاء المصدر/الرسم البياني في كل مرة نشغِّل فيها المحتوى. تنتج معظم تعقيدات هذه الدالة من المتطلبات اللازمة لاستئناف مقطع متوقف مؤقتًا (this.pauseTime_ > 0). لاستئناف تشغيل مقطع متوقف مؤقتًا، نستخدم السمة noteGrainOn، التي تسمح بتشغيل منطقة فرعية من مورد احتياطي. لا يتفاعل noteGrainOn مع التكرار الحلقي في هذا السيناريو (لأنّه سيؤدي إلى تكرار المنطقة الفرعية وليس المورد الاحتياطي بأكمله). لذلك، يجب حلّ هذه المشكلة من خلال تشغيل باقي المقطع باستخدام noteGrainOn، ثم إعادة تشغيله من البداية مع تفعيل ميزة التكرار.

/**
* Recreates the audio graph. Each source can only be played once, so
* we must recreate the source each time we want to play.
* @return {BufferSource}
* @param {boolean=} loop
*/
AudioClip.prototype.createGraph = function(loop) {
var source = AudioClip.context.createBufferSource();
source.buffer = this.buffer_;
source.connect(AudioClip.context.destination);

// Looping is handled by the Web Audio API.
source.loop = loop;

return source;
}

/**
* Plays the given AudioClip. Clips played in this manner can be stopped
* or paused/resumed.
*/
AudioClip.prototype.play = function() {
if (this.buffer_ && !this.isPlaying()) {
// Record the start time so we know how long we've been playing.
this.startTime_ = AudioClip.context.currentTime;
this.playing_ = true;
this.resetTimeout_ = null;

// If the clip is paused, we need to resume it.
if (this.pauseTime_ > 0) {
    // We are resuming a clip, so it's current playback time is not correctly
    // indicated by startTime_. Correct this by subtracting pauseTime_.
    this.startTime_ -= this.pauseTime_;
    var remainingTime = this.buffer_.duration - this.pauseTime_;

    if (this.loop_) {
    // If the clip is paused and looping, we need to resume the clip
    // with looping disabled. Once the clip has finished, we will re-start
    // the clip from the beginning with looping enabled
    this.source_ = this.createGraph(false);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime)

    // Handle restarting the playback once the resumed clip has completed.
    // *Note that setTimeout is not the ideal method to use here. A better 
    // option would be to handle timing in a more predictable manner,
    // such as tying the update to the game loop.
    var clip = this;
    this.resetTimeout_ = setTimeout(function() { clip.stop(); clip.play() },
                                    remainingTime * 1000);
    } else {
    // Paused non-looping case, just create the graph and play the sub-
    // region using noteGrainOn.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime);
    }

    this.pauseTime_ = 0;
} else {
    // Normal case, just creat the graph and play.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteOn(0);
}
}
}

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

/**
* Plays the given AudioClip as a sound effect. Sound Effects cannot be stopped
* or paused/resumed, but can be played multiple times with overlap.
* Additionally, sound effects cannot be looped, as there is no way to stop
* them. This method of playback is best suited to very short, one-off sounds.
*/
AudioClip.prototype.playAsSFX = function() {
if (this.buffer_) {
var source = this.createGraph(false);
source.noteOn(0);
}
}

حالة التوقف والإيقاف المؤقت والاستعلام - بقية الدوال مباشرة جدًا ولا تتطلب الكثير من الشرح:

/**
* Stops an AudioClip , resetting its seek position to 0.
*/
AudioClip.prototype.stop = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.startTime_ = 0;
this.pauseTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Pauses an AudioClip. The offset into the stream is recorded to allow the
* clip to be resumed later.
*/
AudioClip.prototype.pause = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.pauseTime_ = AudioClip.context.currentTime - this.startTime_;
this.pauseTime_ = this.pauseTime_ % this.buffer_.duration;
this.startTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Indicates whether the sound is playing.
* @return {boolean}
*/
AudioClip.prototype.isPlaying = function() {
var playTime = this.pauseTime_ +
            (AudioClip.context.currentTime - this.startTime_);

return this.playing_ && (this.loop_ || (playTime < this.buffer_.duration));
}

خاتمة الصوت

نأمل أن تكون هذه الفئة المساعِدة مفيدة للمطوّرين الذين يواجهون مشاكل مشابهة في مجال الصوت مثلي. أيضًا، تبدو فئة كهذه مناسبة للبدء، حتى لو كنت بحاجة إلى إضافة بعض الميزات الأكثر فعالية في Web Audio API. في كلتا الحالتين، لبّى هذا الحل احتياجات لعبة Boouncey Mouse، وسمح للّعبة بأن تكون لعبة HTML5 حقيقية، بدون قيود.

عروض أداء

هناك مجال آخر يقلقني بشأن منفذ JavaScript وهو الأداء. بعد الانتهاء من الإصدار 1 من المنفذ، وجدت أن كل شيء يعمل على ما يرام على سطح المكتب رباعي النواة. لكن للأسف، كانت الأمور على ما يرام على أجهزة الكمبيوتر المحمولة أو Chromebook. في هذه الحالة، أنقذني محلل Chrome من خلال توضيح المكان الذي أقضي فيه جميع الوقت في برامجي. تُبرز تجربتي أهمية التحليل قبل إجراء أي تحسين. كنت أتوقّع أن تكون فيزياء Box2D أو رمز العرض مصدرًا رئيسيًا للبطء في الأداء، إلا أنّ معظم وقتي قضيته في استخدام وظيفة Matrix.clone(). ونظرًا إلى طبيعة لعبتي المليئة بالرياضيات، كنت أعرف أنّني أجريتُ الكثير من صناعة/استنساخ المصفوفات، لكنني لم أتوقع أن يكون هذا هو العائق في الألعاب. في النهاية، اتضح أن تغييرًا بسيطًا للغاية سمح للعبة بتقليل استخدام وحدة المعالجة المركزية (CPU) بأكثر من 3 مرات، حيث ارتفعت نسبة استخدام وحدة المعالجة المركزية (CPU) من 6 إلى 7% على كمبيوتر سطح المكتب إلى %2. ربما كانت هذه معرفة شائعة لمطوّري JavaScript، ولكن بصفتي مطوّرًا بلغة C++ فاجأتني هذه المشكلة، لذا سأدخل في مزيد من التفاصيل. في الأساس، كانت فئة المصفوفة الأصلية مصفوفة 3×3: مصفوفة من 3 عناصر، ويحتوي كل عنصر على مصفوفة من 3 عناصر. لسوء الحظ، كان هذا يعني أنه عندما حان الوقت لاستنساخ المصفوفة، كان علي إنشاء 4 صفائف جديدة. كان التغيير الوحيد الذي أحتاج إلى إجرائها هو نقل هذه البيانات إلى صفيف واحد من 9 عناصر وتحديث الرياضيات وفقًا لذلك. كان هذا التغيير مسؤولاً تمامًا عن الانخفاض الذي لاحظته في وحدة المعالجة المركزية (CPU) بمقدار 3 مرات، وبعد هذا التغيير كان أدائي مقبولاً على جميع أجهزة الاختبار.

المزيد من التحسين

على الرغم من أنّ أدائي كان مقبولاً، كنت لا أزال أواجه بعض العقبات البسيطة. بعد مزيد من التحليل، أدركت أن هذا كان بسبب مجموعة القمامة في Javascript. كان تطبيقي يعمل بمعدّل 60 لقطة في الثانية، ما يعني أنّ كل إطار يحتاج إلى 16 ملي ثانية للرسم. ولسوء الحظ، عندما يتم تجميع البيانات غير المرغوب فيها على جهاز أبطأ، قد ينفد استهلاكها مدة 10 ملّي ثانية تقريبًا. وقد أدّى ذلك إلى حدوث تباطؤ في أداء اللعبة خلال بضع ثوانٍ، حيث تطلبت اللعبة 16 ملي ثانية تقريبًا لرسم لقطة كاملة. للحصول على فكرة أفضل عن سبب إنشاء الكثير من البيانات غير المرغوب فيها، استخدمت المحلل في Chrome. اتضح لي أنّ الغالبية العظمى من المحتوى غير المهم (أكثر من 70% من البيانات) كانت ناتجة عن استخدام Box2D. يعد التخلص من البيانات غير الضرورية في Javascript عملاً معقدًا، وكانت إعادة كتابة Box2D أمرًا غير متوقع، لذلك أدركت أنني قد وقعت في أحد المواقف. لحسن الحظ، لا تزال لديّ إحدى أقدم الحِيل المتوفرة لي في الكتاب وهي: عندما لا يمكنك الوصول إلى 60 إطارًا في الثانية، قم بالتشغيل بمعدل 30 إطارًا في الثانية. لقد تم الاتفاق جيدًا على أن التشغيل بمعدل 30 لقطة في الثانية ثابت أفضل بكثير من التشغيل بمعدل 60 إطارًا في الثانية غير مستقر. في الواقع، لم أتلقَّ شكوى أو تعليقًا واحدًا مفاده أنّ اللعبة تعمل بمعدّل 30 لقطة في الثانية (من الصعب جدًا معرفة ذلك إلا إذا قارنت الإصدارَين جنبًا إلى جنب). وكانت هذه السرعة الإضافية التي تبلغ 16 ملي ثانية لكل إطار تعني أنّه لا يزال لديّ متسع من الوقت لعرض الإطار حتى في حال تجميع البيانات غير المرغوب فيها. حتى عند تشغيل بسرعة 30 لقطة في الثانية، لا يمكن تنفيذه بواسطة واجهة برمجة التطبيقات للتوقيت التي كنت أستخدمها (وهي requestAnimationFrame من WebKit)، ولكن يمكن إنجازها بطريقة تافهة للغاية. قد لا يكون عدد 30 لقطة في الثانية أنيقًا مثل واجهة برمجة التطبيقات الصريحة (API)، ولكن يمكن تحقيق ذلك من خلال معرفة أنّ الفاصل الزمني في RequestAnimationFrame يتوافق مع واجهة VSYNC الخاصة بالشاشة (عادةً ما تبلغ 60 لقطة في الثانية). وهذا يعني أنّه علينا تجاهل كل معاودة الاتصال الأخرى. بشكل أساسي، إذا كان لديك علامة "Tick" لمعاودة الاتصال يتم استدعاؤها في كل مرة يتم فيها تنشيط "RequestAnimationFrame"، يمكن تنفيذ ذلك على النحو التالي:

var skip = false;

function Tick() {
skip = !skip;
if (skip) {
return;
}

// OTHER CODE
}

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

التوزيع وتحقيق الربح

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

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

يتوفّر لدى Boungy Mouse طريقة واحدة بسيطة لتحقيق الربح، ألا وهي إعلان بانر بجانب محتوى اللعبة. ومع ذلك، نظرًا لاتساع نطاق وصول اللعبة إلى الجمهور، تبيّن أنّ إعلان البانر هذا حقّق دخلاً كبيرًا. وخلال فترة الذروة، حقّق التطبيق دخلاً يتناسب مع نظامي الأساسي الأكثر نجاحًا، وهو Android. يتمثّل أحد العوامل في ذلك أنّ إعلانات AdSense الأكبر حجمًا المعروضة على إصدار HTML5 تُحقِّق أرباحًا أعلى بكثير لكل ظهور مقارنةً بإعلانات AdMob الأصغر حجمًا المعروضة على Android. ولا يقتصر الأمر على ذلك فقط، بل يعتبر إعلان البانر على إصدار HTML5 أقل تداخلاً بكثير منه في إصدار Android، مما يسمح بتجربة لعب أكثر وضوحًا. بشكل عام، كنت مفاجأة سارة للغاية من هذه النتيجة.

الأرباح التي تتم تسويتها على مدار الوقت.
تسوية الأرباح بمرور الوقت

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

الخلاصة

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