Studium przypadku – Bouncy Mouse

Wstęp

Wesoła mysz

Po opublikowaniu pod koniec zeszłego roku gry Bouncy Mouse na iOS i Androida udało mi się nauczyć mnie kilku bardzo ważnych rzeczy. Kluczem było między innymi to, że trudno jest wkroczyć na rynek o ugruntowanej pozycji. Na bardzo nasyconym rynku iPhone'ów zdobycie nowych klientów było bardzo trudne. Na mniej nasyconym rynku Android Market postęp był łatwiejszy, lecz niełatwy. W związku z tym dostrzegłem w Chrome Web Store interesującą możliwość. Choć Web Store nie jest w żaden sposób pusty, jego katalog wysokiej jakości gier w formacie HTML5 dopiero się rozrasta. Dla nowego dewelopera aplikacji tworzenie rankingów i poprawianie widoczności jest znacznie łatwiejsze. Postanowiłem przenieść mysz Bouncy Mouse do HTML5, mając nadzieję, że uda mi się dotrzeć do nowej, ekscytującej gry, w której będę mogła zagrać. W tym studium przypadku omówię ogólny proces przenoszenia myszy Bouncy Mouse do HTML5, a potem zajmę się 3 obszarami, które okazały się interesujące: audio, wydajności i monetyzacji.

Przenoszenie gry w C++ do kodu HTML5

