Fieldrunners
Fieldrunners adalah game bergaya pertahanan menara pemenang penghargaan yang awalnya dirilis untuk iPhone pada tahun 2008. Sejak saat itu, game ini telah di-porting ke banyak platform lain. Salah satu platform terbaru adalah browser Chrome pada Oktober 2011. Salah satu tantangan dalam mem-porting Fieldrunners ke platform HTML5 adalah cara memutar suara.
Fieldrunners tidak menggunakan efek suara yang rumit, tetapi memiliki beberapa ekspektasi tentang cara berinteraksi dengan efek suaranya. Game ini memiliki 88 efek suara yang sebagian besar dapat diputar sekaligus. Sebagian besar suara ini sangat singkat dan perlu diputar secepat mungkin agar tidak menimbulkan ketidaksesuaian dengan presentasi grafis.
Beberapa tantangan muncul
Saat mem-porting Fieldrunners ke HTML5, kami mengalami masalah pemutaran audio dengan tag Audio dan sejak awal memutuskan untuk berfokus pada Web Audio API. Penggunaan WebAudio membantu kami mengatasi masalah seperti memberikan jumlah efek serentak yang tinggi yang diputar ulang yang diperlukan Fieldrunners. Namun, saat mengembangkan sistem audio untuk Fieldrunners HTML5, kami mengalami beberapa masalah kecil yang mungkin perlu diketahui oleh developer lain.
Sifat AudioBufferSourceNode
AudioBufferSourceNode adalah metode utama Anda untuk memutar suara dengan WebAudio. Sangat penting untuk memahami bahwa objek ini hanya dapat digunakan satu kali. Anda membuat AudioBufferSourceNode, menetapkan buffer, menghubungkannya ke grafik, dan memutarnya dengan noteOn atau noteGrainOn. Setelah itu, Anda dapat memanggil noteOff untuk menghentikan pemutaran, tetapi Anda tidak akan dapat memutar sumber lagi dengan memanggil noteOn atau noteGrainOn - Anda harus membuat AudioBufferSourceNode lain. Namun, Anda dapat menggunakan kembali objek AudioBuffer dasar yang sama (bahkan, Anda dapat memiliki beberapa AudioBufferSourceNode aktif yang mengarah ke instance AudioBuffer yang sama). Anda dapat menemukan cuplikan pemutaran dari Fieldrunners di Give Me a Beat.
Konten yang tidak di-cache
Saat dirilis, server HTML5 Fieldrunners menunjukkan jumlah permintaan yang sangat besar untuk file musik. Hasil ini muncul karena Chrome 15 melanjutkan untuk mendownload file dalam potongan-potongan, lalu tidak menyimpannya dalam cache. Sebagai respons, saat itu kami memutuskan untuk memuat file musik seperti file audio lainnya. Melakukan hal ini kurang optimal, tetapi beberapa versi browser lain masih melakukannya.
Menonaktifkan audio saat tidak fokus
Sebelumnya, mendeteksi kapan tab game Anda tidak fokus adalah hal yang sulit. Fieldrunners mulai melakukan porting sebelum Chrome 13, saat Page Visibility API menggantikan kebutuhan akan kode rumit kami untuk mendeteksi pengaburan tab. Setiap game harus menggunakan Visibility API untuk menulis cuplikan kecil guna membisukan atau menjeda suara jika tidak menjeda seluruh game. Karena Fieldrunners menggunakan API requestAnimationFrame, jeda game ditangani secara implisit, tetapi tidak dengan jeda suara.
Menjeda suara
Anehnya, saat mendapatkan masukan untuk artikel ini, kami diberi tahu bahwa teknik yang kami gunakan untuk menjeda suara tidak sesuai - kami memanfaatkan bug dalam implementasi Web Audio saat ini untuk menjeda pemutaran suara. Karena masalah ini akan diperbaiki pada masa mendatang, Anda tidak dapat menjeda suara hanya dengan memutuskan koneksi node atau subgrafik untuk menghentikan pemutaran.
Arsitektur node Web Audio sederhana
Fieldrunners memiliki model audio yang sangat sederhana. Model tersebut dapat mendukung set fitur berikut:
- Mengontrol volume efek suara.
- Mengontrol volume trek musik latar belakang.
- Membisukan semua audio.
- Nonaktifkan pemutaran suara saat game dijeda.
- Aktifkan kembali suara yang sama saat game dilanjutkan.
- Menonaktifkan semua audio saat tab game kehilangan fokus.
- Mulai ulang pemutaran setelah suara diputar sesuai kebutuhan.
Untuk mencapai fitur di atas dengan Web Audio, digunakan 3 node yang disediakan: DestinationNode, GainNode, AudioBufferSourceNode. AudioBufferSourceNode memutar suara. GainNode menghubungkan AudioBufferSourceNode bersama-sama. DestinationNode, yang dibuat oleh konteks Web Audio, yang disebut destination, memutar suara untuk pemain. Web Audio memiliki lebih banyak jenis node, tetapi hanya dengan node ini, kita dapat membuat grafik yang sangat sederhana untuk suara dalam game.
Grafik node Web Audio mengarah dari node daun ke node tujuan. Fieldrunners menggunakan 6 node penguatan permanen, tetapi 3 node sudah cukup untuk memudahkan kontrol volume dan menghubungkan lebih banyak node sementara yang akan memutar buffer. Pertama, node penguatan master yang menghubungkan setiap node turunan ke tujuan. Dua node penguatan langsung terpasang ke node penguatan master, satu untuk saluran musik dan satu lagi untuk menautkan semua efek suara.
Fieldrunners memiliki 3 node penguatan tambahan karena penggunaan bug yang salah sebagai fitur. Kami menggunakan node tersebut untuk memotong grup suara yang diputar dari grafik yang menghentikan progresnya. Kita melakukan ini untuk menjeda suara. Karena hal ini tidak benar, sekarang kita hanya akan menggunakan total 3 node penguatan seperti yang dijelaskan di atas. Banyak cuplikan berikut akan menyertakan node yang salah, yang menunjukkan apa yang kami lakukan, dan cara kami memperbaikinya dalam jangka pendek. Namun, dalam jangka panjang, Anda tidak ingin menggunakan node kami setelah node 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 );
}
Sebagian besar game memungkinkan kontrol terpisah untuk efek suara dan musik. Hal ini dapat dilakukan dengan mudah menggunakan grafik di atas. Setiap node penguatan memiliki atribut "penguatan" yang dapat ditetapkan ke nilai desimal antara 0 dan 1, yang pada dasarnya dapat digunakan untuk mengontrol volume. Karena kita ingin mengontrol volume saluran musik dan efek suara secara terpisah, kita memiliki node penguatan untuk masing-masing saluran yang dapat kita gunakan untuk mengontrol volumenya.
function setArbitraryVolume() {
var musicGainNode = this.nodes.backgroundMusicGain;
// set music volume to 50%
musicGainNode.gain.value = 0.5;
}
Kita dapat menggunakan kemampuan yang sama ini untuk mengontrol volume semuanya, efek suara, dan musik. Menyetel penguatan node master akan memengaruhi semua suara dari game. Jika Anda menetapkan nilai penguatan ke 0, Anda akan membisukan suara dan musik. AudioBufferSourceNode juga memiliki parameter penguatan. Anda dapat melacak daftar semua suara yang diputar dan menyesuaikan nilai penguatannya satu per satu untuk volume keseluruhan. Jika Anda membuat efek suara dengan tag Audio, inilah yang harus Anda lakukan. Sebagai gantinya, grafik node Web Audio memudahkan Anda mengubah volume suara dari banyak suara. Mengontrol volume dengan cara ini juga memberi Anda daya ekstra tanpa kerumitan. Kita dapat langsung memasang AudioBufferSourceNode ke node master untuk memutar musik dan mengontrol penguatannya sendiri. Namun, Anda harus menetapkan nilai ini setiap kali membuat AudioBufferSourceNode untuk tujuan memutar musik. Sebagai gantinya, Anda hanya mengubah satu node saat pemain mengubah volume musik dan saat peluncuran. Sekarang kita memiliki nilai penguatan pada sumber buffer untuk melakukan hal lain. Untuk musik, salah satu penggunaan yang umum adalah untuk membuat cross fade dari satu trek audio ke trek audio lainnya saat satu trek berakhir dan trek lainnya dimulai. Web Audio menyediakan metode yang bagus untuk melakukan hal ini dengan mudah.
function arbitraryCrossfade( track1, track2 ) {
track1.gain.linearRampToValueAtTime( 0, 1 );
track2.gain.linearRampToValueAtTime( 1, 1 );
}
Fieldrunners tidak menggunakan crossfading secara khusus. Seandainya kami mengetahui fungsi setelan nilai WebAudio selama proses awal sistem suara, kami mungkin akan menggunakannya.
Menjeda suara
Saat pemain menjeda game, mereka dapat mengharapkan beberapa suara tetap diputar. Suara adalah bagian penting dari masukan untuk penekanan umum elemen antarmuka pengguna di menu game. Karena Fieldrunners memiliki sejumlah antarmuka yang dapat digunakan pengguna untuk berinteraksi saat game dijeda, kita tetap ingin antarmuka tersebut diputar. Namun, kita tidak ingin suara yang panjang atau berulang terus diputar. Sangat mudah untuk menghentikan suara tersebut dengan Web Audio atau setidaknya itulah yang kami pikirkan.
AudioManager.prototype.pauseEffects = function() {
this.nodes.effectsGain.disconnect();
}
Node efek yang dijeda masih terhubung. Suara apa pun yang diizinkan untuk mengabaikan status game yang dijeda akan terus diputar. Saat game dilanjutkan, kita dapat menghubungkan kembali node tersebut dan memutar semua suara lagi secara instan.
AudioManager.prototype.resumeEffects = function() {
this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );
}
Setelah mengirimkan Fieldrunners, kami menemukan bahwa memutuskan koneksi node atau subgrafik saja tidak akan menjeda pemutaran AudioBufferSourceNodes. Kami sebenarnya memanfaatkan bug di WebAudio yang saat ini menghentikan pemutaran node yang tidak terhubung ke node Destination dalam grafik. Jadi, untuk memastikan kita siap menghadapi perbaikan di masa mendatang, kita memerlukan beberapa kode seperti berikut:
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;
}
}
};
Jika kami mengetahui hal ini lebih awal, bahwa kami menyalahgunakan bug, struktur kode audio kami akan sangat berbeda. Oleh karena itu, hal ini memengaruhi sejumlah bagian dalam artikel ini. Hal ini memiliki efek langsung di sini, tetapi juga dalam cuplikan kode kita di Losing Focus dan Give Me a Beat. Untuk mengetahui cara kerjanya, diperlukan perubahan pada grafik node Fieldrunners (karena kita membuat node untuk mempersingkat pemutaran) dan kode tambahan yang akan merekam dan memberikan status dijeda yang tidak dilakukan Web Audio sendiri.
Kehilangan fokus
Node master kita berperan untuk fitur ini. Saat pengguna browser beralih ke tab lain, game tidak lagi terlihat. Jauh di mata, jauh di hati, dan begitu juga dengan suaranya. Ada trik yang dapat dilakukan untuk menentukan status visibilitas tertentu untuk halaman game, tetapi hal ini menjadi jauh lebih mudah dengan Visibility API.
Fieldrunners hanya akan diputar sebagai tab aktif berkat penggunaan requestAnimationFrame untuk memanggil loop update-nya. Namun, konteks Web Audio akan terus memutar efek berulang dan trek latar belakang saat pengguna berada di tab lain. Namun, kita dapat menghentikannya dengan cuplikan yang sangat kecil yang mendukung 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();
}
});
}
Sebelum menulis artikel ini, kami mengira bahwa memutuskan koneksi master sudah cukup untuk menjeda semua suara, bukan membisukannya. Dengan memutuskan koneksi node pada saat itu, kita menghentikan node tersebut dan turunannya agar tidak memproses dan memutar audio. Saat terhubung kembali, semua suara dan musik akan mulai diputar dari bagian terakhirnya, sama seperti gameplay yang akan dilanjutkan dari bagian terakhirnya. Namun, ini adalah perilaku yang tidak terduga. Memutuskan koneksi saja tidak cukup untuk menghentikan pemutaran.
Page Visibility API memudahkan Anda mengetahui kapan tab tidak lagi menjadi fokus. Jika Anda sudah memiliki kode yang efektif untuk menjeda suara, Anda hanya perlu menulis beberapa baris untuk menjeda suara saat tab game disembunyikan.
Beri satu irama
Kita telah menyiapkan beberapa hal. Kita memiliki grafik node. Kita dapat menjeda suara saat pemain menjeda game, dan memutar suara baru untuk elemen seperti menu game. Kita dapat menjeda semua suara dan musik saat pengguna beralih ke tab baru. Sekarang kita perlu memutar suara.
Daripada memutar beberapa salinan suara untuk beberapa instance entitas game seperti karakter yang mati, Fieldrunners memutar satu suara hanya sekali selama durasinya. Jika suara diperlukan setelah selesai diputar, suara dapat dimulai ulang, tetapi tidak saat sedang diputar. Ini adalah keputusan untuk desain audio Fieldrunners karena memiliki suara yang diminta untuk diputar dengan cepat, yang akan tersendat jika diizinkan untuk dimulai ulang atau membuat suara bising yang tidak menyenangkan jika diizinkan untuk memutar beberapa instance. AudioBufferSourceNode diharapkan digunakan sebagai satu kali. Buat node, lampirkan buffer, tetapkan nilai boolean loop jika diperlukan, hubungkan ke node pada grafik yang akan mengarah ke tujuan, panggil noteOn atau noteGrainOn, dan panggil noteOff secara opsional.
Untuk Fieldrunners, tampilannya akan terlihat seperti:
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 );
}
}
Terlalu banyak streaming
Fieldrunners awalnya diluncurkan dengan musik latar yang diputar dengan tag Audio. Saat rilis, kami menemukan bahwa file musik diminta dalam jumlah yang tidak proporsional dibandingkan dengan konten game lainnya. Setelah melakukan riset, kami menemukan bahwa pada saat itu browser Chrome tidak meng-cache potongan file musik yang di-streaming. Hal ini menyebabkan browser meminta lagu yang diputar setiap beberapa menit saat lagu selesai. Dalam pengujian yang lebih baru, Chrome meng-cache trek yang di-streaming, tetapi browser lain mungkin belum melakukannya. Melakukan streaming file audio berukuran besar dengan tag Audio untuk fungsi seperti pemutaran musik adalah hal yang optimal, tetapi untuk beberapa versi browser, Anda mungkin ingin memuat musik dengan cara yang sama seperti memuat efek suara.
Karena semua efek suara diputar melalui Web Audio, kami juga memindahkan pemutaran musik latar ke Web Audio. Artinya, kami akan memuat trek dengan cara yang sama seperti kami memuat semua efek dengan XMLHttpRequest dan jenis respons 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();
}
Ringkasan
Fieldrunners sangat menyenangkan untuk dihadirkan ke Chrome dan HTML5. Selain banyaknya pekerjaan yang harus dilakukan untuk mengonversi ribuan baris C++ ke JavaScript, muncul beberapa dilema dan keputusan menarik yang khusus terkait HTML5. Untuk mengulangi salah satu jika tidak ada yang lain, AudioBufferSourceNode adalah objek sekali pakai. Buat node, lampirkan Audio Buffer, hubungkan ke grafik Web Audio, dan putar dengan noteOn atau noteGrainOn. Perlu memutar suara itu lagi? Kemudian, buat AudioBufferSourceNode lain.