Studi Kasus - Bouncy Mouse

Pengantar

Mouse Memantul

Setelah memublikasikan Bouncy Mouse di iOS dan Android pada akhir tahun lalu, saya belajar beberapa pelajaran yang sangat penting. Kuncinya adalah sulit untuk menembus pasar yang sudah mapan. Di pasar iPhone yang benar-benar jenuh, mendapatkan perhatian sangatlah sulit; di Android Marketplace yang kurang satu sama lain, kemajuannya lebih mudah, namun tetap tidak mudah. Berdasarkan pengalaman ini, saya melihat peluang menarik di Chrome Web Store. Meski Web Store sama sekali kosong, katalog game berbasis HTML5 berkualitas tinggi baru saja mulai tumbuh menjadi dewasa. Bagi developer aplikasi baru, ini artinya membuat diagram peringkat dan mendapatkan visibilitas jauh lebih mudah. Dengan mempertimbangkan peluang ini, saya memulai porting Bouncy Mouse ke HTML5 dengan harapan dapat memberikan pengalaman gameplay terbaru saya kepada basis pengguna baru yang menarik. Dalam studi kasus ini, saya akan berbicara sedikit tentang proses umum porting Bouncy Mouse ke HTML5, lalu saya akan menggali lebih dalam ke tiga area yang terbukti menarik: Audio, Performa, dan Monetisasi.

Mem-porting Game C++ ke HTML5

