Örnek Olay - Bouncy Mouse

Giriş

Zıplayan Fare

Geçen yılın sonunda Bouncy Mouse'u iOS ve Android'de yayınladıktan sonra çok önemli birkaç ders çıkardım. Bunların en önemlisi, köklü bir pazara girmenin zor olmasıydı. Oldukça doymuş iPhone pazarında ilgi kazanmak çok zordu. Doygunluğu daha düşük olan Android Marketplace'te ilerleme daha kolay olsa da yine de kolay değildi. Bu deneyime dayanarak Chrome Web Mağazası'nda ilginç bir fırsat gördüm. Web Mağazası kesinlikle boş olmasa da, yüksek kaliteli HTML5 tabanlı oyunlardan oluşan kataloğu, olgunlaşma yolunda büyümeye yeni başlıyor. Yeni bir uygulama geliştirici için bu, sıralama grafiklerini oluşturmanın ve görünürlük kazanmanın çok daha kolay olduğu anlamına gelir. Bu fırsatı düşünerek, en son oyun deneyimimi heyecan verici yeni bir kullanıcı tabanına sunabilmem umuduyla Bouncy Mouse'u HTML5'e taşımaya karar verdim. Bu örnek olay incelemesinde, Bouncy Mouse'un HTML5'e taşınmasıyla ilgili genel süreçten bahsedeceğim ve ardından, ilgi çekici olduğunu düşündüğüm üç alanı biraz daha ayrıntılı olarak inceleyeceğiz: Ses, Performans ve Para Kazanma.

C++ Oyunlarını HTML5'e Taşıma

