Fieldrunners
Fieldrunners هي لعبة حائزة على جوائز من نوع ألعاب الدفاع عن البرج، وتم إصدارها في الأصل لأجهزة iPhone في عام 2008. ومنذ ذلك الحين، تم نقله إلى العديد من المنصات الأخرى. وكانت إحدى أحدث المنصات هي متصفّح Chrome في أكتوبر 2011. كانت إحدى التحديات التي واجهتنا في نقل Fieldrunners إلى منصة HTML5 هي كيفية تشغيل الصوت.
لا تستخدم Fieldrunners المؤثرات الصوتية بشكل معقد، ولكنها تأتي مع بعض التوقعات حول كيفية تفاعلها مع مؤثراتها الصوتية. تحتوي اللعبة على 88 مؤثرًا صوتيًا، ومن المتوقع تشغيل عدد كبير منها في وقت واحد. معظم هذه الأصوات قصيرة جدًا ويجب تشغيلها في الوقت المناسب قدر الإمكان لتجنب حدوث أي انفصال عن العرض الرسومي.
ظهرت بعض التحديات
أثناء نقل Fieldrunners إلى HTML5، واجهنا مشاكل في تشغيل الصوت باستخدام علامة الصوت، وقرّرنا في وقت مبكر التركيز على Web Audio API بدلاً من ذلك. ساعدنا استخدام WebAudio في حلّ مشاكل مثل توفير عدد كبير من التأثيرات المتزامنة التي تتطلبها لعبة Fieldrunners. ومع ذلك، أثناء تطوير نظام صوتي للعبة Fieldrunners HTML5، واجهنا بعض المشاكل الدقيقة التي قد يرغب مطوّرون آخرون في معرفتها.
طبيعة AudioBufferSourceNodes
AudioBufferSourceNodes هي الطريقة الأساسية لتشغيل الأصوات باستخدام WebAudio. من المهم جدًا فهم أنّ هذه الرموز تُستخدَم لمرة واحدة فقط. يمكنك إنشاء AudioBufferSourceNode وتعيين مخزن مؤقت له وربطه بالرسم البياني وتشغيله باستخدام noteOn أو noteGrainOn. بعد ذلك، يمكنك استدعاء noteOff لإيقاف التشغيل، ولكن لن تتمكن من تشغيل المصدر مرة أخرى عن طريق استدعاء noteOn أو noteGrainOn - عليك إنشاء AudioBufferSourceNode آخر. يمكنك إعادة استخدام عنصر AudioBuffer الأساسي نفسه، وهذا أمر أساسي (في الواقع، يمكنك حتى استخدام عدة عناصر AudioBufferSourceNodes نشطة تشير إلى مثيل AudioBuffer نفسه). يمكنك العثور على مقتطف تشغيل من Fieldrunners في Give Me a Beat.
محتوى لا يتم تخزينه مؤقتًا
عند إصدار لعبة Fieldrunners HTML5، أظهر الخادم عددًا هائلاً من الطلبات لملفات الموسيقى. نتجت هذه النتيجة عن متابعة Chrome 15 لتنزيل الملف في أجزاء ثم عدم تخزينه مؤقتًا. استجابةً لذلك، قرّرنا تحميل ملفات الموسيقى مثل بقية الملفات الصوتية. يُعدّ ذلك دون المستوى الأمثل، ولكن بعض إصدارات المتصفّحات الأخرى لا تزال تفعل ذلك.
كتم الصوت عندما لا تكون النافذة في المقدّمة
كان من الصعب في السابق رصد الحالات التي تكون فيها علامة تبويب اللعبة غير نشطة. بدأ نقل Fieldrunners قبل إصدار Chrome 13 حيث حلّت Page Visibility API محل الحاجة إلى الرمز المعقّد لرصد تمويه علامة التبويب. يجب أن تستخدم كل لعبة واجهة Visibility API لكتابة مقتطف صغير لكتم الصوت أو إيقافه مؤقتًا إذا لم يتم إيقاف اللعبة بأكملها مؤقتًا. بما أنّ لعبة Fieldrunners تستخدم واجهة برمجة التطبيقات requestAnimationFrame، تم التعامل مع إيقاف اللعبة مؤقتًا بشكل ضمني، ولكن لم يتم إيقاف الصوت مؤقتًا.
جارٍ إيقاف الأصوات مؤقتًا
من الغريب أنّه أثناء الحصول على ملاحظات حول هذه المقالة، تم إبلاغنا بأنّ الأسلوب الذي كنّا نستخدمه لإيقاف الأصوات مؤقتًا لم يكن مناسبًا، فقد كنّا نستخدم خطأ في عملية التنفيذ الحالية لواجهة برمجة التطبيقات Web Audio لإيقاف تشغيل الأصوات مؤقتًا. بما أنّ هذه المشكلة سيتم حلّها في المستقبل، لا يمكنك إيقاف الصوت مؤقتًا عن طريق فصل عقدة أو رسم بياني فرعي لإيقاف التشغيل.
بنية بسيطة لعُقد Web Audio
تتضمّن لعبة Fieldrunners نموذجًا صوتيًا بسيطًا للغاية. يمكن لهذا النموذج استخدام مجموعة الخصائص التالية:
- التحكّم في مستوى صوت المؤثرات الصوتية
- التحكّم في مستوى صوت المقطع الموسيقي في الخلفية
- كتم الصوت بالكامل
- أوقِف تشغيل الأصوات عندما تكون اللعبة متوقفة مؤقتًا.
- أعِد تفعيل هذه الأصوات نفسها عند استئناف اللعبة.
- إيقاف كل الأصوات عندما لا تكون علامة تبويب اللعبة نشطة
- أعِد تشغيل الصوت بعد تشغيله حسب الحاجة.
لتحقيق الميزات المذكورة أعلاه باستخدام Web Audio، تم استخدام 3 من العُقد المتاحة: DestinationNode وGainNode وAudioBufferSourceNode. وتشغّل AudioBufferSourceNodes الأصوات. تربط GainNodes بين AudioBufferSourceNodes. يُشغِّل DestinationNode، الذي تم إنشاؤه بواسطة سياق Web Audio، ويُسمى الوجهة، الأصوات للمشغِّل. تتضمّن واجهة Web Audio أنواعًا أخرى من العُقد، ولكن باستخدام هذه العُقد فقط، يمكننا إنشاء رسم بياني بسيط جدًا للأصوات في اللعبة.
يؤدي الرسم البياني لعُقد Web Audio من العُقد الطرفية إلى العُقدة الوجهة. استخدمت لعبة Fieldrunners 6 عُقد كسب دائمة، ولكن 3 عُقد كافية للسماح بالتحكّم السهل في مستوى الصوت وربط عدد أكبر من العُقد المؤقتة التي ستشغّل المخازن المؤقتة. أولاً، عقدة كسب رئيسية تربط كل عقدة فرعية بالوجهة. يتم إرفاق عقدتَي كسب مباشرةً بعقدة الكسب الرئيسية، إحداهما لقناة الموسيقى والأخرى لربط جميع المؤثرات الصوتية.
كانت لعبة Fieldrunners تحتوي على 3 عُقد إضافية لزيادة الموارد بسبب الاستخدام غير الصحيح لخطأ برمجي كميزة. استخدمنا هذه العُقَد لاقتطاع مجموعات من الأصوات التي يتم تشغيلها من الرسم البياني، ما يؤدي إلى إيقاف تقدّمها. لقد فعلنا ذلك لإيقاف الأصوات مؤقتًا. بما أنّ هذا غير صحيح، سنستخدم الآن 3 عُقد إجمالية فقط كما هو موضّح أعلاه. ستتضمّن العديد من المقتطفات التالية العُقد غير الصحيحة، ما يوضّح ما فعلناه وكيفية إصلاح ذلك على المدى القصير. ولكن على المدى الطويل، ننصحك بعدم استخدام عُقدنا بعد عُقدة coreEffectsGain.
function AudioManager() {
// map for loaded sounds
this.sounds = {};
// create our permanent nodes
this.nodes = {
destination: this.audioContext.destination,
masterGain: this.audioContext.createGain(),
backgroundMusicGain: this.audioContext.createGain(),
coreEffectsGain: this.audioContext.createGain(),
effectsGain: this.audioContext.createGain(),
pausedEffectsGain: this.audioContext.createGain()
};
// and setup the graph
this.nodes.masterGain.connect( this.nodes.destination );
this.nodes.backgroundMusicGain.connect( this.nodes.masterGain );
this.nodes.coreEffectsGain.connect( this.nodes.masterGain );
this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );
this.nodes.pausedEffectsGain.connect( this.nodes.coreEffectsGain );
}
تتيح معظم الألعاب التحكّم في المؤثرات الصوتية والموسيقى بشكل منفصل. يمكن تحقيق ذلك بسهولة من خلال الرسم البياني أعلاه. تحتوي كل عقدة كسب على سمة "كسب" يمكن ضبطها على أي قيمة عشرية بين 0 و1، ويمكن استخدامها للتحكّم في مستوى الصوت بشكل أساسي. بما أنّنا نريد التحكّم في مستوى صوت قنوات الموسيقى والمؤثرات الصوتية بشكل منفصل، لدينا عقدة كسب لكل قناة يمكننا من خلالها التحكّم في مستوى الصوت.
function setArbitraryVolume() {
var musicGainNode = this.nodes.backgroundMusicGain;
// set music volume to 50%
musicGainNode.gain.value = 0.5;
}
يمكننا استخدام هذه الإمكانية نفسها للتحكم في مستوى صوت كل شيء، من المؤثرات الصوتية والموسيقى. سيؤثر ضبط كسب العقدة الرئيسية في كل الأصوات الصادرة من اللعبة. إذا ضبطت قيمة الكسب على 0، سيتم كتم الصوت والموسيقى. تحتوي AudioBufferSourceNodes على مَعلمة كسب أيضًا. يمكنك تتبُّع قائمة بجميع الأصوات التي يتم تشغيلها وضبط قيم كسبها بشكل فردي لمستوى الصوت الإجمالي. إذا كنت تنشئ مؤثرات صوتية باستخدام علامات الصوت، فهذا ما عليك فعله. بدلاً من ذلك، يسهّل الرسم البياني للعقد في Web Audio تعديل مستوى صوت عدد لا يحصى من الأصوات. يمنحك التحكّم في مستوى الصوت بهذه الطريقة أيضًا قدرة إضافية بدون تعقيد. يمكننا فقط إرفاق AudioBufferSourceNode مباشرةً بالعقدة الرئيسية لتشغيل الموسيقى والتحكّم في مستوى الصوت الخاص بها. ولكن عليك ضبط هذه القيمة في كل مرة تنشئ فيها AudioBufferSourceNode لغرض تشغيل الموسيقى. بدلاً من ذلك، يمكنك تغيير عقدة واحدة فقط عندما يغيّر اللاعب مستوى صوت الموسيقى وعند بدء التشغيل. الآن لدينا قيمة كسب على مصادر المخزن المؤقت للقيام بشيء آخر. بالنسبة إلى الموسيقى، يمكن استخدام هذه الميزة لإنشاء تأثير تلاشٍ متداخل من مقطع صوتي إلى آخر عند انتهاء مقطع وبدء آخر. توفّر واجهة برمجة التطبيقات Web Audio طريقة جيدة لإجراء ذلك بسهولة.
function arbitraryCrossfade( track1, track2 ) {
track1.gain.linearRampToValueAtTime( 0, 1 );
track2.gain.linearRampToValueAtTime( 1, 1 );
}
لم تستخدم Fieldrunners التداخل التدريجي بشكل محدد. لو كنّا على علم بوظيفة ضبط قيمة WebAudio أثناء تمريرنا الأصلي لنظام الصوت، لكان من المحتمل أن نستخدمها.
جارٍ إيقاف الأصوات مؤقتًا
عندما يوقف اللاعب اللعبة مؤقتًا، يمكنه توقُّع استمرار تشغيل بعض الأصوات. يُعدّ الصوت جزءًا رائعًا من الملاحظات التي يتلقّاها المستخدمون عند الضغط على عناصر واجهة المستخدم في قوائم الألعاب. بما أنّ لعبة Fieldrunners تتضمّن عددًا من الواجهات التي يمكن للمستخدم التفاعل معها أثناء إيقاف اللعبة مؤقتًا، نريد أن يستمر تشغيل هذه الواجهات. ومع ذلك، لا نريد أن يستمر تشغيل أي أصوات طويلة أو متكررة. من السهل جدًا إيقاف هذه الأصوات باستخدام Web Audio أو على الأقل اعتقدنا ذلك.
AudioManager.prototype.pauseEffects = function() {
this.nodes.effectsGain.disconnect();
}
لا تزال عقدة المؤثرات المتوقفة مؤقتًا متصلة. وستستمر الأصوات المسموح لها بتجاهل حالة الإيقاف المؤقت للعبة في التشغيل خلال ذلك. عند استئناف اللعبة، يمكننا إعادة ربط هذه العُقد وتشغيل كل الأصوات مرة أخرى على الفور.
AudioManager.prototype.resumeEffects = function() {
this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );
}
بعد شحن Fieldrunners، اكتشفنا أن فصل عقدة أو رسم بياني فرعي وحده لن يوقف تشغيل AudioBufferSourceNodes مؤقتًا. لقد استفدنا من خطأ في WebAudio يوقف حاليًا تشغيل العُقد غير المرتبطة بعقدة الوجهة في الرسم البياني. لذا للتأكد من أننا مستعدون لهذا الإصلاح المستقبلي، نحتاج إلى بعض التعليمات البرمجية مثل ما يلي:
AudioManager.prototype.pauseEffects = function() {
this.nodes.effectsGain.disconnect();
var now = Date.now();
for ( var name in this.sounds ) {
var sound = this.sounds[ name ];
if ( !sound.ignorePause && ( now - sound.source.noteOnAt < sound.buffer.duration * 1000 ) ) {
sound.pausedAt = now - sound.source.noteOnAt;
sound.source.noteOff();
}
}
}
AudioManager.prototype.resumeEffects = function() {
this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );
var now = Date.now();
for ( var name in this.sounds ) {
if ( sound.pausedAt ) {
this.play( sound.name );
delete sound.pausedAt;
}
}
};
لو علمنا في وقت سابق أنّنا نسيء استخدام خطأ، لكانت بنية رمز الصوت مختلفة تمامًا. وبالتالي، أثّر ذلك في عدد من أقسام هذه المقالة. له تأثير مباشر هنا ولكن أيضًا في مقتطفات الرموز في Losing Focus وGive Me a Beat. يتطلب فهم كيفية عمل ذلك إجراء تغييرات في كل من الرسم البياني لعقدة Fieldrunners (بما أننا أنشأنا عقدًا لتقصير التشغيل) والرمز الإضافي الذي سيسجل ويوفر حالات الإيقاف المؤقت التي لا يقوم بها Web Audio بمفرده.
فقدان التركيز
تُستخدم العقدة الرئيسية لهذه الميزة. عندما ينتقل مستخدم المتصفّح إلى علامة تبويب أخرى، لن تظهر اللعبة بعد ذلك. بعيدًا عن الأنظار، بعيدًا عن العقل، وكذلك يجب أن يختفي الصوت. هناك حيل يمكن تنفيذها لتحديد حالات ظهور معيّنة لصفحة اللعبة، ولكن أصبح الأمر أسهل بكثير باستخدام Visibility API.
لن يتم تشغيل Fieldrunners إلا كعلامة التبويب النشطة بفضل استخدام requestAnimationFrame لاستدعاء حلقة التحديث الخاصة بها. ومع ذلك، سيستمر سياق Web Audio في تشغيل المؤثرات الصوتية والمقاطع الصوتية في الخلفية بشكل متكرر أثناء وجود المستخدم في علامة تبويب أخرى. ولكن يمكننا إيقاف ذلك باستخدام مقتطف صغير جدًا من Visibility API.
function AudioManager() {
// map and node setup
// ...
// disable all sound when on other tabs
var self = this;
window.addEventListener( 'webkitvisibilitychange', function( e ) {
if ( document.webkitHidden ) {
self.nodes.masterGain.disconnect();
// As noted in Pausing Sounds disconnecting isn't enough.
// For Fieldrunners calling our new pauseEffects method would be
// enough to accomplish that, though we may still need some logic
// to not resume if already paused.
self.pauseEffects();
} else {
self.nodes.masterGain.connect( this.nodes.destination );
self.resumeEffects();
}
});
}
قبل كتابة هذه المقالة، اعتقدنا أنّ فصل الجهاز الرئيسي سيكون كافيًا لإيقاف كل الأصوات مؤقتًا بدلاً من كتمها. من خلال فصل العقدة في ذلك الوقت، أوقفنا العقدة والعقد التابعة لها عن المعالجة والتشغيل. وعند إعادة الاتصال، يتم تشغيل جميع الأصوات والموسيقى من حيث توقفت، تمامًا كما تستمر اللعبة من حيث توقفت. ولكن هذا السلوك غير متوقّع. لا يكفي إلغاء الربط لإيقاف التشغيل.
تسهّل واجهة Page Visibility API معرفة متى لم تعُد علامة التبويب في المقدّمة. إذا كان لديك رمز فعّال لإيقاف الأصوات مؤقتًا، لن يتطلّب الأمر سوى بضعة أسطر لكتابة رمز إيقاف الصوت مؤقتًا عندما تكون علامة تبويب الألعاب مخفية.
أريد سماع إيقاع
لدينا بعض الأشياء التي تم إعدادها الآن. لدينا رسم بياني للعُقد. يمكننا إيقاف الأصوات مؤقتًا عندما يوقف اللاعب اللعبة مؤقتًا، وتشغيل أصوات جديدة لعناصر مثل قوائم اللعبة. يمكننا إيقاف كل الأصوات والموسيقى مؤقتًا عندما ينتقل المستخدم إلى علامة تبويب جديدة. الآن، نحتاج إلى تشغيل صوت.
بدلاً من تشغيل نُسخ متعددة من الصوت لعدة حالات من كيان اللعبة مثل موت شخصية، تشغّل Fieldrunners صوتًا واحدًا مرة واحدة فقط طوال مدته. إذا كان الصوت مطلوبًا بعد انتهاء تشغيله، يمكن إعادة تشغيله ولكن ليس أثناء تشغيله. هذا قرار بشأن تصميم الصوت في Fieldrunners لأنه يحتوي على أصوات يُطلب تشغيلها بسرعة والتي من شأنها أن تتلعثم إذا سُمح لها بإعادة التشغيل أو إنشاء نشاز غير ممتع إذا سُمح لها بتشغيل مثيلات متعددة. من المتوقّع استخدام AudioBufferSourceNodes كلقطات فردية. أنشئ عقدة، وأرفِق مخزنًا مؤقتًا، واضبط القيمة المنطقية للتكرار إذا لزم الأمر، واتصل بعقدة على الرسم البياني تؤدي إلى الوجهة، واستدعِ noteOn أو noteGrainOn، واستدعِ noteOff اختياريًا.
بالنسبة إلى Fieldrunners، يبدو الأمر على النحو التالي:
AudioManager.prototype.play = function( options ) {
var now = Date.now(),
// pull from a map of loaded audio buffers
sound = this.sounds[ options.name ],
channel,
source,
resumeSource;
if ( !sound ) {
return;
}
if ( sound.source ) {
var source = sound.source;
if ( !options.loop && now - source.noteOnAt > sound.buffer.duration * 1000 ) {
// discard the previous source node
source.stop( 0 );
source.disconnect();
} else {
return;
}
}
source = this.audioContext.createBufferSource();
sound.source = source;
// track when the source is started to know if it should still be playing
source.noteOnAt = now;
// help with pausing
sound.ignorePause = !!options.ignorePause;
if ( options.ignorePause ) {
channel = this.nodes.pausedEffectsGain;
} else {
channel = this.nodes.effectsGain;
}
source.buffer = sound.buffer;
source.connect( channel );
source.loop = options.loop || false;
// Fieldrunners' current code doesn't consider sound.pausedAt.
// This is an added section to assist the new pausing code.
if ( sound.pausedAt ) {
source.start( ( sound.buffer.duration * 1000 - sound.pausedAt ) / 1000 );
source.noteOnAt = now + sound.buffer.duration * 1000 - sound.pausedAt;
// if you needed to precisely stop sounds, you'd want to store this
resumeSource = this.audioContext.createBufferSource();
resumeSource.buffer = sound.buffer;
resumeSource.connect( channel );
resumeSource.start(
0,
sound.pausedAt,
sound.buffer.duration - sound.pausedAt / 1000
);
} else {
// start play immediately with a value of 0 or less
source.start( 0 );
}
}
بث محتوى كثير جدًا
تم إطلاق Fieldrunners في الأصل مع موسيقى في الخلفية يتم تشغيلها باستخدام علامة صوتية. عند إصدار اللعبة، اكتشفنا أنّه يتم طلب ملفات الموسيقى لعدد مرات غير متناسب مع عدد مرات طلب بقية محتوى اللعبة. بعد إجراء بعض الأبحاث، اكتشفنا أنّ متصفّح Chrome لم يكن يخزّن مؤقتًا الأجزاء التي تم بثّها من ملفات الموسيقى. وأدى ذلك إلى طلب المتصفّح تشغيل الأغنية كل بضع دقائق عند انتهائها. في الاختبارات الأحدث، خزّن Chrome مؤقتًا المقاطع الصوتية التي تم تشغيلها، ولكن قد لا تكون المتصفحات الأخرى تفعل ذلك بعد. يُعد بث الملفات الصوتية الكبيرة باستخدام علامة الصوت لوظائف مثل تشغيل الموسيقى أمرًا مثاليًا ولكن بالنسبة لبعض إصدارات المتصفح، قد ترغب في تحميل الموسيقى بنفس الطريقة التي تقوم بها بتحميل المؤثرات الصوتية.
بما أنّ جميع المؤثرات الصوتية كانت تعمل من خلال Web Audio، نقلنا تشغيل الموسيقى في الخلفية إلى Web Audio أيضًا. وهذا يعني أننا سنحمّل المقاطع الصوتية بالطريقة نفسها التي حمّلنا بها جميع المؤثرات باستخدام XMLHttpRequests ونوع الاستجابة arraybuffer.
AudioManager.prototype.load = function( options ) {
var xhr,
// pull from a map of name, object pairs
sound = this.sounds[ options.name ];
if ( sound ) {
// this is a great spot to add success methods to a list or use promises
// for handling the load event or call success if already loaded
if ( sound.buffer && options.success ) {
options.success( options.name );
} else if ( options.success ) {
sound.success.push( options.success );
}
// one buffer is enough so shortcut here
return;
}
sound = {
name: options.name,
buffer: null,
source: null,
success: ( options.success ? [ options.success ] : [] )
};
this.sounds[ options.name ] = sound;
xhr = new XMLHttpRequest();
xhr.open( 'GET', options.path, true );
xhr.responseType = 'arraybuffer';
xhr.onload = function( e ) {
sound.buffer = self._context.createBuffer( xhr.response, false );
// call all waiting handlers
sound.success.forEach( function( success ) {
success( sound.name );
});
delete sound.success;
};
xhr.onerror = function( e ) {
// failures are uncommon but you want to do deal with them
};
xhr.send();
}
ملخّص
كانت لعبة Fieldrunners رائعة عند نقلها إلى Chrome وHTML5. بعيدًا عن العمل الشاق الذي تطلّبه نقل آلاف أسطر C++ إلى JavaScript، ظهرت بعض المعضلات والقرارات المثيرة للاهتمام والمتعلقة بـ HTML5. للتأكيد على نقطة واحدة إذا لم تكن أي من النقاط الأخرى، فإن AudioBufferSourceNodes هي كائنات تستخدم لمرة واحدة. يمكنك إنشاؤها وإرفاق مخزن مؤقت للصوت وربطه بالرسم البياني لواجهة برمجة التطبيقات Web Audio وتشغيله باستخدام noteOn أو noteGrainOn. هل تريد تشغيل هذا الصوت مرة أخرى؟ بعد ذلك، أنشئ AudioBufferSourceNode آخر.