Pengantar
Setelah memublikasikan Bouncy Mouse di iOS dan Android pada akhir tahun lalu, saya mendapatkan beberapa pelajaran yang sangat penting. Salah satu tantangan utamanya adalah sulitnya masuk ke pasar yang sudah mapan. Di pasar iPhone yang sangat jenuh, mendapatkan daya tarik sangat sulit; di Android Marketplace yang kurang jenuh, progresnya lebih mudah, tetapi tetap tidak mudah. Dengan pengalaman ini, saya melihat peluang yang menarik di Chrome Web Store. Meskipun Web Store tidak kosong, katalog game berbasis HTML5 berkualitas tinggi baru saja mulai berkembang. Bagi developer aplikasi baru, hal ini berarti membuat diagram peringkat dan mendapatkan visibilitas menjadi jauh lebih mudah. Dengan mempertimbangkan peluang ini, saya mulai mem-port Bouncy Mouse ke HTML5 dengan harapan dapat memberikan pengalaman gameplay terbaru saya kepada basis pengguna baru yang menarik. Dalam studi kasus ini, saya akan membahas sedikit tentang proses umum porting Bouncy Mouse ke HTML5, lalu saya akan mempelajari lebih dalam tiga area yang terbukti menarik: Audio, Performa, dan Monetisasi.
Mentransfer 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 menulis game yang dapat dengan mudah di-porting ke beberapa platform?. Saya merasa bahwa orang-orang berharap ada solusi ajaib yang dapat mereka gunakan untuk mencapai tingkat portabilitas ini tanpa harus menggunakan port tangan. Sayangnya, saya tidak yakin solusi semacam itu sudah ada (hal yang paling mendekati mungkin adalah framework PlayN Google atau engine Unity, tetapi keduanya tidak memenuhi semua target yang saya minati). Pendekatan saya sebenarnya adalah port tangan. Saya pertama-tama menulis versi iOS/Android dalam C++, lalu mem-porting kode ini ke setiap platform baru. Meskipun mungkin terdengar seperti banyak pekerjaan, versi WP7 dan Chrome masing-masing memerlukan waktu tidak lebih dari 2 minggu untuk diselesaikan. Jadi, sekarang pertanyaannya adalah, apakah ada yang dapat dilakukan untuk membuat codebase mudah dibawa-bawa? Ada beberapa hal yang saya lakukan yang membantu dalam hal ini:
Mempertahankan Basis Kode Tetap Kecil
Meskipun hal ini mungkin tampak jelas, ini adalah alasan utama saya dapat mem-port game dengan sangat cepat. Kode klien Bouncy Mouse hanya sekitar 7.000 baris C++. 7.000 baris kode bukanlah jumlah yang kecil, tetapi cukup kecil untuk dikelola. Versi kode klien C# dan JavaScript memiliki ukuran yang kurang lebih sama. Mempertahankan codebase saya tetap kecil pada dasarnya merupakan dua praktik utama: Jangan menulis kode yang berlebihan, dan lakukan sebanyak mungkin dalam kode prapemrosesan (non-runtime). Tidak menulis kode yang berlebihan mungkin tampak jelas, tetapi ini adalah satu hal yang selalu saya perjuangkan. Saya sering kali ingin menulis class/fungsi helper untuk apa pun yang dapat dipertimbangkan sebagai helper. Namun, kecuali jika Anda benar-benar berencana menggunakan helper beberapa kali, biasanya kode Anda akan membengkak. Dengan Bouncy Mouse, saya berhati-hati untuk tidak pernah menulis helper kecuali jika saya akan menggunakannya setidaknya tiga kali. Saat menulis class helper, saya mencoba membuatnya bersih, portabel, dan dapat digunakan kembali untuk project mendatang. Di sisi lain, saat menulis kode hanya untuk Bouncy Mouse, dengan kemungkinan penggunaan kembali yang rendah, fokus saya adalah menyelesaikan tugas coding sesederhana dan secepat mungkin, meskipun ini bukan cara “tercantik” untuk menulis kode. Bagian kedua, dan yang lebih penting untuk menjaga codebase tetap kecil adalah mendorong sebanyak mungkin ke langkah prapemrosesan. Jika Anda dapat mengambil tugas runtime dan memindahkannya ke tugas prapemrosesan, game Anda tidak hanya akan berjalan lebih cepat, tetapi Anda juga tidak perlu melakukan port kode ke setiap platform baru. Sebagai contoh, saya awalnya menyimpan data geometri level sebagai format yang cukup belum diproses, dengan menyusun buffer vertex OpenGL/WebGL yang sebenarnya saat runtime. Proses ini memerlukan sedikit penyiapan dan beberapa ratus baris kode runtime. Kemudian, saya memindahkan kode ini ke langkah prapemrosesan, dengan menulis buffer vertex OpenGL/WebGL yang dikemas sepenuhnya pada waktu kompilasi. Jumlah kode yang sebenarnya hampir sama, tetapi beberapa ratus baris tersebut telah dipindahkan ke langkah prapemrosesan, yang berarti saya tidak perlu mem-portingnya ke platform baru. Ada banyak contoh hal ini di Bouncy Mouse, dan kemungkinannya akan bervariasi dari game ke game, tetapi perhatikan saja apa pun yang tidak perlu terjadi saat runtime.
Jangan Mengambil Dependensi yang Tidak Diperlukan
Alasan lain Bouncy Mouse mudah di-port adalah karena hampir tidak memiliki dependensi. Diagram berikut merangkum dependensi library utama Bouncy Mouse per platform:
Cukup begitu saja. Tidak ada library pihak ketiga besar yang digunakan, selain Box2D, yang portabel di semua platform. Untuk grafis, WebGL dan XNA memetakan hampir 1:1 dengan OpenGL, sehingga ini bukan masalah besar. Hanya di area suara yang library-nya berbeda. Namun, kode suara di Bouncy Mouse berukuran kecil (sekitar seratus baris kode khusus platform), sehingga ini bukan masalah besar. Memastikan Bouncy Mouse bebas dari library besar yang tidak portabel berarti logika kode runtime dapat hampir sama di antara versi (meskipun ada perubahan bahasa). Selain itu, hal ini mencegah kita terkunci dalam rantai alat yang tidak portabel. Saya pernah ditanya apakah coding terhadap OpenGL/WebGL secara langsung menyebabkan peningkatan kompleksitas dibandingkan dengan menggunakan library seperti Cocos2D atau Unity (ada juga beberapa helper WebGL). Bahkan, saya percaya sebaliknya. Sebagian besar game Ponsel / HTML5 (setidaknya game 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 1.000 baris. Saya akan terkejut jika menggunakan library helper benar-benar akan mengurangi jumlah ini. Meskipun jumlah ini dapat dikurangi setengahnya, saya harus menghabiskan waktu yang cukup lama 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 menggunakan dependensi tersebut akan sangat mengurangi portabilitas. Jika saya menulis game 3D yang memerlukan lightmap, LOD dinamis, animasi dengan skin, dan sebagainya, jawaban saya pasti akan berubah. Dalam hal ini, saya akan membuat ulang roda untuk mencoba membuat kode seluruh mesin saya secara manual terhadap OpenGL. Maksud saya di sini adalah sebagian besar game Seluler/HTML5 belum (mungkin) masuk dalam kategori ini, jadi tidak perlu mempersulit sebelum diperlukan.
Jangan Meremehkan Kesamaan Antarbahasa
Satu trik terakhir yang menghemat banyak waktu dalam melakukan porting codebase C++ saya ke bahasa baru adalah menyadari bahwa sebagian besar kode hampir identik di antara setiap bahasa. Meskipun beberapa elemen utama dapat berubah, jumlahnya jauh lebih sedikit daripada hal-hal yang tidak berubah. Faktanya, untuk banyak fungsi, beralih dari C++ ke JavaScript hanya melibatkan beberapa penggantian ekspresi reguler di codebase C++ saya.
Kesimpulan Porting
Cukup begitu saja untuk proses transfer. Saya akan membahas beberapa tantangan khusus HTML5 di beberapa bagian berikutnya, tetapi pesan utamanya adalah, jika Anda membuat kode tetap sederhana, porting akan menjadi masalah kecil, bukan mimpi buruk.
Audio
Salah satu area yang menyebabkan saya (dan tampaknya semua orang) mengalami masalah adalah audio. Di iOS dan Android, sejumlah pilihan audio yang solid tersedia (OpenSL, OpenAL), tetapi di dunia HTML5, semuanya terlihat lebih suram. Meskipun Audio HTML5 tersedia, saya mendapati bahwa audio ini memiliki beberapa masalah yang dapat merusak kesepakatan saat digunakan dalam game. Bahkan di browser terbaru, saya sering mengalami perilaku aneh. Misalnya, Chrome tampaknya memiliki batas jumlah elemen Audio serentak (sumber) yang dapat Anda buat. Selain itu, meskipun suara akan diputar, terkadang suara akan terdistorsi tanpa alasan yang jelas. Secara keseluruhan, saya sedikit khawatir. Penelusuran online menunjukkan bahwa hampir semua orang mengalami masalah yang sama. Solusi yang awalnya saya temukan adalah API bernama SoundManager2. API ini menggunakan Audio HTML5 jika tersedia, dan melakukan fallback ke Flash dalam situasi yang sulit. Meskipun solusi ini berhasil, solusi ini masih memiliki bug dan tidak dapat diprediksi (hanya lebih sedikit daripada Audio HTML5 murni). Seminggu setelah peluncuran, saya berbicara dengan beberapa orang yang membantu di Google, yang mengarahkan saya ke Web Audio API Webkit. Awalnya saya mempertimbangkan untuk menggunakan API ini, tetapi saya menghindarinya karena API tersebut tampaknya memiliki kompleksitas yang tidak perlu (bagi saya). Saya hanya ingin memutar beberapa suara: dengan Audio HTML5, ini setara dengan beberapa baris JavaScript. Namun, dalam pengamatan singkat saya terhadap Web Audio, saya terkejut dengan spesifikasinya yang sangat besar (70 halaman), jumlah sampel yang sedikit di web (biasa untuk API baru), dan penghapusan fungsi “putar”, “jeda”, atau “berhenti” di mana pun dalam spesifikasi. Dengan jaminan Google bahwa kekhawatiran saya tidak berdasar, saya mempelajari API lagi. Setelah melihat beberapa contoh lainnya dan melakukan sedikit riset lebih lanjut, saya mendapati bahwa Google benar–API ini pasti dapat memenuhi kebutuhan saya, dan dapat melakukannya tanpa bug yang mengganggu API lainnya. Artikel Memulai Web Audio API sangat berguna, yang merupakan tempat yang tepat jika Anda ingin mendapatkan pemahaman yang lebih mendalam tentang API. Masalah sebenarnya adalah meskipun setelah memahami dan menggunakan API, API ini masih tampak seperti API yang tidak dirancang untuk “hanya memutar beberapa suara”. Untuk mengatasi keraguan ini, saya menulis class helper kecil yang memungkinkan saya menggunakan API seperti yang saya inginkan–untuk memutar, menjeda, menghentikan, dan membuat kueri status suara. Saya menyebut class helper ini AudioClip. Sumber lengkap tersedia di GitHub berdasarkan lisensi Apache 2.0, dan saya akan membahas detail class di bawah. Namun, pertama-tama, beberapa latar belakang tentang Web Audio API:
Grafik Audio Web
Hal pertama yang membuat Web Audio API lebih kompleks (dan lebih canggih) daripada elemen Audio HTML5 adalah kemampuannya untuk memproses / menggabungkan audio sebelum menampilkannya kepada pengguna. Meskipun canggih, fakta bahwa pemutaran audio apa pun melibatkan grafik membuat semuanya menjadi sedikit lebih rumit dalam skenario sederhana. Untuk mengilustrasikan kecanggihan Web Audio API, pertimbangkan grafik berikut:
Meskipun contoh di atas menunjukkan kemampuan Web Audio API, saya tidak memerlukan sebagian besar kemampuan ini dalam skenario saya. Saya hanya ingin memutar suara. Meskipun masih memerlukan grafik, grafiknya sangat sederhana.
Grafik Dapat Sederhana
Hal pertama yang membuat Web Audio API lebih kompleks (dan lebih canggih) daripada elemen Audio HTML5 adalah kemampuannya untuk memproses / menggabungkan audio sebelum menampilkannya kepada pengguna. Meskipun canggih, fakta bahwa pemutaran audio apa pun melibatkan grafik membuat semuanya menjadi sedikit lebih rumit dalam skenario sederhana. Untuk mengilustrasikan kecanggihan Web Audio API, pertimbangkan grafik berikut:
Grafik sederhana yang ditampilkan di atas dapat menyelesaikan semua hal yang diperlukan untuk memutar, menjeda, atau menghentikan suara.
Namun, Jangan Khawatir dengan Grafik
Meskipun memahami grafik itu bagus, saya tidak ingin berurusan dengan grafik setiap kali memutar suara. Oleh karena itu, saya menulis class wrapper sederhana “AudioClip”. Class ini mengelola grafik ini secara internal, tetapi menampilkan API yang lebih sederhana bagi pengguna.
Class ini tidak lebih dari grafik Web Audio dan beberapa status helper, tetapi memungkinkan saya menggunakan kode yang jauh lebih sederhana daripada jika saya harus membuat grafik Web Audio 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. Perhatikan bahwa Web Audio API mengharuskan kita mengambil data ini sebagai “arraybuffer”. Setelah data diterima, kita membuat buffer Web Audio dari data ini (mendekodenya dari format aslinya menjadi 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();
}
Memutar – 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 memutar.
Sebagian besar kompleksitas fungsi ini berasal dari persyaratan yang diperlukan untuk melanjutkan klip yang dijeda (this.pauseTime_ > 0
). Untuk melanjutkan pemutaran klip yang dijeda, kita menggunakan noteGrainOn
,
yang memungkinkan pemutaran sub-wilayah buffering. Sayangnya, noteGrainOn
tidak berinteraksi dengan looping dengan cara yang diinginkan untuk skenario ini (noteGrainOn
akan melakukan loop pada sub-region, bukan seluruh buffer).
Oleh karena itu, kita perlu mengatasinya dengan memutar sisa klip dengan noteGrainOn
, lalu memulai ulang klip dari awal dengan mengaktifkan looping.
/**
* 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 memutar suara beberapa kali tanpa menunggu setiap pemutaran selesai (mengumpulkan koin dalam game, dll.). Untuk mengaktifkannya, 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 statusnya. Looping juga dinonaktifkan, karena tidak ada cara untuk menghentikan suara looping 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);
}
}
Menghentikan, menjeda, dan membuat kueri status – Fungsi lainnya cukup mudah 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 helper ini berguna bagi developer yang mengalami masalah Audio yang sama seperti saya. Selain itu, class seperti ini tampaknya merupakan tempat yang wajar untuk memulai meskipun Anda perlu menambahkan beberapa fitur Web Audio API yang lebih canggih. Apa pun solusinya, solusi ini memenuhi kebutuhan Bouncy Mouse, dan memungkinkan game menjadi game HTML5 sejati, tanpa syarat apa pun.
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, hal-hal tersebut tidak terlalu baik di netbook atau Chromebook. Dalam hal ini, profiler Chrome menyelamatkan saya dengan menunjukkan dengan tepat tempat semua waktu program saya dihabiskan.
Pengalaman saya menunjukkan pentingnya pembuatan profil sebelum melakukan pengoptimalan apa pun. Saya mengira fisika Box2D atau mungkin kode rendering menjadi sumber utama pelambatan; namun, sebagian besar waktu saya sebenarnya dihabiskan dalam fungsi Matrix.clone()
. Mengingat sifat game saya yang banyak menggunakan matematika, saya tahu bahwa saya melakukan banyak pembuatan/pembuatan duplikat matriks, tetapi saya tidak pernah menyangka bahwa ini akan menjadi bottleneck. Pada akhirnya, ternyata perubahan yang sangat sederhana memungkinkan game mengurangi penggunaan CPU-nya hingga 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 secara lebih mendetail. Pada dasarnya, class matriks asli saya adalah matriks 3x3: array 3 elemen, setiap elemen berisi array 3 elemen. Sayangnya, ini berarti bahwa saat tiba waktunya untuk meng-clone matriks, saya harus membuat 4 array baru. Satu-satunya perubahan yang perlu saya lakukan adalah memindahkan data ini ke dalam satu array 9 elemen dan memperbarui matematika saya. Satu perubahan ini sepenuhnya bertanggung jawab atas pengurangan CPU 3x lipat yang saya lihat, dan setelah perubahan ini, performa saya dapat diterima di semua perangkat pengujian.
Pengoptimalan Lainnya
Meskipun performa saya dapat diterima, saya masih mengalami beberapa masalah kecil. Setelah melakukan profiling lebih lanjut, saya menyadari bahwa hal ini disebabkan oleh Pengumpulan Sampah Javascript. Aplikasi saya berjalan pada 60 fps, yang berarti setiap frame hanya memiliki waktu 16 md untuk menggambar. Sayangnya, saat pembersihan sampah memori diaktifkan di komputer yang lebih lambat, terkadang pembersihan sampah memori akan menghabiskan waktu ~10 md. Hal ini menyebabkan gangguan setiap beberapa detik, karena game memerlukan hampir 16 md penuh untuk menggambar frame penuh. Untuk mendapatkan gambaran yang lebih baik tentang alasan saya menghasilkan begitu banyak sampah, saya menggunakan profiler heap Chrome. Saya sangat kecewa karena ternyata sebagian besar sampah (lebih dari 70%) dihasilkan oleh Box2D. Menghapus sampah di JavaScript adalah hal yang rumit, dan menulis ulang Box2D tidak mungkin dilakukan, jadi saya menyadari bahwa saya telah terjebak. Untungnya, saya masih memiliki salah satu trik paling lama yang tersedia: Jika Anda tidak dapat mencapai 60 fps, jalankan pada 30 fps. Sudah cukup disepakati bahwa menjalankan game pada kecepatan 30 fps yang konsisten jauh lebih baik daripada menjalankan game pada kecepatan 60 fps yang tidak stabil. Bahkan, saya masih belum menerima satu keluhan atau komentar bahwa game berjalan pada 30 fps (sangat sulit untuk mengetahuinya kecuali jika Anda membandingkan kedua versi secara berdampingan). Tambahan 16 md per frame ini berarti bahwa meskipun dalam kasus pembersihan sampah memori yang buruk, saya masih memiliki banyak waktu untuk merender frame. Meskipun berjalan pada 30 fps tidak diaktifkan secara eksplisit oleh API pengaturan waktu yang saya gunakan (requestAnimationFrame WebKit yang sangat baik), hal ini dapat dilakukan dengan cara yang sangat sederhana. Meskipun mungkin tidak seanggun API eksplisit, 30 fps dapat dicapai dengan mengetahui bahwa interval RequestAnimationFrame diselaraskan dengan VSYNC monitor (biasanya 60 fps). Artinya, kita hanya perlu 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 ingin lebih berhati-hati, Anda harus memeriksa apakah VSYNC komputer sudah mencapai atau di bawah 30 fps saat memulai, dan menonaktifkan lewati dalam hal ini. Namun, saya belum melihatnya di konfigurasi desktop/laptop yang telah saya uji.
Distribusi dan Monetisasi
Satu hal terakhir yang mengejutkan saya tentang port Chrome Bouncy Mouse adalah monetisasi. Dalam project ini, saya membayangkan game HTML5 sebagai eksperimen yang menarik untuk mempelajari teknologi yang sedang berkembang. Yang tidak saya sadari adalah port tersebut akan menjangkau audiens yang sangat besar dan memiliki potensi monetisasi yang signifikan.
Bouncy Mouse diluncurkan pada akhir Oktober di Chrome Web Store. Dengan merilis di Chrome Web Store, saya dapat memanfaatkan sistem yang ada untuk visibilitas, interaksi komunitas, peringkat, dan fitur lainnya yang sudah saya gunakan di platform seluler. Yang mengejutkan saya adalah jangkauan toko yang sangat luas. Dalam waktu satu bulan sejak 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 - iklan banner di samping konten game. Namun, mengingat jangkauan game yang luas, saya mendapati bahwa iklan banner ini dapat menghasilkan pendapatan yang signifikan, dan selama periode puncaknya, aplikasi menghasilkan pendapatan yang sebanding dengan platform saya yang paling sukses, Android. Salah satu faktor yang berkontribusi pada hal ini adalah iklan AdSense yang lebih besar yang ditampilkan di versi HTML5 menghasilkan pendapatan per tayangan iklan yang jauh lebih tinggi daripada iklan AdMob yang lebih kecil yang ditampilkan di Android. Tidak hanya itu, iklan banner di versi HTML5 jauh lebih tidak mengganggu daripada di versi Android, sehingga memungkinkan pengalaman gameplay yang lebih bersih. Secara keseluruhan, saya sangat terkejut dengan hasilnya.

Meskipun penghasilan dari game jauh lebih baik dari yang diharapkan, perlu diperhatikan bahwa jangkauan Chrome Web Store masih lebih kecil daripada platform yang lebih matang seperti Android Market. Meskipun Bouncy Mouse dapat dengan cepat naik ke posisi ke-9 game paling populer di Chrome Web Store, laju pengguna baru yang datang ke situs tersebut 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
Saya dapat mengatakan bahwa proses transfer Bouncy Mouse ke Chrome berjalan jauh lebih lancar dari yang saya harapkan. Selain beberapa masalah audio dan performa minor, saya mendapati bahwa Chrome adalah platform yang sangat mumpuni untuk game smartphone yang ada. Saya mendorong developer yang telah menghindari pengalaman ini untuk mencobanya. Saya sangat senang dengan proses porting serta audiens game baru yang terhubung dengan saya karena memiliki game HTML5. Jangan ragu untuk mengirim email kepada kami jika ada pertanyaan. Atau, cukup berikan komentar di bawah. Kami akan mencoba memeriksanya secara rutin.