Bouncy Mouse şu anda Android(C++), iOS (C++), Windows Phone 7 (C#) ve Chrome'da (JavaScript) kullanılabilir. Bu da bazen şu soruyu soruyor: Birden çok platforma kolayca taşınabilecek bir oyun nasıl yazılır? İnsanların el taşımak zorunda kalmadan bu taşınabilirlik düzeyine ulaşmak için kullanabilecekleri sihirli bir kurşun umduklarını hissediyorum. Ne yazık ki böyle bir çözümün henüz mevcut olduğundan emin değilim (en yakın olanı muhtemelen Google'ın PlayN çerçevesi veya Unity motorudur, ancak bunların hiçbiri ilgimi çeken tüm hedeflere ulaşamaz). Yaklaşımım aslında bir el limanıydı. iOS/Android sürümünü önce C++'ta yazdım, ardından bu kodu her yeni platforma taşıdım. Bu çok fazla çalışma gibi görünse de, WP7 ve Chrome sürümlerinin her birinin tamamlanması 2 haftadan fazla sürmedi. Bu durumda, kod tabanını kolayca taşınabilir hale getirmek için herhangi bir şey yapılabilir mi? Bu konuda yardımcı olduğum birkaç şey oldu:

Kod Tabanını Küçük Tutun

Bu açık gibi görünse de oyunu bu kadar hızlı aktarabilmemin asıl nedeni buydu. Bouncy Mouse'un istemci kodu yaklaşık 7.000 satır C++'dan oluşur. 7.000 satırlık kod hiçbir şey değildir, ancak yönetilebilecek kadar küçüktür. İstemci kodunun hem C# hem de Javascript sürümleri aşağı yukarı aynı boyutta olmuştur. Kod tabanımı küçük tutmak temelde iki temel uygulamaya denk gelir: Fazla kod yazmayın ve ön işleme (çalışma zamanı olmayan) kodda mümkün olduğunca çok işlem yapın. Fazladan kod yazmamak bariz gibi görünebilir, ancak kendi kendimle her zaman mücadele ettiğim bir konu. Yardımcı olarak kabul edilebilecek herhangi bir şey için sık sık bir yardımcı sınıf/işlev yazma isteği duyuyorum. Bununla birlikte, bir yardımcıyı birden çok kez kullanmayı planlamıyorsanız, genellikle kodunuzu şişirir. Bouncy Mouse kullanırken en az üç kez kullanmayacaksam yardımcı yazmamaya dikkat ediyordum. Yardımcı sınıf hazırlamaya başladığımda, bu eğitimin gelecekteki projelerim için temiz, taşınabilir ve yeniden kullanılabilir olmasını sağlamaya çalıştım. Diğer yandan, yalnızca Bouncy Mouse için kullandığım, tekrar kullanılma ihtimali düşük olan bir kod yazarken, kodu yazmanın "en güzel" yolu bu olmasa bile kodlama görevini mümkün olduğunca kolay ve hızlı bir şekilde tamamlamaya odaklandım. Kod tabanını küçük tutmanın ikinci ve daha önemli parçası, ön işleme adımlarına mümkün olduğunca fazla itmekti. Bir çalışma zamanı görevi alıp bunu bir ön işleme görevine taşıyabilirseniz hem oyununuz daha hızlı çalışır hem de kodu her yeni platforma taşımanıza gerek kalmaz. Bir örnek vermek gerekirse, başlangıçta, düzey geometri verilerimi epey işlenmemiş bir biçimde depoladım ve çalışma sırasında gerçek OpenGL/WebGL köşe arabelleklerini birleştirdim. Bu işlem biraz kurulum gerektirdi ve birkaç yüz satır çalışma zamanı kodu aldı. Daha sonra, derleme zamanında tam olarak doldurulmuş OpenGL/WebGL köşe arabelleklerini yazarak bu kodu bir ön işleme adımına taşıdım. Gerçek kod miktarı yaklaşık olarak aynıydı ancak bu birkaç yüz satır ön işleme adımına taşınmıştı. Yani bu kodları hiçbir zaman yeni platformlara taşımak zorunda kalmamıştım. Bouncy Mouse'ta bunun tonlarca örneği var ve nelerin mümkün olduğu oyundan oyuna değişiklik gösteriyor. Ama çalışma zamanında olması gerekmeyen her şeye dikkat edin.

İhtiyacınız Olmayan Bağımlılıkları Almayın

Bouncy Mouse'un kolay taşınmasının bir başka nedeni de neredeyse hiç bağımlılığı olmaması. Aşağıdaki grafikte, Bouncy Mouse'un platforma göre başlıca kitaplık bağımlılıkları özetlenmiştir:

Android iOS HTML5 WP7
Grafik OpenGL ES OpenGL ES WebGL XNA
Ses OpenSL ES OpenAL Web Sesi XNA
Fizik Kutu2D Kutu2D Box2D.js Box2D.xna

Bu oldukça yeterli. Tüm platformlarda taşınabilir Box2D dışında büyük 3. taraf kitaplıklar kullanılmadı. Grafikler için, hem WebGL hem de XNA, OpenGL ile neredeyse 1:1 eşler, dolayısıyla bu büyük bir sorun değildi. Asıl kütüphaneler yalnızca ses alanında farklıydı. Ancak Bouncy Mouse'taki ses kodu küçüktür (platforma özgü yaklaşık yüz satırlık kod) bu çok büyük bir sorun değildi. Bouncy Mouse'un taşınamayan büyük kitaplıklar içermemesi, çalışma zamanı kodunun mantığının, dil değişikliğine rağmen sürümler arasında neredeyse aynı olabileceği anlamına gelir. Ayrıca, bizi taşınabilir olmayan bir alet zincirine hapsetmekten de kurtarıyor. OpenGL/WebGL'ye göre kodlamanın, Cocos2D veya Unity gibi bir kitaplık kullanmaya kıyasla doğrudan karmaşıklık artışına neden olup olmadığı soruldu (bazı WebGL yardımcıları da vardır). Hatta ben bunun tam tersini inanıyorum. Çoğu cep Telefonu / HTML5 oyunu (En azından Bouncy Mouse gibi oyunlar) çok basittir. Çoğu durumda, oyun sadece birkaç imge ve belki de biraz dokulu geometri çizer. Bouncy Mouse'ta OpenGL'ye özgü kodların toplamı muhtemelen 1000 satırdan azdır. Yardımcı kitaplık kullanmak bu sayıyı azaltsa bizi şaşırtırdım. Bu sayı yarıya inmiş olsa bile 500 satır koddan tasarruf etmek için yeni kitaplıklar/araçlar öğrenmeye epey vakit harcamam gerekecekti. Üstelik, ilgimi çeken tüm platformlara aktarılabilecek bir yardımcı kitaplık henüz bulamadım. Bu yüzden böyle bir bağımlılığa sahip olmak taşınabilirliğe büyük ölçüde zarar verecek. Işık haritaları, dinamik LOD, dış görünümlü animasyon vb. gerektiren bir 3D oyun yazıyor olsaydım yanıtım kesinlikle değişecekti. Bu durumda, motorumun tamamını OpenGL'ye göre elle kodlamaya çalışmak için tekerleği yeniden icat ederdim. Burada anlatmak istediğim, çoğu Mobil/HTML5 oyununun (henüz) bu kategoride olmadığıdır. Bu nedenle, çok zorlanmadan işleri karmaşık hale getirmeye gerek yoktur.

Diller arasındaki benzerlikleri hafife almayın.

C++ kod tabanımı yeni bir dile taşırken çok zaman kazandıran son bir ipucu, her dil için kodun büyük bölümünün neredeyse aynı olduğunun farkına varılmasıydı. Bazı temel unsurlar değişebilir ancak bunlar değişmeyen şeylerden çok daha azdır. Aslında, birçok işlev için C++'tan Javascript'e geçmek, C++ kod tabanımda birkaç normal ifade değişikliğinin çalıştırılmasını gerektiriyordu.

Taşıma Sonuçları

Taşıma süreciyle ilgili oldukça söyleyeceklerimiz bu kadar. Sonraki birkaç bölümde HTML5'e özgü birkaç zorluğa değineceğim, ancak ana mesaj şudur: Kodunuzu basit tutarsanız taşıma işlemi bir kabus değil, yalnızca küçük bir baş ağrısı olur.

Ses

Bana (ve görünüşe göre herkeste) bazı sorunlara neden olan alanlardan biri sesle ilgiliydi. iOS ve Android'de bir dizi etkili ses seçeneği mevcut (OpenSL, OpenAL), ancak HTML5 dünyasında işler daha da kötü görünüyordu. HTML5 Audio kullanılabilir, ancak oyunlarda kullanıldığında anlaşmayı engelleyen bazı sorunlar olduğunu fark ettim. En yeni tarayıcılarda bile sık sık tuhaf davranışlarla karşılaşıyorum. Örneğin, Chrome'da oluşturabileceğiniz eşzamanlı Ses öğelerinin (kaynak) sayısı konusunda bir sınır olduğu görülüyor. Ayrıca, ses çalsa bile bazen ses açıklanamayan bir şekilde bozuluyordu. Genel olarak biraz endişeliydim. İnternette arama yapmak, hemen hemen herkesin aynı sorunu yaşadığını ortaya çıkardı. İlk başta ulaştığım çözüm SoundManager2 adlı bir API'ydi. Bu API, mümkün olduğunda HTML5 Ses kullanır ve yanıltıcı durumlarda Flash'a geri döner. Bu çözüm işe yaradığı halde, hatalı ve öngörülemez bir çözümdü (sadece HTML5 Ses'ten çok daha az). Özelliği kullanıma sunduktan bir hafta sonra, Google'daki arkadaşlarımla görüştüm ve bana Webkit'in Web Audio API'sını yönelttiler. Başlangıçta bu API'yı kullanmayı düşünmüştüm, ancak API'nin sahip olduğu gereksiz (benim için) karmaşıklık düzeyi yüzünden bu API'den uzak durmuştum. Sadece birkaç ses çalmak istedim: HTML5 Ses ile bu birkaç satır JavaScript'e karşılık gelir. Bununla birlikte, Web Audio'ya kısa baktığımda devasa (70 sayfa) spesifikasyonu, web'deki az sayıda örnek olması (yeni bir API için tipik) ve teknik özelliğin herhangi bir yerinde "oynat", "duraklat" veya "durdur" işlevlerinin eksik olması beni çok etkiledi. Birkaç örneğe daha bakıp biraz daha araştırma yaptıktan sonra, Google’ın doğru olduğunu gördüm. İhtiyaçlarımı kesinlikle karşılayabiliyor ve bunu diğer API'ları rahatsız eden hatalar olmadan yapabiliyor. Özellikle Getting Started with Web Audio API (Web Audio API'sına Giriş) başlıklı makale yer alır. Bu makale, API'yi daha ayrıntılı şekilde incelemek için mükemmel bir kaynaktır. Asıl sorunum, API'yi anlayıp kullandıktan sonra bile bana "yalnızca birkaç ses çalacak" şekilde tasarlanmamış bir API gibi görünüyor. Bu yanlışı atlatmak için küçük bir yardımcı sınıf yazdım. Bu yardımcı sınıf sayesinde API'yi tam istediğim gibi kullanabilirim: ses çalmak, duraklatmak, durdurmak ve sesin durumunu sorgulamak. Bu yardımcı sınıfı AudioClip olarak adlandırdım. Kaynağa GitHub'da Apache 2.0 lisansı altında tam kaynaktan erişebilirsiniz. Aşağıda sınıfın ayrıntılarını ele alacağız. Ama önce Web Audio API'sı ile ilgili biraz bilgi verelim:

Web Ses Grafikleri

Web Audio API'sını HTML5 Ses öğesine göre daha karmaşık (ve daha güçlü) yapan ilk özellik, sesi kullanıcıya çıkarmadan önce işleyebilmesi / miksleyebilmesidir. Güçlü olsa da herhangi bir sesin oynatılmasının grafik içermesi basit senaryolarda işleri biraz daha karmaşık hale getirir. Web Audio API'nın gücünü göstermek için aşağıdaki grafiği göz önünde bulundurun:

Temel Web Ses Grafiği
Temel Web Ses Grafiği

Yukarıdaki örnek Web Audio API'sının gücünü gösterse de kendi senaryomda bu gücün çoğuna ihtiyacım yoktu. Sadece bir ses çalmak istedim. Bu durum için de bir grafik gerekli olsa da, grafik son derece basittir.

Grafikler Basit Olabilir

Web Audio API'sını HTML5 Ses öğesine göre daha karmaşık (ve daha güçlü) yapan ilk özellik, sesi kullanıcıya çıkarmadan önce işleyebilmesi / miksleyebilmesidir. Güçlü olsa da herhangi bir sesin oynatılmasının grafik içermesi basit senaryolarda işleri biraz daha karmaşık hale getirir. Web Audio API'nın gücünü göstermek için aşağıdaki grafiği göz önünde bulundurun:

Trivial Web Audio Graph (Önemsiz Web Ses Grafiği)
Önemsiz Web Ses Grafiği

Yukarıda gösterilen önemsiz grafik bir sesi çalmak, duraklatmak veya durdurmak için gereken her şeyi yapabilir.

Ancak Grafik Hakkında Endişelenmeyelim

Grafiği anlamak güzel olsa da her ses çaldığımda bu sorunla uğraşmak istemiyorum. Bu nedenle, basit bir sarmalayıcı sınıfı olan "AudioClip" yazdım. Bu sınıf, bu grafiği dahili olarak yönetiyor ancak kullanıcılara yönelik çok daha basit bir API sunuyor.

AudioClip
AudioClip

Bu ders, yalnızca bir Web Audio grafiği ve yardımcı durumundan ibaret değildir. Her sesi çalmak için Web Audio grafiği oluşturmaktan çok daha basit bir kod kullanmama olanak tanır.

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

// Later
sound.play();

Uygulama Ayrıntıları

Yardımcı sınıfın koduna hızlıca göz atalım: Oluşturucu: Oluşturucu, ses verilerinin yüklenmesini XHR kullanarak yapar. Burada gösterilmese de (örneği basit tutmak için), bir HTML5 Ses öğesi ayrıca kaynak düğüm olarak da kullanılabilir. Bu, özellikle büyük boyutlu örnekler için faydalıdır. Web Audio API'sının bu verileri bir "dizi arabelleği" olarak getirmemizi gerektirdiğini unutmayın. Veriler alındıktan sonra bu verilerden bir Web Audio arabelleği oluştururuz (orijinal biçiminden kodu bir çalışma zamanı PCM biçimine çözeriz).

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

Oynat – Sesimizi çalmak iki adımdan oluşur: oynatma grafiğini ayarlamak ve grafiğin kaynağında bir “noteOn” sürümünü çağırmak. Bir kaynak yalnızca bir kez oynatılabilir. Bu nedenle, her oynattığımızda kaynağı/grafiği yeniden oluşturmamız gerekir. Bu işlevin karmaşıklığı, duraklatılmış bir klibin (this.pauseTime_ > 0) devam ettirilmesi için gereken koşullardan kaynaklanır. Duraklatılmış bir klibin oynatılmasını devam ettirmek için bir arabelleğin alt bölgesinin oynatılmasına olanak tanıyan noteGrainOn kodunu kullanırız. Maalesef noteGrainOn bu senaryoda döngüyle istenen şekilde etkileşime girmez (arabelleğin tamamını değil, alt bölgeyi döngüye alır). Bu nedenle, klibin kalan kısmını noteGrainOn ile oynatıp döngü etkin bir şekilde klibi en baştan başlatarak bu sorunu gidermemiz gerekir.

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

Ses Efekti olarak Oynat - Yukarıdaki oynatma işlevi, ses klibinin üst üste binerek birden çok kez oynatılmasına izin vermez (ikinci bir oynatma yalnızca klip bittiğinde veya durdurulduğunda mümkündür). Bazen bir oyunda her oynatmanın tamamlanmasını beklemeden birçok kez ses çalınması gerekir (oyunda jeton toplama vb.). Bunu etkinleştirmek için AudioClip sınıfının bir playAsSFX() yöntemi vardır. Aynı anda birden fazla oynatma yapılabildiğinden, playAsSFX() hizmetinden alınan oynatma AudioClip ile 1:1 arasında bir sınır değildir. Bu nedenle, oynatma durdurulamaz, duraklatılamaz veya durum için sorgulanamaz. Bu şekilde çalınan döngüsel bir sesi durdurmanın hiçbir yolu olmayacağından döngü özelliği de devre dışı bırakılır.

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

Durdurma, duraklatma ve sorgulama durumu: İşlevlerin geri kalanı oldukça basittir ve fazla açıklama gerektirmez:

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

Ses Sonucu

Umarım bu yardımcı dersin, benimle aynı Ses sorunlarıyla mücadele eden geliştiriciler için faydalı olur. Ayrıca, Web Audio API'sının daha güçlü özelliklerinden bazılarını eklemeniz gerekse bile bunun gibi bir sınıf, başlamak için makul bir yer gibi görünebilir. Her iki şekilde de bu çözüm Bouncy Mouse'un ihtiyaçlarını karşıladı ve oyunun hiçbir koşul içermeyen gerçek bir HTML5 oyunu olmasını sağladı.

Performans

JavaScript bağlantı noktası konusunda beni endişelendiren bir diğer alan da performanstı. Bağlantı noktamın v1 sürümünü tamamladıktan sonra dört çekirdekli masaüstümde her şeyin sorunsuz çalıştığını gördüm. Ne yazık ki, netbook veya Chromebook'ta işler normalin biraz altındaydı. Bu durumda, Chrome'un profil aracı programımın tüm zamanlarının tam olarak nerede harcandığını göstererek beni kurtardı. Deneyimim, optimizasyon yapmadan önce profil çıkarmanın önemini vurguluyor. Box2D fiziğinin veya belki de oluşturma kodunun yavaşlamanın ana nedenlerinden biri olmasını bekliyordum. Ancak, zamanımın büyük bir kısmı aslında Matrix.clone() işlevime harcanıyordu. Oyunumun matematiği yoğun olduğu için çok sayıda matris oluşturma/klonlama işlemi gerçekleştirdiğimi biliyordum ama bunun bir performans sorunu olacağını hiçbir zaman beklemiyordum. Sonunda, çok basit bir değişiklik oyunun CPU kullanımını 3 kattan fazla azaltmasını, masaüstü bilgisayarımdaki CPU'nun% 6-7'den %2'ye düşmesini sağladı. Bu, Javascript geliştiricilerinin bildiği ortak bir nokta olabilir, ancak bir C++ geliştiricisi olarak bu sorun beni şaşırttı, bu yüzden biraz daha ayrıntıya gireceğim. Temelde, orijinal matris sınıfım 3x3 boyutunda bir matristi: 3 öğeli bir dizi ve her öğe 3 öğe dizisi içeriyordu. Ne yazık ki bu durum, matrisi klonlama zamanı geldiğinde 4 yeni dizi oluşturmamız gerekti. Yapmam gereken tek değişiklik, bu verileri 9 öğeli tek bir diziye taşımak ve matematiğimi buna göre güncellemekti. Gördüğüm bu 3 kat CPU azalmasından tamamıyla bu değişiklik sorumluydu ve bu değişiklikten sonra performansımın tüm test cihazlarımda kabul edilebilir olduğunu gördüm.

Daha Fazla Optimizasyon

Performansım kabul edilebilir düzeyde olsa da hâlâ birkaç küçük aksaklıkla karşılaşıyorum. Biraz daha profil çıkarmadan sonra, bunun JavaScript'in Çöp Toplama özelliğinden kaynaklandığını fark ettim. Uygulamam 60 fps'de çalışıyordu, yani her kare için yalnızca 16 ms. Maalesef çöp toplama işlemi daha yavaş bir makinede devreye girdiğinde bazen yaklaşık 10 ms veri yiyordu. Oyunun tam kareyi çizmesi neredeyse 16 ms'yi gerektirdiğinden bu durum birkaç saniyede bir takılmaya neden oldu. Neden bu kadar çok çöp ürettiğimi daha iyi anlamak için Chrome'un yığın profili aracını kullandım. Umutsuzluğa rağmen, çöplerin büyük bir çoğunluğunun (%70'in üzerinde) Box2D tarafından üretildiği ortaya çıktı. JavaScript'te çöpleri ortadan kaldırmak zorlu bir iştir ve Box2D'yi yeniden yazmak söz konusu değildi. Bu yüzden kendimi bir köşeye çektiğimi fark ettim. Neyse ki kitaptaki en eski püf noktalarından birini biliyorsunuz: 60 fps'ye basamadığınızda 30 fps'de koşun. Tutarlı bir 30 fps ile koşmanın, 60 fps'de titreyen bir hızda koşmaktan çok daha iyi olduğu, üzerinde yaygın bir şekilde kabul edilmiştir. Aslında oyunun 30 fps'de çalıştığına dair hâlâ tek bir şikayet veya yorum almadım (iki sürümü yan yana karşılaştırmazsanız bunu anlamamız gerçekten zordur). Kare başına bu ekstra 16 ms., kötü bir çöp toplama söz konusu olduğunda bile kareyi oluşturmak için hâlâ bolca zamanım olduğu anlamına geliyordu. 30 fps'de çalıştırmak, kullanmakta olduğum zamanlama API'si (WebKit'in mükemmel requestAnimationFrame özelliği) tarafından açıkça etkinleştirilmese de, bunu çok önemsiz bir şekilde gerçekleştirilebilir. Açık bir API kadar zarif olmasa da, RequestAnimationFrame’in aralığının monitörün VSYNC'si (genellikle 60 fps) ile hizalanması bilerek 30 fps gerçekleştirilebilir. Bu, diğer tüm geri aramaları göz ardı etmemiz gerektiği anlamına gelir. Temel olarak, "RequestAnimationFrame" her tetiklendiğinde çağrılan bir "Tick" geri çağırmanız varsa, bu şu şekilde yapılabilir:

var skip = false;

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

// OTHER CODE
}

