مقدمة
بعد نشر لعبة Bouncy Mouse على أجهزة iOS وAndroid في نهاية العام الماضي، تعلمت بعض الدروس المهمة جدًا. ومن بين هذه التحديات، كان من الصعب دخول سوق راسخة. في سوق iPhone المشبّع تمامًا، كان من الصعب جدًا اكتساب الزخم، وفي سوق Android Marketplace الأقل تشبّعًا، كان من الأسهل تحقيق تقدّم، ولكن ليس بالسهولة التي تتوقّعها. استنادًا إلى هذه التجربة، رأيت فرصة مثيرة للاهتمام في "سوق Chrome الإلكتروني". على الرغم من أنّ "متجر الويب" ليس فارغًا على الإطلاق، إلا أنّ قائمة الألعاب العالية الجودة المستندة إلى HTML5 فيه لا تزال في مرحلة النمو. بالنسبة إلى مطوّر التطبيقات الجديد، يعني ذلك أنّه من الأسهل بكثير إنشاء الرسوم البيانية للترتيب والحصول على مستوى رؤية أكبر. استنادًا إلى هذه الفرصة، بدأت في نقل لعبة Bouncy Mouse إلى HTML5 على أمل أن أتمكّن من تقديم أحدث تجربة ألعاب لي إلى قاعدة مستخدمين جديدة ومثيرة. في هذه الدراسة، سأتحدث قليلاً عن العملية العامة لنقل لعبة Bouncy Mouse إلى HTML5، ثم سأتناول بالتفصيل ثلاثة مجالات أثبتت أهميتها: الصوت والأداء وتحقيق الربح.
نقل لعبة C++ إلى HTML5
يتوفّر تطبيق Bouncy Mouse حاليًا على Android(C++) وiOS (C++) وWindows Phone 7 (C#) وChrome (Javascript). يطرح هذا السؤال أحيانًا: كيف يمكن كتابة لعبة يمكن نقلها بسهولة إلى منصات متعددة؟ أعتقد أنّ المستخدمين يأملون في الحصول على حلّ سحري يمكنهم استخدامه لتحقيق هذا المستوى من إمكانية النقل بدون اللجوء إلى منفذ يدوي. للأسف، لست متأكّدًا من توفّر حلّ كهذا حتى الآن (أقرب حلّ هو على الأرجح إطار عمل PlayN أو محرك Unity، ولكن لا يحقق أي منهما جميع الأهداف التي تهمّني). كان أسلوبي في الواقع عبارة عن ترجمة يدويّة. لقد كتبتُ أولاً إصدار iOS/Android بلغة C++، ثم نقلتُ هذا الرمز إلى كل نظام أساسي جديد. قد يبدو لك أنّ هذا العمل شاق، ولكن لم يستغرق إصدارا WP7 وChrome أكثر من أسبوعَين. السؤال الآن هو: هل يمكن إجراء أيّ شيء لتسهيل نقل قاعدة بيانات؟ لقد اتّخذتُ بعض الخطوات التي ساعدتني في ذلك:
إبقاء قاعدة الرموز البرمجية صغيرة
قد يبدو هذا واضحًا، ولكنه السبب الرئيسي الذي مكّنني من نقل اللعبة بسرعة كبيرة. يتألف رمز العميل في لعبة Bouncy Mouse من 7,000 سطر تقريبًا من لغة C++. ورغم أنّ 7,000 سطر من الرموز البرمجية ليس عددًا قليلاً، إلا أنّه صغير بما يكفي لإدارة هذا التطبيق. انتهى الأمر بأنّ حجم رمز العميل بتنسيق C# وJavaScript كانا متطابقَين تقريبًا. يعتمد الحفاظ على صغر حجم قاعدة الرموز البرمجية على ممارسةَين رئيسيتين: عدم كتابة أي رموز برمجية زائدة، وتنفيذ أكبر قدر ممكن من الرموز البرمجية للمعالجة المُسبَقة (غير وقت التشغيل). قد يبدو من البديهي عدم كتابة أي رموز زائدة، ولكنّني أحاول دائمًا تجنُّب ذلك. غالبًا ما أحتاج إلى كتابة فئة/دالة مساعدة لأي شيء يمكن تضمينه في دالة مساعدة. ومع ذلك، ما لم تكن تخطّط لاستخدام دالة مساعدة عدّة مرّات، سيؤدي ذلك عادةً إلى زيادة حجم الرمز البرمجي. في لعبة Bouncy Mouse، كنت حريصًا على عدم كتابة أي مساعدة ما لم أكن سأستخدمها ثلاث مرات على الأقل. عندما كتبتُ فئة مساعدة، حاولتُ جعلها بسيطة وقابلة للنقل وقابلة لإعادة الاستخدام في مشاريعي المستقبلية. من ناحية أخرى، عند كتابة الرمز البرمجي للعبة Bouncy Mouse فقط، مع احتمالية منخفضة لإعادة الاستخدام، كان تركيزي على إكمال مهمة الترميز بأبسط وأسرع طريقة ممكنة، حتى لو لم تكن هذه هي الطريقة "الأكثر جمالًا" لكتابة الرمز البرمجي. أما الجزء الثاني والأكثر أهمية من الحفاظ على صغر حجم قاعدة البيانات، فهو إنجاز أكبر عدد ممكن من خطوات المعالجة المُسبَقة. إذا كان بإمكانك نقل مهمة وقت التشغيل إلى مهمة معالجة مسبقة، لن يؤدي ذلك فقط إلى تشغيل لعبتك بشكل أسرع، بل لن تضطر إلى نقل الرمز إلى كل نظام أساسي جديد. على سبيل المثال، كنت أحفظ في الأصل بيانات هندسة المستوى بتنسيق لم تتم معالجته إلى حدٍ كبير، وأجمع وحدات تخزين رؤوس OpenGL/WebGL الفعلية في وقت التشغيل. استغرق هذا الإجراء بعض الإعدادات وبضع مئات من أسطر رمز التشغيل. في وقت لاحق، نقلتُ هذا الرمز إلى خطوة المعالجة المُسبَقة، وكتبتُ مخازن رؤوس OpenGL/WebGL المعبأة بالكامل في وقت الترجمة. بقيت كمية الرموز البرمجية نفسها تقريبًا، ولكن تم نقل هذه الأجزاء القليلة من الخطوط إلى خطوة المعالجة المُسبَقة، ما يعني أنّني لم أضطر أبدًا إلى نقلها إلى أي منصات جديدة. هناك الكثير من الأمثلة على ذلك في لعبة Bouncy Mouse، ويختلف ما يمكن تنفيذه من لعبة إلى أخرى، ولكن عليك الانتباه إلى أيّ عمليات لا تحتاج إلى تنفيذها أثناء التشغيل.
عدم استخدام التبعيات التي لا تحتاج إليها
من الأسباب الأخرى التي تجعل من السهل نقل لعبة Bouncy Mouse هي أنّها لا تحتوي على أي تبعيات تقريبًا. يلخِّص الرسم البياني التالي تبعيات المكتبة الرئيسية لتطبيق Bouncy Mouse لكل منصة:
هذا كل ما في الأمر. لم يتم استخدام أي مكتبات كبيرة تابعة لجهات خارجية، باستثناء Box2D، وهي قابلة للنقل على جميع المنصات. بالنسبة إلى الرسومات، يتطابق WebGL وXNA تقريبًا بنسبة 1:1 مع OpenGL، لذا لم تكن هذه مشكلة كبيرة. كانت المكتبات الفعلية مختلفة في ما يتعلق بالصوت فقط. ومع ذلك، رمز الصوت في Bouncy Mouse صغير (حوالي مائة سطر من الرموز الخاصة بالمنصة)، لذا لم تكن هذه مشكلة كبيرة. إنّ إبقاء لعبة Bouncy Mouse خالية من المكتبات الكبيرة غير القابلة للنقل يعني أنّ منطق رمز التشغيل يمكن أن يكون متطابقًا تقريبًا بين الإصدارات (على الرغم من تغيير اللغة). بالإضافة إلى ذلك، يجنبنا ذلك الاعتماد على سلسلة أدوات غير قابلة للنقل. لقد سُئلت عما إذا كان الترميز باستخدام OpenGL/WebGL مباشرةً يؤدي إلى زيادة التعقيد مقارنةً باستخدام مكتبة مثل Cocos2D أو Unity (تتوفّر أيضًا بعض أدوات المساعدة في WebGL). في الواقع، أعتقد أنّ العكس هو الصحيح. إنّ معظم ألعاب الهواتف الجوّالة أو ألعاب HTML5 (على الأقل الألعاب مثل Bouncy Mouse) بسيطة جدًا. في معظم الحالات، ترسم اللعبة بضع صور متحركة وربما بعض الأشكال الهندسية المموّهة. من المرجّح أنّ إجمالي عدد الأسطر البرمجية الخاصة بـ OpenGL في Bouncy Mouse يقل عن 1,000 سطر. سأتفاجأ إذا كان استخدام مكتبة مساعدة سيؤدي إلى تقليل هذا العدد. حتى لو خفضت هذه الأرقام إلى النصف، سأحتاج إلى قضاء وقت كبير في تعلُّم مكتبات أو أدوات جديدة لتوفير 500 سطر من الرموز البرمجية فقط. بالإضافة إلى ذلك، لم نعثر بعد على مكتبة مساعدة قابلة للنقل على جميع الأنظمة الأساسية التي تهمّنا، لذا فإنّ استخدام مكتبة تعتمد على مكتبة أخرى سيؤثّر بشكل كبير في قابلية النقل. إذا كنت أكتب لعبة ثلاثية الأبعاد تحتاج إلى خرائط إضاءة ومستوى تفاصيل ديناميكي ورسوم متحركة مخصّصة وما إلى ذلك، ستتغيّر إجابتي بالتأكيد. في هذه الحالة، سأعيد اختراع العجلة لمحاولة كتابة رمز برمجي يدوي لمحركي بالكامل باستخدام OpenGL. أريد أن أشير إلى أنّ معظم ألعاب الأجهزة الجوّالة/HTML5 ليست (حتى الآن) ضمن هذه الفئة، لذا لا داعي لتعقيد الأمور قبل أن يصبح ذلك ضروريًا.
عدم التقليل من أهمية أوجه التشابه بين اللغات
من الحيل الأخيرة التي وفّرت الكثير من الوقت في نقل قاعدة بيانات C++ إلى لغة جديدة هي إدراك أنّ معظم الرموز البرمجية متطابقة تقريبًا بين كل لغة. على الرغم من أنّ بعض العناصر الرئيسية قد تتغيّر، إلا أنّ هذه العناصر أقل بكثير من العناصر التي لا تتغيّر. في الواقع، بالنسبة إلى العديد من الدوال، كان الانتقال من C++ إلى JavaScript يتطلّب ببساطة تنفيذ بعض عمليات الاستبدال باستخدام التعبيرات العادية في قاعدة بيانات C++.
استنتاجات حول نقل الأرقام
هذه هي كل المعلومات التي تحتاج إليها حول عملية نقل الرقم. سأتناول بعض التحديات المتعلّقة بخدمة HTML5 في الأقسام القليلة التالية، ولكن الرسالة الرئيسية هي أنّه إذا أبقيت الرمز بسيطًا، سيكون نقل البيانات مشكلة بسيطة، وليس كابوسًا.
الصوت
واجهتُ (وجميع المستخدمين على ما يبدو) بعض المشاكل في الصوت. على نظامَي التشغيل iOS وAndroid، تتوفّر مجموعة من خيارات الصوت القوية (OpenSL وOpenAL)، لكن في عالم HTML5، كانت الأمور أكثر تعقيدًا. على الرغم من توفّر ميزة HTML5 Audio، تبيّن لنا أنّها تتضمّن بعض المشاكل الخطيرة عند استخدامها في الألعاب. حتى على أحدث المتصفحات، كنت أواجه سلوكًا غريبًا بشكل متكرر. على سبيل المثال، يبدو أنّ Chrome يفرض حدًا أقصى لعدد عناصر الصوت المتزامنة (source) التي يمكنك إنشاؤها. بالإضافة إلى ذلك، حتى عند تشغيل الصوت، ينتهي الأمر أحيانًا بتشويش الصوت بشكل غير مبرر. بشكل عام، كنت قلقًا بعض الشيء. أظهرت عملية البحث على الإنترنت أنّ جميع المستخدمين تقريبًا يواجهون المشكلة نفسها. كان الحلّ الذي وصلت إليه في البداية هو واجهة برمجة تطبيقات تُسمى SoundManager2. تستخدِم واجهة برمجة التطبيقات هذه تنسيق HTML5 Audio عند توفّره، وتستخدِم Flash في الحالات المعقّدة. على الرغم من أنّ هذا الحلّ كان ناجحًا، إلا أنّه كان لا يزال يتضمّن أخطاءً وغير متوقّع (بدرجة أقل من HTML5 Audio). بعد أسبوع من الإطلاق، تواصلت مع بعض الخبراء في Google الذين ساعدوني في العثور على Web Audio API من Webkit. لقد فكّرت في البداية في استخدام واجهة برمجة التطبيقات هذه، لكنني تجنّبت ذلك بسبب التعقيد غير الضروري (بالنسبة إليّ) الذي بدت أنّه يتخلّل واجهة برمجة التطبيقات. أردتُ فقط تشغيل بعض الأصوات: باستخدام HTML5 Audio، يُعدّ ذلك بضع سطور من JavaScript. ومع ذلك، بعد إلقاء نظرة سريعة على Web Audio، لفتت انتباهي مواصفاته الكبيرة (70 صفحة) والعدد القليل من العيّنات على الويب (وهو أمر شائع في واجهات برمجة التطبيقات الجديدة)، والإغفال عن تضمين وظيفة "تشغيل" أو "إيقاف مؤقت" أو "إيقاف" في أي مكان من المواصفات. بعد أن اطّلعت على تأكيدات Google بأنّ مخاوفي غير مستندة إلى أساس من الصحة، راجعت واجهة برمجة التطبيقات مرة أخرى. بعد الاطّلاع على بعض الأمثلة الإضافية وإجراء المزيد من الأبحاث، تبيّن لي أنّ Google كانت على صواب، إذ يمكن لواجهة برمجة التطبيقات تلبية احتياجاتي بالتأكيد، ويمكنها إجراء ذلك بدون الأخطاء التي تصيب واجهات برمجة التطبيقات الأخرى. ومن المقالات المفيدة بشكل خاص مقالة البدء باستخدام Web Audio API، وهي مكان رائع للاطّلاع عليه إذا كنت تريد الحصول على فهم أعمق لواجهة برمجة التطبيقات. المشكلة الحقيقية هي أنّه حتى بعد فهم واجهة برمجة التطبيقات واستخدامها، لا تزال تبدو لي أنّها ليست مصمّمة "لتشغيل بعض الأصوات فقط". للتغلب على هذا الشعور بالارتباك، كتبت فئة مساعدة صغيرة تتيح لي استخدام واجهة برمجة التطبيقات بالطريقة التي أريدها، أي تشغيل الصوت وإيقافه مؤقتًا وإيقافه والاستعلام عن حالة الصوت. لقد أطلقت على هذه الفئة المساعِدة اسم AudioClip. يتوفّر المصدر الكامل على GitHub بموجب ترخيص Apache 2.0، وسأناقش تفاصيل الصف أدناه. أولاً، إليك بعض المعلومات الأساسية عن Web Audio API:
الرسوم البيانية لواجهة برمجة التطبيقات Web Audio
إنّ أول ما يجعل Web Audio API أكثر تعقيدًا (وأكثر فعالية) من عنصر HTML5 Audio هو قدرته على معالجة الصوت أو مزجه قبل عرضه للمستخدم. على الرغم من أنّ تشغيل أي صوت يتضمن رسمًا بيانيًا، إلا أنّ ذلك يجعل الأمور أكثر تعقيدًا في السيناريوهات البسيطة. لتوضيح مدى فعالية Web Audio API، اطّلِع على الرسم البياني التالي:
على الرغم من أنّ المثال أعلاه يوضّح مدى فعالية Web Audio API، لم أكن بحاجة إلى معظم هذه الفعالية في السيناريو الذي استخدمته. أردت فقط تشغيل صوت. على الرغم من أنّ هذا يتطلّب استخدام رسم بياني، إلا أنّه رسم بياني بسيط جدًا.
يمكن أن تكون الرسوم البيانية بسيطة
إنّ أول ما يجعل Web Audio API أكثر تعقيدًا (وأكثر فعالية) من عنصر HTML5 Audio هو قدرته على معالجة الصوت أو مزجه قبل عرضه للمستخدم. على الرغم من أنّ تشغيل أي صوت يتضمن رسمًا بيانيًا، إلا أنّ ذلك يجعل الأمور أكثر تعقيدًا في السيناريوهات البسيطة. لتوضيح مدى فعالية Web Audio API، اطّلِع على الرسم البياني التالي:
يمكن للرسم البياني البسيط المعروض أعلاه تنفيذ كل ما يلزم لتشغيل صوت أو إيقافه مؤقتًا أو إيقافه.
ولكن لن نهتم بالرسم البياني.
على الرغم من أنّ فهم الرسم البياني أمر جيد، إلا أنّني لا أريد التعامل معه في كل مرة أشغّل فيها صوتًا. لذلك، كتبتُ فئة ملفّ تعريف بسيطة "AudioClip". تدير هذه الفئة هذا الرسم البياني داخليًا، ولكنها تقدّم واجهة برمجة تطبيقات أبسط بكثير موجّهة للمستخدم.
هذه الفئة ليست أكثر من رسم بياني لصوت الويب وبعض حالات المساعدة، ولكنها تسمح لي باستخدام رمز أبسط بكثير مما لو اضطررت إلى إنشاء رسم بياني لصوت الويب لتشغيل كل صوت.
// At startup time
var sound = new AudioClip("ping.wav");
// Later
sound.play();
تفاصيل التنفيذ
لنلقِ نظرة سريعة على رمز فئة المساعدة: المنشئ: يعالج المنشئ تحميل بيانات الصوت باستخدام طلب XHR. على الرغم من أنّه لم يتم عرض عنصر HTML5 Audio هنا (للحفاظ على بساطة المثال)، يمكن أيضًا استخدامه كعقدة مصدر. ويُعدّ ذلك مفيدًا بشكل خاص للعيّنات الكبيرة. يُرجى العِلم أنّ Web Audio API تتطلّب جلب هذه البيانات كـ "ملف بيانات مصفوفة". وبعد استلام البيانات، ننشئ ملف تخزين 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);
}
}
}
التشغيل كتأثير صوتي: لا تسمح وظيفة التشغيل أعلاه بتشغيل المقطع الصوتي عدة مرات مع تداخل (لا يمكن تشغيله مرة ثانية إلا عند انتهاء المقطع أو إيقافه). قد تحتاج اللعبة أحيانًا إلى تشغيل صوت عدة مرات بدون انتظار اكتمال كل عملية تشغيل (مثل جمع العملات في لعبة وما إلى ذلك). لتفعيل ذلك، تتضمّن فئة AudioClip طريقة playAsSFX()
.
بما أنّه يمكن أن تحدث عمليات تشغيل متعددة في الوقت نفسه، لا يكون التشغيل من playAsSFX()
مرتبطًا بنسبة 1:1 بـ AudioClip. لذلك، لا يمكن إيقاف التشغيل أو إيقافه مؤقتًا أو الاستعلام عن حالته. يتم أيضًا إيقاف ميزة "التشغيل المتكرّر"، لأنّه لن تكون هناك طريقة لإيقاف صوت يتم تشغيله بهذه الطريقة بشكل متكرّر.
/**
* 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. وفي كلتا الحالتَين، كان هذا الحلّ يلبي احتياجات Bouncy Mouse، وسمح للّعبة بأن تكون لعبة HTML5 حقيقية بدون أي قيود.
الأداء
كان الأداء هو المجال الآخر الذي أثار قلقي في ما يتعلّق بأحد منافذ JavaScript. بعد الانتهاء من الإصدار 1 من المنفذ، تبيّن لي أنّ كل شيء يعمل على ما يرام على جهاز الكمبيوتر المكتبي رباعي النوى. لم تكن الأمور على ما يرام على جهاز كمبيوتر محمول صغير أو جهاز Chromebook. في هذه الحالة، ساعدني أداة تحليل الأداء في Chrome من خلال عرض الوقت الذي تم إنفاقه في كل برامجي بدقة.
تُبرز تجربتي أهمية إعداد الملف الشخصي قبل إجراء أي تحسين. كنت أتوقع أن تكون قوانين الفيزياء في Box2D أو ربما رمز العرض مصدرًا رئيسيًا للتباطؤ، ولكنني كنت أقضي معظم وقتي في دالة Matrix.clone()
. نظرًا للطبيعة الحسابية المكثفة للعبة، كنت أعلم أنّني أنشأتُ أو نسختُ الكثير من المصفوفات، ولكنّني لم أتوقع أبدًا أن يكون ذلك هو السبب في حدوث عرقلة. في النهاية، تبيّن أنّ تغييرًا بسيطًا جدًا سمح لوحدة المعالجة المركزية (CPU) بخفض استخدامها بنسبة تزيد عن 3 أضعاف، من 6-7% على الكمبيوتر المكتبي إلى 2%.
قد تكون هذه المعلومات معروفة لمطوّري JavaScript، ولكن بصفتي مطوّر C++، فاجأتني هذه المشكلة، لذا سأوضّح المزيد من التفاصيل. في الأساس، كانت فئة المصفوفة الأصلية عبارة عن مصفوفة 3×3: صفيف 3 عناصر، يحتوي كل عنصر على صفيف 3 عناصر. وهذا يعني أنّه عندما حان وقت استنساخ المصفوفة، كان عليّ إنشاء 4 صفائف جديدة. التغيير الوحيد الذي كنت بحاجة إلى إجرائه هو نقل هذه البيانات إلى صفيف واحد يتألف من 9 عناصر وتعديل العمليات الحسابية وفقًا لذلك. كان هذا التغيير وحده مسؤولاً بالكامل عن الانخفاض الذي شهدته في استخدام وحدة المعالجة المركزية بمقدار 3 أضعاف، وبعد هذا التغيير، أصبح الأداء مقبولًا على جميع الأجهزة الاختبارية.
مزيد من التحسين
على الرغم من أنّ أدائي كان مقبولًا، كنت أواجه بعض المشاكل البسيطة. بعد إجراء المزيد من عمليات التحليل، تبيّن لي أنّ ذلك كان بسبب ميزة "جمع المهملات" في JavaScript. كان تطبيقي يعمل بمعدّل 60 لقطة في الثانية، ما يعني أنّه كان يستغرق 16 ملي ثانية فقط لعرض كل لقطة. عند بدء عملية جمع المهملات على جهاز أبطأ، كان يستغرق أحيانًا 10 مللي ثانية تقريبًا. وقد أدّى ذلك إلى حدوث تقطُّع كل بضع ثوانٍ، لأنّ اللعبة كانت تتطلّب 16 ملي ثانية تقريبًا لعرض إطار كامل. للحصول على فكرة أفضل عن سبب إنشاء هذا الكم الهائل من البيانات غير الصالحة، استخدمت أداة تحليل الذاكرة في Chrome. لخيبة أملي، تبيّن أنّ الغالبية العظمى من البيانات غير الصالحة (أكثر من %70) كانت تُنشئها مكتبة Box2D. إنّ إزالة البيانات غير المفيدة في JavaScript عملية صعبة، ولم يكن من الممكن إعادة كتابة Box2D، لذا عرفت أنّني أوقعت نفسي في مأزق. لحسن الحظ، كان لا يزال لديّ إحدى أقدم الحيل المتاحة لي: عندما لا تتمكّن من الوصول إلى 60 لقطة في الثانية، يمكنك تشغيل الفيديو بمعدّل 30 لقطة في الثانية. من المتفق عليه بشكل عام أنّ تشغيل الفيديو بمعدّل ثابت يبلغ 30 لقطة في الثانية أفضل بكثير من تشغيله بمعدّل متذبذب يبلغ 60 لقطة في الثانية. في الواقع، لم أتلقَّ حتى الآن أي شكوى أو تعليق بشأن تشغيل اللعبة بمعدّل 30 لقطة في الثانية (من الصعب جدًا معرفة ذلك ما لم تقارن بين الإصدارَين جنبًا إلى جنب). يعني هذا الوقت الإضافي الذي يبلغ 16 ملي ثانية لكل إطار أنّه حتى في حال جمع المهملات بشكل سيئ، سيظلّ لدينا متسع من الوقت لعرض الإطار. على الرغم من أنّ واجهة برمجة التطبيقات الخاصة بالتوقيت التي كنت أستخدمها (requestAnimationFrame الرائعة من WebKit) لا تتيح تشغيل المحتوى بمعدّل 30 لقطة في الثانية بشكل صريح، يمكن تنفيذ ذلك بطريقة بسيطة جدًا. على الرغم من أنّه قد لا يكون أنيقًا مثل واجهة برمجة تطبيقات صريحة، يمكن تحقيق معدل 30 لقطة في الثانية من خلال معرفة أنّ الفاصل الزمني لـ RequestAnimationFrame متوافق مع ميزة VSYNC في الشاشة (عادةً 60 لقطة في الثانية). وهذا يعني أنّه علينا تجاهل كلّ طلبات إعادة الاتصال الأخرى. بشكل أساسي، إذا كان لديك دالة استدعاء "Tick" يتمّ استدعاؤها في كلّ مرّة يتمّ فيها تشغيل "RequestAnimationFrame"، يمكن تنفيذ ذلك على النحو التالي:
var skip = false;
function Tick() {
skip = !skip;
if (skip) {
return;
}
// OTHER CODE
}
إذا أردت توخي الحذر بشكلٍ أكبر، يجب التأكّد من أنّ سرعة عرض اللقطات في الثانية (VSYNC) على الكمبيوتر لا تقل عن 30 لقطة في الثانية أو تساويها عند بدء التشغيل، وإيقاف ميزة التخطّي في هذه الحالة. ومع ذلك، لم ألاحظ ذلك بعد في أيّ من إعدادات أجهزة الكمبيوتر المكتبي/الكمبيوتر المحمول التي اختبرتُها.
التوزيع وتحقيق الربح
كان تحقيق الربح هو المجال الأخير الذي فاجأني بشأن إصدار Chrome من لعبة Bouncy Mouse. عند بدء هذا المشروع، كنت أرى ألعاب HTML5 كتجربة مثيرة للاهتمام للتعرّف على التكنولوجيات الصاعدة. لم أدرك أنّه سيصل إلى جمهور كبير جدًا ويحقّق أرباحًا كبيرة.
تم إطلاق لعبة Bouncy Mouse في نهاية تشرين الأول (أكتوبر) على "سوق Chrome الإلكتروني". من خلال الإصدار على "سوق Chrome الإلكتروني"، تمكّنت من الاستفادة من نظام حالي لسهولة العثور على التطبيق والتفاعل مع المنتدى والترتيبات والميزات الأخرى التي اعتدت عليها على منصات الأجهزة الجوّالة. ما أذهلني هو مدى اتّساع نطاق وصول المتجر. وخلال شهر واحد من الإصدار، حققتُ ما يقرب من أربعمائة ألف عملية تثبيت، وكنت أستفيد من تفاعل المستخدمين (الإبلاغ عن الأخطاء والملاحظات). لقد فاجأني أيضًا مدى إمكانية تحقيق الربح من تطبيقات الويب.
تتضمّن لعبة Bouncy Mouse طريقة واحدة بسيطة لتحقيق الربح، وهي إعلان بانر بجانب محتوى اللعبة. ومع ذلك، نظرًا لوصول اللعبة إلى شريحة واسعة من الجمهور، تبيّن لي أنّ إعلان البانر هذا كان قادرًا على تحقيق أرباح كبيرة، وخلال ذروة نشاطه، حقّق التطبيق أرباحًا مماثلة لمنصّتي الأكثر نجاحًا، وهي Android. ويعود أحد العوامل المساهمة في ذلك إلى أنّ إعلانات AdSense الأكبر حجمًا التي يتم عرضها على إصدار HTML5 تحقّق أرباحًا أعلى بكثير لكلّ مرّة ظهور مقارنةً بإعلانات AdMob الأصغر حجمًا التي يتم عرضها على Android. بالإضافة إلى ذلك، فإنّ إعلان البانر في إصدار HTML5 أقلّ تدخلاً بكثير من إصدار Android، ما يتيح تجربة لعب أكثر سلاسة. بشكل عام، كانت هذه النتيجة مفاجأة سارة بالنسبة إليّ.

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