Funkcja Bouncy Mouse jest obecnie dostępna na urządzeniach z Androidem(C++), iOS (C++), Windows Phone 7 (C#) i Chrome (JavaScript). Czasami pojawia się pytanie: jak stworzyć grę, którą można łatwo przenieść na wiele platform? Mam wrażenie, że ludzie mają nadzieję na magiczną konstrukcję, która pozwoli im osiągnąć taki poziom mobilności bez konieczności korzystania z ręcznych portów. Niestety nie wiem, czy istnieje takie rozwiązanie (najbliższym rozwiązaniem jest prawdopodobnie platforma Google PlayN lub silnik Unity, ale żadne z tych rozwiązań nie spełnia wszystkich celów, które mnie interesują). Moje podejście było tak naprawdę złożeniem portu w dłonie. Najpierw napisałem wersję na iOS/Androida w C++, a potem przeniósł go na każdą nową platformę. Chociaż może się to wydawać dużo pracy, opracowanie wersji WP7 i Chrome nie zajęło nam więcej niż 2 tygodni. Teraz pojawia się pytanie: czy można coś zrobić, aby bazę kodu można było łatwo przenosić ręcznie? Pomogło mi kilka rzeczy, które mi w tym pomogły:

Baza kodu powinna być niewielka

Choć może się to wydawać oczywiste, to właśnie z tego powodu udało mi się tak szybko przenieść grę. Kod klienta aplikacji Bouncy Mouse zawiera tylko około 7000 wierszy C++. 7 tys. wierszy to nic, ale jest wystarczająco mały, aby można było nim zarządzać. Oba wersje kodu klienta w językach C# i JavaScript mają mniej więcej taki sam rozmiar. Utrzymywanie niewielkiej bazy kodu sprowadza się do 2 kluczowych sposobów: nie pisz nadmiarowego kodu i rób jak najwięcej na kodzie w ramach wstępnego przetwarzania (nie w czasie działania). Nie pisanie zbędnego kodu może wydawać się oczywiste, ale to jedna z rzeczy, które zawsze ze sobą walczę. Często mam potrzebę utworzenia klasy/funkcji pomocniczej do wszystkiego, co może być uwzględniane w usłudze pomocniczej. Jeśli jednak nie planujesz wiele razy korzystać z pomocy Asystenta, zwykle skutkuje to nadmiernym zniekształceniem kodu. Jeśli chodzi o gry Bouncy Mouse, uważam, że nigdy nie napiszę pomocnika, chyba że używam go co najmniej trzy razy. Po pisaniu lekcji pomocniczych starałam się, by były one przejrzyste, przenośne i łatwe do wielokrotnego użytku w przyszłych projektach. Z drugiej strony podczas pisania kodu przeznaczonego tylko dla Bouncy Mouse (o niskim prawdopodobieństwie jego ponownego wykorzystania) skupiłem się na jak najprostszym i szybkim pisaniu kodu, nawet jeśli nie był to „najładniejszy” sposób pisania kodu. Drugim i ważniejszym aspektem utrzymania małej bazy kodu było przekazanie jak największej liczby elementów do etapów wstępnego przetwarzania danych. Jeśli możesz wykonać zadanie w środowisku wykonawczym i przenieść je do zadania przetwarzania wstępnego, gra nie tylko będzie działać szybciej, ale również nie będzie trzeba przenosić kodu na każdą nową platformę. Dla przykładu dane geometryczne poziomu były przechowywane najpierw w nieprzetworzonym formacie, a rzeczywiste bufory wierzchołków OpenGL/WebGL były tworzone w czasie działania. Wymagało to czasu na konfigurację i kilkuset wierszy kodu w czasie działania. Później przeniosłam ten kod do etapu wstępnego przetwarzania, zapisując w pełni rozpakowane bufory wierzchołków OpenGL/WebGL w czasie kompilacji. Rzeczywista ilość kodu była taka sama, ale te kilkaset wierszy zostało przeniesionych do etapu wstępnego przetwarzania, co oznacza, że nie trzeba było przenosić ich na żadną nową platformę. Jest wiele przykładów tego typu w grze Bouncy Mouse, a możliwości mogą różnić się w zależności od gry, ale uważaj na wszystkie, które nie muszą wydarzyć się w czasie działania.

Nie bierz zależności, których nie potrzebujesz

Innym powodem, dla którego Bouncy Mouse jest łatwa do przeniesienia, jest to, że niemal nie ma żadnych zależności. Ten wykres zawiera podsumowanie głównych zależności bibliotek Bouncy Mouse z poszczególnymi platformami:

Android iOS HTML5 WP7
Grafika OpenGL ES OpenGL ES WebGL XNA
Dźwięk OpenSL ES OpenAL Audio internetowe XNA
Fizyka Pole 2D Pole 2D Box2D.js Box2D.xna

To wszystko. Nie używano żadnych dużych bibliotek zewnętrznych poza biblioteką Box2D, która jest dostępna na wszystkich platformach. W przypadku grafiki zarówno WebGL, jak i XNA mapują się niemal 1:1 w trybie OpenGL, więc nie było to wielkim problemem. Rzeczywiste biblioteki się różniły tylko w dziedzinie dźwięku. Jednak kod dźwięku w Bouncy Mouse jest mały (około 100 wierszy kodu przeznaczonego na platformę), więc nie jest to wielkim problemem. Dzięki temu, że w aplikacji Bouncy Mouse nie ma dużych, nieprzenośnych bibliotek, logika kodu środowiska wykonawczego może być prawie taka sama w obu wersjach (mimo zmiany języka). Pozwala to też uniknąć uwięzienia w nieprzenośnym łańcuchu narzędzi. Spytano mnie, czy kodowanie w środowisku OpenGL/WebGL bezpośrednio zwiększa złożoność w porównaniu z użyciem takiej biblioteki jak Cocos2D czy Unity (istnieją też pomocnicy WebGL). Uważam, że jest wręcz przeciwnie. Większość gier w formacie HTML5 lub na telefony komórkowe (przynajmniej takich jak Bouncy Mouse) jest bardzo prosta. W większości przypadków gra rysuje po prostu kilka sprite’ów i teksturowaną geometrię. Łączna liczba kodu specyficznego dla OpenGL w przypadku myszy Bouncy Mouse jest prawdopodobnie mniejsza niż 1000 wierszy. Byłbym zaskoczony, jeśli użycie biblioteki pomocniczej w rzeczywistości spowoduje zmniejszenie tej liczby. Nawet gdyby zmniejszyła się o połowę, musiałbym poświęcić sporo czasu na poznawanie nowych bibliotek i narzędzi, żeby zaoszczędzić 500 wierszy kodu. Poza tym nie udało mi się jeszcze znaleźć biblioteki pomocniczej, którą można by przenieść na wszystkie platformy, które mnie interesują, dlatego taka zależność znacznie pogorszyłaby jej przenoszenie. Gdybym pisał do gry 3D, która wymagała mapy świetlnej, dynamicznego LOD, animacji ze skórkami itd., moja odpowiedź z pewnością się zmieniła. W tym przypadku musiałbym wymyślić koło i spróbować ręcznie zakodować cały silnik pod kątem OpenGL. Chodzi mi o to, że większość gier mobilnych lub w formacie HTML5 nie zaliczono (jeszcze) do tej kategorii, więc nie trzeba komplikować pewnych kwestii, zanim to konieczne.

Nie lekceważ podobieństw między językami

Ostatnia sztuczka, która pozwoliła zaoszczędzić sporo czasu przy przenoszeniu bazy kodu C++ do nowego języka, polegała na przekonaniu, że większość kodu jest prawie identyczna w każdym języku. Chociaż niektóre kluczowe elementy mogą się zmienić, to znacznie mniej niż te, które się nie zmieniają. W przypadku wielu funkcji przejście z C++ do JavaScriptu wymagało po prostu uruchomienia kilku zamienników wyrażeń regularnych w bazie kodu C++.

Wnioski dotyczące przenoszenia

To tyle, jeśli chodzi o proces przenoszenia. W kilku kolejnych sekcjach opowiem o kilku wyzwaniach związanych z HTML5, ale głównym przekazem jest to, że jeśli będziesz dbać o prostą strukturę kodu, przeniesienie nie będzie zbyt kiepskie, a nie pomoże.

Dźwięk

Jednym z największych problemów dla mnie (i prawdopodobnie wszystkich innych) był dźwięk. Na iOS i Androidzie można wybrać różne typy dźwięku (OpenSL, OpenAL), ale w HTML5 wszystko wyglądało gorzej. Choć dźwięk HTML5 jest dostępny, widzę, że pojawiają się w nim problemy, które mogą sprawiać problemy w grach. Nawet w najnowszych przeglądarkach często występowały dziwne działania. Na przykład w Chrome obowiązuje ograniczenie liczby jednoczesnych elementów audio (źródła), które można utworzyć. Poza tym nawet podczas odtwarzania dźwięku czasem dźwięk może zostać niezauważalnie zniekształcony. Ogólnie trochę się martwiłam. Wyszukiwanie w internecie wykazało, że praktycznie wszyscy mają ten sam problem. Początkowo trafiłem na interfejs API o nazwie SoundManager2. Ten interfejs API wykorzystuje HTML5 Audio, gdy jest dostępny, i w trudnych sytuacjach korzysta z technologii Flash. To rozwiązanie działało, ale było problematyczne i nieprzewidywalne (mniej niż w przypadku czystego dźwięku HTML5). Tydzień po wdrożeniu aplikacji spotkałem się z pomocnymi pracownikami Google, którzy skierują mnie do interfejsu Web Audio API w Webkit. Początkowo chciałem(-am) używać tego interfejsu API, ale zrezygnowałem z niego ze względu na zbyt dużą (dla mnie) złożoność. Zależało mi tylko na zagraniu kilku dźwięków. W przypadku dźwięku w HTML5 to wystarczy kilka wierszy kodu JavaScript. W krótkim spojrzeniu na usługę Web Audio zaskoczyła mnie ogromna (70-stronicowa) specyfikacja, niewielka liczba próbek dostępnych w internecie (typowa dla nowego interfejsu API) i brak funkcji odtwarzania, wstrzymania i zatrzymania w dowolnym miejscu w specyfikacji. Z oczekiwaniami Google, że moje obawy nie zostały dobrze uzasadnione, przeanalizowałem ponownie interfejs API. Po zapoznaniu się z kilkoma przykładami i pogłębianiu badań doszłam do wniosku, że Google ma rację – interfejs API z pewnością odpowiada moim potrzebom i może to robić bez błędów, które morują pozostałe interfejsy API. Szczególnie przyda Ci się artykuł Wprowadzenie do interfejsu API Web Audio, w którym znajdziesz informacje na temat tego, jak lepiej korzystać z tego interfejsu. Mój rzeczywisty problem polega na tym, że nawet po zrozumieniu i korzystaniu z interfejsu API wydaje mi się, że jest to interfejs API, który nie został zaprojektowany tak, aby „tylko odtwarzać kilka dźwięków”. Aby rozwiązać ten problem, napisałam małą klasę pomocniczą, która pozwala mi używać interfejsu API w oczekiwany sposób – do odtwarzania, wstrzymywania, zatrzymywania i wykonywania zapytań o stan dźwięku. Nazwałam tę klasę pomocniczą AudioClip. Pełne źródło jest dostępne na GitHubie w ramach licencji Apache 2.0. Szczegóły zajęć omówimy poniżej. Najpierw kilka informacji o interfejsie Web Audio API:

Wykresy Web Audio

Pierwszą cechą, która sprawia, że interfejs Web Audio API jest bardziej złożony (i potężniejszy) od elementu audio HTML5, jest możliwość przetwarzania / miksowania dźwięku przed przekazaniem go użytkownikowi. Choć przy każdym odtwarzaniu dźwiękowym towarzyszą wykresy, w prostych scenariuszach sprawa staje się nieco bardziej złożona. Aby pokazać możliwości interfejsu Web Audio API, spójrz na ten wykres:

Podstawowy internetowy wykres audio
Podstawowy Web Audio Graph

Powyższy przykład pokazuje potęgę interfejsu Web Audio API, jednak w moim przypadku większość mocy nie była mi potrzebna. Chciałam tylko włączyć jakiś dźwięk. Chociaż wymaga to wykresu, jest on bardzo prosty.

Wykresy mogą być proste

Pierwszą cechą, która sprawia, że interfejs Web Audio API jest bardziej złożony (i potężniejszy) od elementu audio HTML5, jest możliwość przetwarzania / miksowania dźwięku przed przekazaniem go użytkownikowi. Choć przy każdym odtwarzaniu dźwiękowym towarzyszą wykresy, w prostych scenariuszach sprawa staje się nieco bardziej złożona. Aby pokazać możliwości interfejsu Web Audio API, spójrz na ten wykres:

Trivial Web Audio Graph
Trivial Web Audio Graph

Prosty wykres pokazany powyżej pozwala osiągnąć wszystko, co jest potrzebne do odtwarzania, wstrzymywania i zatrzymywania dźwięku.

Ale nie martwmy się nawet o wykres

Wykres jest fajny, ale nie chcę zajmować się tym za każdym razem, gdy gram dźwięk. W związku z tym napisałam prostą klasę opakowania „AudioClip”. Ta klasa zarządza tym wykresem wewnętrznie, ale ma znacznie prostszy interfejs API dla użytkowników.

AudioClip
AudioClip

Ta klasa to nic więcej niż wykres Web Audio i stan pomocniczy, ale pozwalają mi użyć znacznie prostszego kodu niż gdyby musiałem stworzyć wykres Web Audio do odtwarzania każdego dźwięku.

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

// Later
sound.play();

Szczegóły implementacji

Przyjrzyjmy się pokrótce kodowi klasy pomocniczej: Konstruktor – konstruktor obsługuje wczytywanie danych dźwiękowych za pomocą XHR. Chociaż nie widać tu tego elementu (aby nie był to prosty przykład), element Audio HTML5 może być również używany jako węzeł źródłowy. Jest to szczególnie przydatne w przypadku dużych próbek. Pamiętaj, że interfejs Web Audio API wymaga pobierania tych danych w postaci „bufora tablicowego”. Po otrzymaniu danych tworzymy na ich podstawie bufor Web Audio (dekodowanie ich z oryginalnego formatu do formatu PCM środowiska wykonawczego).

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

Granie – odtwarzanie dźwięku obejmuje 2 etapy: ustawienie wykresu odtwarzania i wywołanie wersji „noteOn” w źródle wykresu. Źródło może być odtworzone tylko raz, więc za każdym razem, gdy gramy, musimy odtworzyć źródło/wykres. Większość złożoności tej funkcji wynika z wymagań koniecznych do wznowienia wstrzymanego klipu (this.pauseTime_ > 0). Aby wznowić odtwarzanie wstrzymanego klipu, używamy funkcji noteGrainOn, która umożliwia odtwarzanie podregionu bufora. Niestety noteGrainOn nie oddziałuje w pętlę w pożądany sposób w tym scenariuszu (zapętla podregion, a nie cały bufor). Musimy więc obejść ten problem, odtwarzając pozostałą część klipu za pomocą funkcji noteGrainOn, a następnie uruchamiając klip od początku z włączoną pętlą.

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

Odtwórz jako efekt dźwiękowy – powyższa funkcja odtwarzania nie pozwala na wielokrotne odtwarzanie klipu audio, który nakłada się na siebie (drugie odtwarzanie jest możliwe tylko po zakończeniu lub zatrzymaniu klipu). Czasami w grze chcesz odtworzyć dźwięk kilka razy bez czekania na zakończenie każdego odtwarzania (zbieranie monet itp.). Aby to umożliwić, klasa AudioClip ma metodę playAsSFX(). W tym samym czasie może być wiele odtworzeń, więc odtwarzanie z usługi playAsSFX() nie jest powiązane z klipem audio 1:1. Dlatego nie można zatrzymać ani wstrzymać odtwarzania. Nie można też sprawdzić jego stanu. Zapętlanie również jest wyłączone, ponieważ nie ma sposobu na zatrzymanie odtwarzanego w ten sposób dźwięku.

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

Zatrzymywanie, wstrzymywanie i wykonywanie zapytań – pozostałe funkcje są dość proste i nie wymagają zbyt wielu wyjaśnień:

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

Podsumowanie audio

Mam nadzieję, że ta klasa pomocnicza będzie przydatna dla programistów, którzy mają te same problemy z dźwiękiem co ja. Warto też zacząć od takiej klasy, nawet jeśli chcesz dodać bardziej zaawansowane funkcje interfejsu Web Audio API. Tak czy inaczej, to rozwiązanie spełniło wymagania Bouncy Mouse i sprawiło, że gra była prawdziwą grą HTML5 bez żadnych zobowiązań.

Wydajność

Kolejnym obszarem, które niepokoiło mnie w związku z portem JavaScript, była wydajność. Po zakończeniu wersji 1 portu okazało się, że na moim czterordzeniowym komputerze wszystko działa prawidłowo. Niestety na netbooku i Chromebooku wszystko było źle. W tym przypadku program profilujący w Chrome pozwolił mi zaoszczędzić, pokazując dokładnie, gdzie został spędzony cały czas moich programów. Moje doświadczenie podkreśla znaczenie profilowania przed przeprowadzeniem optymalizacji. Spodziewałem się, że działanie fizyki Box2D (lub kodu renderowania) będzie głównym źródłem spowolnienia pracy, ale większość czasu była w rzeczywistości poświęcana mojej funkcji Matrix.clone(). Biorąc pod uwagę rozbudowany matematyczny charakter mojej gry, wiedziałam, że potrafię tworzyć/klonować matryce, ale nigdy nie spodziewałam się, że będzie to wąskie gardło. Ostatecznie okazało się, że bardzo prosta zmiana pozwoliła grze ponad 3-krotnie zmniejszyć zużycie procesora – z 6–7% na komputerze do 2%. Być może jest to dość powszechna wiedza programistów JavaScriptu, ale jako programista C++ ten problem mnie zaskoczył, więc przejdę bardziej szczegółowo. Zasadniczo pierwotna klasa macierzy była macierzą 3 x 3: macierze 3 elementów, z których każdy zawiera tablicę 3 elementów. Oznaczało to, że przy sklonowaniu macierzy trzeba było utworzyć 4 nowe tablice. Jedyną zmianą, jaką musiałem(-am), było przeniesienie tych danych do pojedynczej tablicy 9-elementowej i odpowiednie aktualizowanie obliczeń matematycznych. Ta 1 zmiana w całości przyczyniła się do 3-krotnego zmniejszenia CPU i po tej zmianie wydajność była akceptowalna na wszystkich moich urządzeniach testowych.

Więcej optymalizacji

Choć moja skuteczność była zadowalająca, nadal widziałem(-am) kilka drobnych problemów. Po dokładniejszym profilowaniu zauważyłem, że jest to zasługa funkcji Kolekcjonowania śmieci w języku JavaScript. Moja aplikacja działała z szybkością 60 kl./s, co oznacza, że wyświetlenie każdej klatki trwało tylko 16 ms. Niestety po uruchomieniu odśmiecania na wolniejszej maszynie czasochłonność transmisji wynosi ok. 10 ms. Powoduje to zacinanie się co kilka sekund, ponieważ wyświetlenie pełnej klatki zajęło prawie całe 16 ms. Aby lepiej zrozumieć, dlaczego generuje takie odpady, użyłam narzędzia do profilowania sterty w Chrome. Bardzo mi się nie podobało, ale okazało się, że zdecydowana większość odpadów (ponad 70%) jest generowana przez Box2D. Eliminowanie śmieci z JavaScriptu to niełatwe zadanie, a przeredagowanie Box2D nie było proste, więc zdałam sobie sprawę, że znalazłam się w wątku. Na szczęście nadal miałam jedną z najstarszych sztuczek dostępnych w książce: gdy nie osiągasz 60 kl./s, biegaj z 30. Powszechnie wiadomo, że prędkość 30 klatek na sekundę jest znacznie lepsza niż przy tej szybkości 60 klatek na sekundę. Nadal nie mam skargi ani komentarza, że gra działa z szybkością 30 kl./s (trudno to stwierdzić, chyba że porównasz obie wersje). Te dodatkowe 16 ms na klatkę oznaczały, że nawet w przypadku brzydkiej kolekcji śmieci miałem dużo czasu na wyrenderowanie klatki. Mimo że działanie z szybkością 30 kl./s nie jest bezpośrednio włączone przez używany przeze mnie interfejs API czasu (doskonała requestAnimationFrame z WebKit), można to zrobić w bardzo prosty sposób. Choć może nie być tak eleganckie jak jawny interfejs API, 30 kl./s można uzyskać, wiedząc, że interwał metody RequestAnimationFrame jest wyrównany do wartości VSYNC monitora (zwykle 60 kl./s). Oznacza to, że musimy zignorować wszystkie pozostałe wywołania. Ogólnie rzecz biorąc, masz wywołanie zwrotne „Tick”, które jest wywoływane przy każdym uruchomieniu „RequestAnimationFrame”, można to zrobić w następujący sposób:

var skip = false;

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

// OTHER CODE
}