Daha tedbirli olmak isterseniz, bilgisayarın VSYNC'sinin başlatma sırasında zaten 30 fps veya altında olmadığını kontrol etmeniz ve bu durumda atlama özelliğini devre dışı bırakmanız gerekir. Ancak bunu test ettiğim hiçbir masaüstü/dizüstü bilgisayar yapılandırmasında henüz görmedim.

Dağıtım ve Para Kazanma

Bouncy Mouse'un Chrome'daki özelliği konusunda beni şaşırtan son alan da para kazanmaydı. Bu projeye girerken, HTML5 oyunlarını gelecek teknolojileri öğrenmek için ilginç bir deney olarak düşündüm. Ancak bu platformun çok geniş bir kitleye ulaşacağını ve önemli bir para kazanma potansiyeline sahip olduğunu fark etmemiştim.

Bouncy Mouse, Ekim ayının sonunda Chrome Web Mağazası'nda piyasaya sürüldü. Chrome Web Mağazası'nda uygulama yayınlayarak bulunabilirlik, topluluk etkileşimi, sıralamalar ve mobil platformlarda alıştığım diğer özellikler için mevcut bir sistemden yararlanmayı başardım. Mağazanın erişiminin bu kadar geniş olması beni şaşırttı. Uygulamayı kullanıma sunduktan sonraki bir ay içinde yaklaşık dört yüz bin yüklemeye ulaştım ve şimdiden topluluk etkileşiminden (hata raporu, geri bildirim) yararlanmaya başladım. Beni şaşırtan şeylerden biri de web uygulamalarının para kazanma potansiyeliydi.