Bouncy Mouse saat ini tersedia di Android(C++), iOS (C++), Windows Phone 7 (C#), dan Chrome (JavaScript). Hal ini terkadang memunculkan pertanyaan: Bagaimana cara Anda menulis game yang dapat dengan mudah ditransfer ke berbagai platform?. Saya merasa bahwa orang-orang berharap mendapatkan peluru ajaib yang dapat mereka gunakan untuk mencapai tingkat portabilitas ini tanpa menggunakan hand-port. Sayangnya, saya belum yakin solusi seperti itu ada (hal terdekat mungkin adalah framework PlayN Google atau mesin Unity, tetapi tidak satu pun yang mencapai semua target yang saya minati). Pendekatan saya, sebenarnya, adalah porta tangan. Saya pertama kali menulis versi iOS/Android di C++, lalu mem-porting kode ini ke setiap platform baru. Meskipun ini mungkin terdengar seperti banyak pekerjaan, masing-masing versi WP7 dan Chrome masing-masing membutuhkan waktu tidak lebih dari 2 minggu untuk menyelesaikannya. Jadi, pertanyaannya adalah, bisakah Anda melakukan sesuatu untuk membuat codebase mudah dilakukan dengan tangan portabel? Ada beberapa hal yang saya lakukan yang membantu hal ini:

Menjaga Codebase Tetap Kecil

Meskipun ini mungkin tampak jelas, ini benar-benar alasan utama saya bisa mentransfer game dengan cepat. Kode klien Bouncy Mouse hanya sekitar 7.000 baris C ++. 7.000 baris kode bukanlah apa-apa, tetapi cukup kecil untuk dapat dikelola. Baik versi C# dan JavaScript dari kode klien akhirnya memiliki ukuran yang kurang lebih sama. Menjaga codebase agar tetap kecil pada dasarnya adalah dua praktik utama: Jangan menulis kode yang berlebihan, dan lakukan sebanyak mungkin dalam kode pra-pemrosesan (non-runtime). Tidak menulis kode yang berlebihan mungkin tampak jelas, tetapi itu adalah satu hal yang selalu saya perjuangkan dengan diri sendiri. Saya sering kali memiliki keinginan untuk menulis class/fungsi helper untuk apa pun yang dapat diperhitungkan menjadi helper. Namun, kecuali Anda benar-benar berencana untuk menggunakan helper beberapa kali, biasanya itu akan membuat kode menjadi berat. Dengan Bouncy Mouse, saya berhati-hati agar tidak pernah menulis helper kecuali jika saya akan menggunakannya setidaknya tiga kali. Ketika saya menulis kelas helper, saya mencoba menjadikannya bersih, portabel, dan dapat digunakan kembali untuk proyek mendatang saya. Di sisi lain, ketika menulis kode hanya untuk Bouncy Mouse, dengan kemungkinan penggunaan kembali yang rendah, fokus saya adalah menyelesaikan tugas coding semudah dan secepat mungkin, meskipun ini bukan cara yang “tercanggih” untuk menulis kode tersebut. Bagian kedua dan yang lebih penting dalam menjaga codebase agar tetap kecil adalah mendorong sebanyak mungkin ke dalam langkah-langkah pra-pemrosesan. Jika Anda dapat mengambil tugas runtime dan memindahkannya ke tugas pra-pemrosesan, game Anda tidak hanya akan berjalan lebih cepat, tetapi Anda juga tidak perlu mem-porting kode ke setiap platform baru. Sebagai contoh, saya awalnya menyimpan data geometri level saya sebagai format yang belum diproses, merakit buffer verteks OpenGL/WebGL yang sebenarnya pada saat runtime. Proses ini membutuhkan sedikit penyiapan dan beberapa ratus baris kode runtime. Kemudian, saya memindahkan kode ini ke langkah pra-pemrosesan, dengan menuliskan buffer verteks OpenGL/WebGL yang terisi penuh pada waktu kompilasi. Jumlah kode sebenarnya hampir sama, tetapi beberapa ratus baris itu telah dipindahkan ke langkah pra-pemrosesan, artinya saya tidak perlu memindahnya ke platform baru. Ada banyak contoh tentang hal ini di Bouncy Mouse, dan hal yang dapat dilakukan akan bervariasi dari satu game ke game lainnya, tetapi perhatikan apa pun yang tidak perlu terjadi saat runtime.

Jangan Ambil Dependensi yang Tidak Anda Perlukan

Alasan lain Bouncy Mouse mudah ditransfer adalah karena hampir tidak memiliki dependensi. Diagram berikut merangkum dependensi library utama Bouncy Mouse per platform:

Android iOS HTML5 WP7
Grafis OpenGL ES OpenGL ES WebGL XNA
Audio OpenSL ES OpenAL Audio Web XNA
Fisika Kotak2D Kotak2D Box2D.js Box2D.xna

Cukup begitu saja. Tidak ada library pihak ketiga besar yang digunakan, selain Box2D, yang portabel di semua platform. Untuk grafik, WebGL dan XNA memetakan hampir 1:1 dengan OpenGL, jadi ini bukan masalah besar. Hanya di area suara yang perpustakaan sebenarnya berbeda. Namun, kode suara di Bouncy Mouse kecil (sekitar seratus baris kode khusus platform), jadi ini bukan masalah besar. Menjaga Bouncy Mouse agar terbebas dari library non-portabel berukuran besar berarti logika kode runtime bisa hampir sama di antara versi (meskipun ada perubahan bahasa). Selain itu, ini menghindarkan kita dari terjebak dalam rantai alat non-portabel. Saya ditanya apakah coding terhadap OpenGL/WebGL secara langsung menyebabkan peningkatan kompleksitas dibandingkan dengan menggunakan library seperti Cocos2D atau Unity (ada beberapa helper WebGL di luar sana). Bahkan, saya yakin hal sebaliknya. Kebanyakan game Ponsel / HTML5 (setidaknya seperti Bouncy Mouse) sangat sederhana. Pada umumnya, game hanya menggambar beberapa sprite dan mungkin beberapa geometri bertekstur. Jumlah total kode khusus OpenGL di Bouncy Mouse mungkin kurang dari 1000 baris. Saya kaget jika menggunakan pustaka pendukung benar-benar akan mengurangi jumlah ini. Bahkan jika jumlah ini terpotong setengahnya, saya harus menghabiskan banyak waktu untuk mempelajari library/alat baru hanya untuk menghemat 500 baris kode. Selain itu, saya belum menemukan library helper yang portabel di semua platform yang saya minati, sehingga mengambil dependensi seperti itu akan sangat merugikan portabilitas. Jika saya menulis game 3d yang membutuhkan peta cahaya, LOD dinamis, animasi berkulit, dan sebagainya, jawaban saya pasti akan berubah. Dalam hal ini saya akan menciptakan kembali roda untuk mencoba mengodekan seluruh mesin saya terhadap OpenGL. Maksud saya di sini adalah bahwa sebagian besar game Seluler/HTML5 belum (belum) masuk dalam kategori ini, jadi tidak perlu merumuskan hal-hal sebelumnya.

Jangan Meremehkan Kesamaan Antara Bahasa

Salah satu trik terakhir yang menghemat banyak waktu dalam mem-porting codebase C++ saya ke bahasa baru adalah kesadaran bahwa sebagian besar kode hampir identik di antara setiap bahasa. Meskipun beberapa elemen utama dapat berubah, elemen ini jauh lebih sedikit daripada elemen-elemen yang tidak berubah. Bahkan, untuk banyak fungsi, beralih dari C++ ke JavaScript hanya melibatkan menjalankan beberapa pengganti ekspresi reguler pada codebase C++ saya.

Kesimpulan Porting

Sekian untuk proses transfer. Saya akan membahas beberapa tantangan khusus HTML5 dalam beberapa bagian selanjutnya, tetapi pesan utamanya adalah, jika Anda membuat kode yang sederhana, porting akan sangat menyulitkan, bukan mimpi buruk.

Audio

Salah satu area yang membuat saya (dan tampaknya semua orang lainnya) mengalami masalah adalah audio. Di iOS dan Android, tersedia sejumlah pilihan audio yang solid (OpenSL, OpenAL), tetapi di dunia HTML5, segalanya tampak lebih suram. Meskipun Audio HTML5 tersedia, saya menemukan bahwa HTML5 memiliki beberapa masalah yang mengganggu transaksi saat digunakan dalam game. Bahkan di browser terbaru, saya sering menemukan perilaku aneh. Chrome, misalnya, tampaknya memiliki batasan jumlah elemen Audio simultan (sumber) yang dapat Anda buat. Selain itu, meskipun diputar, suara terkadang akan terdistorsi secara tidak jelas. Secara keseluruhan, saya sedikit khawatir. Dengan mencari secara {i>online<i}, hasilnya ditemukan bahwa hampir semua orang memiliki masalah yang sama. Solusi yang awalnya saya dapatkan adalah API bernama SoundManager2. Jika tersedia, API ini menggunakan Audio HTML5, dan kembali ke Flash dalam situasi yang rumit. Meskipun berhasil, solusi ini masih mengandung bug dan tidak dapat diprediksi (kurang dari Audio HTML5 murni). Seminggu setelah peluncuran, saya berbincang dengan beberapa orang yang membantu saya di Google, yang mengarahkan saya ke Web Audio API Webkit. Awalnya saya mempertimbangkan untuk menggunakan API ini, tetapi menghindar darinya karena banyaknya kerumitan yang tidak perlu (bagi saya) yang sepertinya dimiliki API. Saya hanya ingin memutar beberapa suara: dengan Audio HTML5, jumlah ini setara dengan beberapa baris JavaScript. Namun, saat sekilas melihat Audio Web, saya dikejutkan oleh spesifikasinya yang besar (70 halaman), kecilnya sampel di web (biasanya untuk API baru), dan penghilangan fungsi "putar", "jeda", atau "berhenti" di mana pun dalam spesifikasi. Dengan jaminan Google bahwa kekhawatiran saya tidak ditemukan dengan baik, saya mempelajari kembali API tersebut. Setelah melihat beberapa contoh lain dan melakukan riset lebih lanjut, saya mendapati bahwa Google benar–API tersebut pasti dapat memenuhi kebutuhan saya, dan dapat melakukannya tanpa bug yang mengganggu API lainnya. Artikel Mulai Menggunakan Web Audio API sangatlah berguna. Artikel ini merupakan referensi yang tepat jika Anda ingin mendapatkan pemahaman yang lebih mendalam tentang API ini. Masalah sebenarnya adalah bahkan setelah memahami dan menggunakan API, sepertinya saya masih seperti API yang tidak dirancang untuk "hanya memutar beberapa suara". Untuk mengatasi keraguan ini, saya menulis class bantuan kecil yang memungkinkan saya menggunakan API seperti yang saya inginkan, yaitu untuk memutar, menjeda, menghentikan, dan membuat kueri status suatu suara. Saya menyebut kelas helper ini AudioClip. Sumber lengkapnya tersedia di GitHub di bawah lisensi Apache 2.0, dan saya akan membahas detail class-nya di bawah ini. Tapi pertama-tama, berikut beberapa latar belakang tentang Web Audio API:

Grafik Audio Web

Hal pertama yang membuat API Audio Web lebih kompleks (dan lebih andal) daripada elemen Audio HTML5 adalah kemampuannya untuk memproses / mencampur audio sebelum menghasilkan output kepada pengguna. Meskipun hebat, fakta bahwa setiap pemutaran audio melibatkan grafik membuat segalanya menjadi sedikit lebih kompleks dalam skenario sederhana. Untuk mengilustrasikan kecanggihan Web Audio API, pertimbangkan grafik berikut:

Grafik Audio Web Dasar
Grafik Audio Web Dasar

Meskipun contoh di atas menunjukkan kecanggihan Web Audio API, saya tidak memerlukan sebagian besar kemampuan ini dalam skenario saya. Saya cuma mau putar suara. Meskipun masih memerlukan grafik, grafiknya sangat sederhana.

Grafik Bisa Sederhana

Hal pertama yang membuat API Audio Web lebih kompleks (dan lebih andal) daripada elemen Audio HTML5 adalah kemampuannya untuk memproses / mencampur audio sebelum menghasilkan output kepada pengguna. Meskipun hebat, fakta bahwa setiap pemutaran audio melibatkan grafik membuat segalanya menjadi sedikit lebih kompleks dalam skenario sederhana. Untuk mengilustrasikan kecanggihan Web Audio API, pertimbangkan grafik berikut:

Grafik Audio Web Tidak Penting
Grafik Audio Web Trivial

Grafik sederhana yang ditampilkan di atas dapat menyelesaikan semua hal yang diperlukan untuk memutar, menjeda, atau menghentikan suara.

Tapi, Jangan Khawatir dengan Grafik

Meskipun memahami grafik itu bagus, saya tidak perlu menanganinya setiap kali saya memutar suara. Oleh karena itu, saya menulis class wrapper sederhana “AudioClip”. Class ini mengelola grafik ini secara internal, tetapi menampilkan API yang jauh lebih sederhana bagi pengguna.

AudioClip
AudioClip

Class ini tidak lebih dari grafik Audio Web dan beberapa status bantuan, tetapi memungkinkan saya menggunakan kode yang jauh lebih sederhana dibandingkan jika saya harus membuat grafik Audio Web untuk memutar setiap suara.

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

// Later
sound.play();

Detail Penerapan

Mari kita lihat sekilas kode class helper: Konstruktor – Konstruktor menangani pemuatan data suara menggunakan XHR. Meskipun tidak ditampilkan di sini (agar contoh tetap sederhana), elemen Audio HTML5 juga dapat digunakan sebagai node sumber. Hal ini sangat membantu untuk sampel yang besar. Perlu diperhatikan bahwa Web Audio API mengharuskan kami mengambil data ini sebagai “arraybuffer”. Setelah data diterima, kami membuat buffer Web Audio dari data ini (mendekodenya dari format aslinya ke dalam format PCM runtime).

/**
* 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();
}

Play – Memutar suara melibatkan dua langkah: menyiapkan grafik pemutaran, dan memanggil versi "noteOn" pada sumber grafik. Sumber hanya dapat diputar sekali, jadi kita harus membuat ulang sumber/grafik setiap kali kita memutarnya. Sebagian besar kompleksitas fungsi ini berasal dari persyaratan yang diperlukan untuk melanjutkan klip yang dijeda (this.pauseTime_ > 0). Untuk melanjutkan pemutaran klip yang dijeda, kami menggunakan noteGrainOn, yang memungkinkan pemutaran sub-region buffer. Sayangnya, noteGrainOn tidak berinteraksi dengan loop dengan cara yang diinginkan untuk skenario ini (ini akan melakukan loop sub-region, bukan seluruh buffer). Oleh karena itu, kita perlu mengatasi masalah ini dengan memutar sisa klip menggunakan noteGrainOn, lalu memulai ulang klip dari awal dengan mengaktifkan loop.

/**
* 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);
}
}
}

Putar sebagai Efek Suara - Fungsi putar di atas tidak memungkinkan klip audio diputar beberapa kali dengan tumpang tindih (pemutaran kedua hanya dapat dilakukan saat klip selesai atau dihentikan). Terkadang game ingin memainkan suara berkali-kali tanpa menunggu setiap pemutaran selesai (mengumpulkan koin dalam game, dll). Untuk mengaktifkan ini, class AudioClip memiliki metode playAsSFX(). Karena beberapa pemutaran dapat terjadi secara bersamaan, pemutaran dari playAsSFX() tidak terikat 1:1 dengan AudioClip. Oleh karena itu, pemutaran tidak dapat dihentikan, dijeda, atau dikueri status. Loop juga dinonaktifkan, karena tidak ada cara untuk menghentikan suara loop yang diputar dengan cara ini.

/**
* 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);
}
}

Berhenti, jeda, dan buat kueri status – Fungsi lainnya cukup sederhana dan tidak memerlukan banyak penjelasan:

/**
* 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));
}

Kesimpulan Audio

Semoga class bantuan ini berguna bagi developer yang kesulitan dengan masalah Audio yang sama seperti saya. Selain itu, class seperti ini tampaknya merupakan tempat yang wajar untuk memulai, bahkan jika Anda perlu menambahkan beberapa fitur yang lebih canggih dari Web Audio API. Bagaimanapun, solusi ini memenuhi kebutuhan Bouncy Mouse, dan memungkinkan game tersebut menjadi game HTML5 sejati, tanpa pamrih!

Performa

Area lain yang membuat saya khawatir terkait port JavaScript adalah performa. Setelah menyelesaikan port v1, saya mendapati bahwa semuanya berfungsi dengan baik di desktop quad-core saya. Sayangnya, kondisi di netbook atau Chromebook menjadi lebih sedikit. Dalam hal ini, profiler Chrome menghemat waktu saya dengan menampilkan secara persis di mana semua waktu program saya dihabiskan. Pengalaman saya menyoroti pentingnya pembuatan profil sebelum melakukan pengoptimalan apa pun. Saya mengharapkan fisika Box2D atau mungkin kode rendering menjadi sumber utama pelambatan; namun, sebagian besar waktu saya sebenarnya dihabiskan di fungsi Matrix.clone(). Mengingat game saya yang sarat matematika, saya tahu bahwa saya melakukan banyak pembuatan/kloning matriks, tetapi saya tidak pernah mengharapkan hal ini menjadi hambatan. Pada akhirnya, perubahan yang sangat sederhana memungkinkan game ini memangkas penggunaan CPU lebih dari 3x lipat, dari 6-7% CPU di desktop saya menjadi 2%. Mungkin ini adalah pengetahuan umum bagi developer JavaScript, tetapi sebagai developer C++, masalah ini mengejutkan saya, jadi saya akan membahasnya lebih detail. Pada dasarnya, kelas matriks asli saya adalah matriks 3x3: array 3 elemen, setiap elemen berisi array 3 elemen. Sayangnya, ini berarti ketika tiba waktunya untuk meng-clone matriks, saya harus membuat 4 array baru. Satu-satunya perubahan yang perlu saya buat adalah memindahkan data ini ke dalam array 9 elemen tunggal dan memperbarui matematika saya. Satu perubahan ini sepenuhnya bertanggung jawab atas pengurangan CPU 3x yang saya lihat, dan setelah perubahan ini, performa saya dapat diterima di semua perangkat pengujian saya.

Pengoptimalan Lainnya

Meskipun performa saya dapat diterima, saya masih melihat beberapa gangguan kecil. Setelah membuat profil, saya menyadari bahwa hal ini disebabkan oleh Pengumpulan Sampah dalam JavaScript. Aplikasi saya berjalan pada 60 fps, yang berarti bahwa setiap frame hanya memiliki waktu 16 md untuk menggambar. Sayangnya, ketika pembersihan sampah memori dijalankan di mesin yang lebih lambat, tugas ini terkadang memakan waktu sekitar 10 md. Hal ini menyebabkan ketersendatan beberapa detik karena game membutuhkan hampir 16 md penuh untuk menggambar frame penuh. Untuk mendapatkan gambaran yang lebih baik tentang mengapa saya menghasilkan begitu banyak sampah, saya menggunakan profiler heap Chrome. Saya sangat putus asa, ternyata sebagian besar sampah (lebih dari 70%) dihasilkan oleh Box2D. Menghilangkan sampah dalam JavaScript adalah bisnis yang rumit, dan menulis ulang Box2D tidak mungkin dilakukan, jadi saya menyadari bahwa saya telah terlalu fokus. Untungnya, masih ada salah satu trik tertua di buku ini: Ketika Anda tidak dapat mencapai 60 fps, larilah pada 30 fps. Sudah cukup disepakati bahwa berjalan pada 30 fps yang konsisten jauh lebih baik daripada berjalan pada 60 fps yang gelisah. Bahkan, saya masih belum menerima satu keluhan atau komentar bahwa game ini berjalan pada 30 fps (sangat sulit untuk dikatakan kecuali jika Anda membandingkan kedua versi tersebut secara berdampingan). Tambahan 16 md per frame ini berarti, meskipun dalam kasus pembersihan sampah memori yang jelek, saya masih punya banyak waktu untuk merender frame. Meskipun berjalan pada 30 fps tidak secara eksplisit diaktifkan oleh API pengaturan waktu yang saya gunakan (requestAnimationFrame WebKit yang sangat baik), hal ini dapat dilakukan dengan cara yang sangat sepele. Meskipun mungkin tidak secanggih API eksplisit, 30 fps dapat dicapai dengan mengetahui bahwa interval RequestAnimationFrame sejajar dengan VSYNC monitor (biasanya 60 fps). Ini berarti kita hanya harus mengabaikan setiap callback lainnya. Pada dasarnya, jika Anda memiliki callback "Tick" yang dipanggil setiap kali "RequestAnimationFrame" diaktifkan, hal ini dapat dilakukan sebagai berikut:

var skip = false;

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

// OTHER CODE
}

Jika Anda ingin lebih berhati-hati, Anda harus memeriksa apakah VSYNC komputer tidak sudah berada pada atau di bawah 30 fps saat memulai, dan dalam kasus ini, nonaktifkan fitur lewati. Namun, saya belum melihat ini pada konfigurasi desktop/laptop yang telah saya uji.

Distribusi dan Monetisasi

Satu area terakhir yang mengejutkan tentang porta Chrome di Bouncy Mouse adalah monetisasi. Dalam proyek ini, saya membayangkan game HTML5 sebagai eksperimen yang menarik untuk mempelajari teknologi yang sedang naik daun. Yang tidak saya sadari adalah bahwa porta ini akan menjangkau audiens yang sangat besar dan memiliki potensi yang signifikan untuk monetisasi.

Bouncy Mouse diluncurkan pada akhir Oktober di Chrome Web Store. Dengan merilis di Chrome Web Store, saya bisa memanfaatkan sistem yang sudah ada untuk memudahkan visibilitas, interaksi komunitas, peringkat, dan fitur lain yang sudah saya biasa gunakan di platform seluler. Yang mengejutkan saya adalah seberapa luas jangkauan tokonya. Dalam waktu satu bulan setelah rilis, saya telah mencapai hampir empat ratus ribu penginstalan dan sudah mendapatkan manfaat dari interaksi komunitas (pelaporan bug, masukan). Satu hal lain yang mengejutkan saya adalah potensi monetisasi aplikasi web.

Bouncy Mouse memiliki satu metode monetisasi sederhana, yaitu iklan banner di samping konten game. Namun, mengingat jangkauan luas dari game ini, saya menyadari bahwa iklan banner ini menghasilkan pendapatan yang signifikan, dan selama periode puncaknya, aplikasi ini menghasilkan pendapatan yang sebanding dengan platform saya yang paling sukses, Android. Salah satu faktor yang berkontribusi pada hal ini adalah bahwa iklan AdSense yang lebih besar yang ditampilkan pada versi HTML5 menghasilkan pendapatan per tayangan yang jauh lebih tinggi daripada iklan AdMob yang lebih kecil yang ditampilkan di Android. Tidak hanya itu, iklan banner pada versi HTML5 tidak begitu mengganggu dibandingkan pada versi Android, sehingga memberikan pengalaman bermain yang lebih rapi. Secara keseluruhan, saya sangat terkejut dengan hasil ini.

Penghasilan yang Dinormalkan Seiring Waktu.
Penghasilan yang Dinormalkan Seiring Waktu

Meskipun penghasilan dari game ini jauh lebih baik daripada yang diharapkan, perlu diperhatikan bahwa jangkauan Chrome Web Store masih lebih kecil dibandingkan dengan platform yang lebih matang seperti Android Market. Sementara Bouncy Mouse mampu dengan cepat menembak hingga game paling populer #9 di Chrome Web Store, tingkat pengguna baru yang datang ke situs melambat secara signifikan sejak rilis awal. Meskipun demikian, game ini masih mengalami pertumbuhan yang stabil, dan saya tidak sabar untuk melihat perkembangan platform ini.

Kesimpulan

Menurut saya, mentransfer Bouncy Mouse ke Chrome berjalan jauh lebih lancar dari perkiraan saya. Selain beberapa masalah kecil pada audio dan performa, saya mendapati bahwa Chrome adalah platform yang sangat mumpuni untuk game smartphone yang sudah ada. Saya akan mendorong setiap developer yang telah menyembunyikan pengalaman ini untuk mencobanya. Saya sangat senang dengan proses porting dan audiens game baru yang membuat saya tertarik dengan game HTML5. Silakan kirim email kepada kami jika Anda memiliki pertanyaan. Atau cukup tulis komentar di bawah, saya akan mencoba memeriksanya secara rutin.