Jeśli chcesz zachować szczególną ostrożność, sprawdź, czy VSYNC na komputerze nie ma jeszcze wartości 30 kl./s w momencie uruchomienia i w tym przypadku wyłącz pomijanie. Nie dotyczy to jednak żadnej z testowanych przeze mnie konfiguracji na komputery/laptopy.

Dystrybucja i zarabianie

Ostatnią rzeczą, która zaskoczyła mnie ofertę portu w Chrome Bouncy Mouse, jest zarabianie. Przed rozpoczęciem tego projektu wyobraziłem sobie, że gry HTML5 mogą być ciekawym eksperymentem, którego celem jest poznanie nowych, obiecujących technologii. Nie wiedziałem(-am), że port dotrze do bardzo dużej grupy odbiorców i będzie mieć potencjał generowania przychodów.

Gra Bouncy Mouse została wydana pod koniec października w Chrome Web Store. Uruchamiając sklep Chrome Web Store, udało mi się wykorzystać istniejący system do zwiększania widoczności, angażowania społeczności, rankingów i innych funkcji, do których przyzwyczaiłem się na platformach mobilnych. Zaskoczyło mnie, jak duży był zasięg sklepu. W ciągu miesiąca od premiery udało mi się uzyskać blisko 400 tys. instalacji i już teraz korzystam z zaangażowania społeczności (zgłaszanie błędów, opinie). Kolejną rzeczą, która mnie zaskoczyła, była potencjał generowania przychodów z aplikacji internetowej.