Bouncy Mouse'ta basit bir para kazanma yöntemi var: oyun içeriğinin yanında bir banner reklam. Ancak, oyunun geniş erişim alanı göz önünde bulundurulduğunda bu banner reklamın önemli bir gelir elde ettiğini gördüm ve uygulamanın en başarılı olduğu dönemde en başarılı platformum olan Android'e eşit düzeyde bir gelir elde ettim. Buna katkıda bulunan faktörlerden biri, HTML5 sürümünde gösterilen daha büyük AdSense reklamlarının, Android'de gösterilen daha küçük AdMob reklamlarına göre gösterim başına önemli ölçüde daha yüksek gelir sağlamasıdır. Bunun yanı sıra HTML5 sürümündeki banner reklam Android sürümüne kıyasla çok daha az rahatsız edici olduğu için daha temiz bir oyun deneyimi sunuyor. Genel olarak, bu sonuç beni çok memnun etti.

Zaman İçinde Normalleştirilmiş Kazançlar.
Zaman İçinde Normalleştirilmiş Kazançlar

Oyundan elde edilen kazanç beklenenden çok daha iyi olsa da, Chrome Web Mağazası'nın erişiminin hâlâ Android Market gibi olgun platformların erişimine göre daha az olduğunu belirtmekte fayda var. Bouncy Mouse, hızlı bir şekilde Chrome Web Mağazası'ndaki en popüler 9 numaralı oyuna kadar hızlı bir şekilde çekim yapabilse de, siteye gelen yeni kullanıcıların oranı ilk sürümden bu yana önemli ölçüde azaldı. Bununla birlikte, oyun istikrarlı bir şekilde büyümeye devam ediyor ve platformun gelişmesini görmek için sabırsızlanıyorum.

Sonuç

Bouncy Mouse'u Chrome'a taşıma işlemi beklediğimden çok daha kolay geçti. Bazı küçük ses ve performans sorunları dışında, Chrome'un mevcut bir akıllı telefon oyunu için mükemmel kapasiteye sahip bir platform olduğunu keşfettim. Bu deneyimden çekinen tüm geliştiricilerin bu deneyimi denemelerini öneririm. Hem taşıma sürecinden hem de HTML5 oyunlarıyla bağlantı kurmamı sağlayan yeni oyun kitlesinden çok memnunum. Sorunuz olursa bana e-posta gönderebilirsiniz. Ya da aşağıya yorumunuzu yazın. Düzenli olarak kontrol etmeye çalışacağım.