Bouncy Mouse ma jedną prostą metodę zarabiania – baner reklamowy obok treści gry. Jednak biorąc pod uwagę szeroki zasięg gry, odkryłem, że ten baner reklamowy był przydatny, a w szczycie sezonu również wygenerowała dochody odpowiadające mojej najpopularniejszej platformie, czyli Androidowi. Między innymi większe reklamy AdSense wyświetlane w wersji HTML5 generują znacznie wyższe przychody za wyświetlenie niż mniejsze reklamy AdMob wyświetlane na urządzeniach z Androidem. Dodatkowo baner reklamowy w wersji HTML5 jest znacznie mniej uciążliwy niż w wersji na Androida, dzięki czemu gra jest bardziej przejrzysta. Ogólnie bardzo miło zaskoczyło mnie to rozwiązanie.

Znormalizowane zarobki w czasie.
Znormalizowane zarobki w czasie

Zarobki z gry były znacznie wyższe od oczekiwanych, warto jednak zauważyć, że zasięg Chrome Web Store jest nadal mniejszy niż w przypadku bardziej dojrzałych platform, takich jak Android Market. Choć firma Bouncy Mouse mogła szybko sfilmować tytuł dziewiątej gry w Chrome Web Store, liczba nowych użytkowników odwiedzających witrynę znacznie spadła od jej premiery. Mimo to gra nadal dynamicznie się rozwija, a ja z niecierpliwością zobaczę, jak rozwinie się nasza platforma.

Podsumowanie

Przeniesienie Bouncy Mouse do Chrome przebiegło znacznie płynniej, niż się spodziewałem. Poza drobnymi problemami z dźwiękiem i wydajnością uznałem, że Chrome w pełni nadaje się do gier na smartfony. Zachęcam wszystkich deweloperów, którzy nie chcą się doczekać tego rozwiązania, do spróbowania swoich sił. Jestem bardzo zadowolony zarówno z procesu przenoszenia, jak i z punktu widzenia nowych graczy, z którymi łączy mnie gra w formacie HTML5. Jeśli będziesz mieć jakieś pytania, napisz do mnie e-maila. Albo po prostu napisz komentarz poniżej. Postaram się sprawdzić to regularnie.