Fallstudie – Bouncy Mouse

Einleitung

Hüpfende Maus

Nachdem ich Ende letzten Jahres Bouncy Mouse für iOS und Android veröffentlicht habe, habe ich einige wichtige Lektionen gelernt. Ein wichtiger Faktor war, dass es schwierig war, in einen etablierten Markt zu expandieren. Auf dem gründlich gesättigten iPhone-Markt war es schwer, sich zu erobern; auf dem weniger gesättigten Android Marketplace war der Fortschritt zwar einfacher, aber immer noch nicht einfach. Aufgrund dieser Erfahrung habe ich eine interessante Gelegenheit im Chrome Web Store gesehen. Auch wenn der Web Store keineswegs leer ist, wird sein Katalog an hochwertigen HTML5-basierten Spielen erst langsam ausgereift. Für neue App-Entwickler bedeutet dies, dass es wesentlich einfacher ist, die Ranking-Diagramme zu verbessern und die Sichtbarkeit zu erhöhen. Vor diesem Hintergrund beschloss ich, Bouncy Mouse auf HTML5 zu übertragen, in der Hoffnung, dass ich mein neuestes Gameplay einer interessanten neuen Nutzerbasis zugänglich machen kann. In dieser Fallstudie werde ich ein wenig über den allgemeinen Prozess der Portierung von Bouncy Mouse zu HTML5 sprechen. Anschließend werde ich die drei Bereiche, die interessant waren, etwas näher betrachten: Audio, Leistung und Monetarisierung.

Portieren eines C++-Spiels in HTML5

Bouncy Mouse ist derzeit für Android(C++), iOS (C++), Windows Phone 7 (C#) und Chrome (JavaScript) verfügbar. Dadurch stellt sich manchmal die Frage: Wie schreibt man ein Spiel, das einfach auf mehrere Plattformen übertragen werden kann? Ich habe das Gefühl, dass sich die Menschen auf ein gewisses Maß an Portabilität hoffen, mit dem sie diese Portabilität erreichen können, ohne auf eine Handportierung zurückgreifen zu müssen. Leider bin ich mir nicht sicher, ob es eine solche Lösung noch gibt. Am nächsten kommt wahrscheinlich das PlayN-Framework von Google oder die Unity-Engine, aber keines davon trifft alle meine Ziele zu. Mein Ansatz war eigentlich eine Handportierung. Zuerst habe ich die iOS/Android-Version in C++ geschrieben und diesen Code dann auf jede neue Plattform übertragen. Das klingt vielleicht nach einer Menge Arbeit, doch die WP7- und die Chrome-Versionen haben jeweils nicht mehr als zwei Wochen in Anspruch genommen. Die Frage ist nun: Kann ich irgendetwas tun, um eine Codebasis leicht handhabbar zu machen? Ein paar Dinge, die ich getan habe, hat dabei geholfen:

Halten Sie die Codebasis klein

Das mag offensichtlich erscheinen, aber es ist wirklich der Hauptgrund dafür, dass ich das Spiel so schnell mitnehmen konnte. Der Clientcode von Bouncy Mouse umfasst nur etwa 7.000 Zeilen in C++. 7.000 Codezeilen sind nichts anderes, aber klein genug, um überschaubar zu sein. Sowohl die C#- als auch die JavaScript-Version des Clientcodes hatten letztendlich ungefähr die gleiche Größe. Meine kleine Codebasis zu klein war, ergaben sich im Grunde um zwei wichtige Praktiken: keinen überschüssigen Code schreiben und möglichst viel Code zur Vorverarbeitung (nicht zur Laufzeit) verwenden. Es mag offensichtlich erscheinen, keinen überschüssigen Code zu schreiben, aber ich streite mich immer mit mir selbst. Ich habe oft den Drang, eine Hilfsklasse/-funktion für alles zu schreiben, was in ein Hilfsprogramm einbezogen werden kann. Sofern Sie ein Hilfsprogramm nicht tatsächlich mehrmals verwenden möchten, wird in der Regel einfach Ihr Code aufgebläht. Bei Bouncy Mouse achte ich darauf, nie ein Hilfsprogramm zu schreiben, es sei denn, ich würde es mindestens dreimal verwenden. Als ich einen Hilfskurs geschrieben habe, habe ich versucht, ihn für meine zukünftigen Projekte sauber, übertragbar und wiederverwendbar zu machen. Wenn ich andererseits Code nur für Bouncy Mouse schreibe und die Wahrscheinlichkeit einer Wiederverwendung gering ist, konzentrierte ich mich darauf, die Programmierungsaufgabe so einfach und schnell wie möglich zu erledigen, auch wenn dies nicht die „schönste“ Art war, den Code zu schreiben. Der zweite und wichtigste Schritt bei der Kleinigkeit der Codebasis bestand darin, so viel wie möglich in Vorverarbeitungsschritte zu übertragen. Wenn Sie eine Laufzeitaufgabe in eine Vorverarbeitungsaufgabe verschieben können, wird Ihr Spiel nicht nur schneller ausgeführt, sondern Sie müssen den Code nicht auf jede neue Plattform übertragen. Um ein Beispiel zu geben, habe ich meine Geometriedaten ursprünglich in einem relativ unverarbeiteten Format gespeichert und zur Laufzeit die eigentlichen OpenGL/WebGL-Scheitelpunktpuffer zusammen gestellt. Dies erforderte eine gewisse Einrichtung und ein paar Hundert Zeilen Laufzeitcode. Später habe ich diesen Code in einen Vorverarbeitungsschritt verschoben und zur Kompilierungszeit vollständig gepackte OpenGL/WebGL-Vertex-Zwischenspeicher geschrieben. Die tatsächliche Menge an Code war ungefähr gleich, aber diese wenigen hundert Zeilen wurden in einen Vorverarbeitungsschritt verschoben, was bedeutet, dass ich sie nie auf neue Plattformen übertragen musste. In Bouncy Mouse gibt es zahlreiche Beispiele dafür, und die Möglichkeiten sind von Spiel zu Spiel unterschiedlich. Achte aber einfach auf alles, was während der Laufzeit nicht notwendig ist.

Keine Abhängigkeiten einnehmen, die Sie nicht brauchen

Ein weiterer Grund, warum Bouncy Mouse so einfach zu portieren ist, besteht darin, dass es fast keine Abhängigkeiten hat. Im folgenden Diagramm sind die wichtigsten Bibliotheksabhängigkeiten von Bouncy Mouse nach Plattform zusammengefasst:

Android iOS HTML5 WP7
Grafik OpenGL ES OpenGL ES WebGL Logo: XNA
Ton OpenSL Spanien OpenAL Web-Audio Logo: XNA
Physik Kasten 2D Kasten 2D Box2D.js Box2D.xna

Das war's auch schon. Abgesehen von Box2D, das auf alle Plattformen übertragen werden kann, wurden keine großen Drittanbieterbibliotheken verwendet. Bei der Grafikkarte erfolgt sowohl WebGL als auch XNA fast 1:1 mit OpenGL. Das war also kein großes Problem. Nur im Bereich der Klänge unterschied sich die eigentlichen Bibliotheken. Der Soundcode in Bouncy Mouse ist jedoch klein (mit etwa hundert Zeilen plattformspezifischer Code), sodass das kein großes Problem war. Wenn Sie Bouncy Mouse frei von großen, nicht portierbaren Bibliotheken halten, kann die Logik des Laufzeitcodes zwischen Versionen trotz der Sprachänderung nahezu identisch sein. Außerdem vermeiden wir so, an eine nicht tragbare Toolkette gefesselt zu werden. Ich wurde gefragt, ob die Programmierung direkt mit OpenGL/WebGL im Vergleich zu einer Bibliothek wie Cocos2D oder Unity komplexer ist (es gibt auch einige WebGL-Hilfsprogramme). Ich glaube sogar, genau das Gegenteil. Die meisten Handy-/HTML5-Spiele (zumindest solche wie Bouncy Mouse) sind sehr einfach. In den meisten Fällen zeichnet das Spiel nur ein paar Sprites und eventuell strukturierte Geometrie. Die Summe des OpenGL-spezifischen Codes in Bouncy Mouse beträgt wahrscheinlich weniger als 1.000 Zeilen. Ich wäre überrascht, wenn die Anzahl durch die Verwendung einer Hilfsbibliothek tatsächlich reduziert werden würde. Selbst wenn sich diese Zahl halbieren würde, müsste ich viel Zeit damit verbringen, neue Bibliotheken/Tools zu erlernen, um 500 Codezeilen zu sparen. Außerdem habe ich noch keine Hilfsbibliothek für alle für mich interessanten Plattformen gefunden, sodass eine solche Abhängigkeit die Portabilität erheblich beeinträchtigen würde. Wenn ich ein 3D-Spiel schreibe, das Lightmaps, dynamische Detailtiefe, Skin-Animationen usw. braucht, würde sich meine Antwort definitiv ändern. In diesem Fall würde ich das Rad neu erfinden, um zu versuchen, meine gesamte Engine per Handschrift für OpenGL zu programmieren. Mein Punkt ist, dass die meisten mobilen/HTML5-Spiele (noch) nicht in dieser Kategorie sind, sodass Sie die Dinge nicht komplizierter machen müssen, bevor es wirklich nötig ist.

Gemeinsamkeiten zwischen Sprachen nicht unterschätzen

Ein letzter Trick, der viel Zeit bei der Portierung meiner C++-Codebasis in eine neue Sprache spart, war die Erkenntnis, dass der Großteil des Codes in jeder Sprache nahezu identisch ist. Zwar können sich einige wichtige Elemente ändern, aber dies sind weitaus weniger als Dinge, die sich nicht ändern. Bei vielen Funktionen mussten in meiner C++-Codebasis ein paar Ersetzungen für reguläre Ausdrücke ausgeführt werden, um von C++ zu JavaScript zu wechseln.

Schlussfolgerungen zur Rufnummernmitnahme

So viel zur Mitnahme. In den nächsten Abschnitten werde ich auf einige HTML5-spezifische Herausforderungen eingehen. Die wichtigste Botschaft ist jedoch, dass die Portierung Ihnen nur ein wenig Kopfzerbrechen bereitet, kein Albtraum, wenn Sie Ihren Code einfach halten.

Audio

Ein Bereich, der mir (und anscheinend allen anderen) Probleme bereitete, war der Ton. Auf iOS- und Android-Geräten gibt es eine Reihe solider Audiooptionen (OpenSL, OpenAL), aber in der Welt von HTML5 sah die Sache schlecht aus. HTML5-Audio ist zwar verfügbar, aber ich habe festgestellt, dass es bei der Verwendung in Spielen Probleme birgt, die das beste Angebot für sich bieten. Selbst mit den neuesten Browsern bin ich häufig auf seltsames Verhalten gestoßen. Beispielsweise scheint es in Chrome nur eine begrenzte Anzahl an gleichzeitigen Audioelementen (Quelle) zu geben, die du erstellen kannst. Auch wenn Ton zu hören ist, wird er manchmal unerklärlich verzerrt. Insgesamt war ich etwas besorgt. Die Online-Suche ergab, dass nahezu alle das gleiche Problem haben. Die Lösung, zu der ich kam, war eine API namens SoundManager2. Diese API verwendet HTML5-Audio, sofern verfügbar, und greift in kniffligen Situationen auf Flash zurück. Obwohl diese Lösung funktionierte, war sie immer noch fehlerhaft und unvorhersehbar (genauer weniger als reines HTML5-Audio). Eine Woche nach der Einführung habe ich mit einigen der hilfreichen Leute bei Google gesprochen, die mich auf die Web Audio API von Webkit aufmerksam machten. Ich hatte ursprünglich darüber nachgedacht, diese API zu verwenden, aber wegen der (für mich unnötigen) Komplexität der API abgeschieden. Ich wollte nur ein paar Töne abspielen: Bei HTML5-Audio wären das ein paar Zeilen JavaScript. Bei meiner kurzen Betrachtung von Web Audio fiel mir jedoch auf, dass die 70-Seiten-Spezifikation sehr groß ist, die geringe Anzahl an Samples im Web (typisch für eine neue API) und irgendwo in der Spezifikation eine Funktion zum Abspielen, Pausieren oder Stoppen vorhanden war. Da Google mir sicher war, dass meine Befürchtungen nicht fundiert waren, habe ich mich wieder mit der API befasst. Nachdem ich mir ein paar weitere Beispiele angeschaut und ein bisschen mehr recherchiert hatte, stellte ich fest, dass Google recht hat. Die API kann meine Anforderungen auf jeden Fall erfüllen, und zwar ohne die Programmfehler, die die anderen APIs beeinträchtigen. Besonders praktisch ist der Artikel Erste Schritte mit der Web Audio API. Hier können Sie nachlesen, wie Sie die API noch besser verstehen. Das eigentliche Problem ist, dass ich die API auch nach dem Kennenlernen und Verwenden der API immer noch wie eine API mag, die nicht dafür entwickelt wurde, „nur ein paar Töne abzuspielen“. Um dieses Problem zu umgehen, habe ich eine kleine Hilfsklasse geschrieben, die es mir ermöglicht, die API genau so zu verwenden, wie ich es wollte: zum Abspielen, Anhalten, Anhalten und Abfragen des Tonstatus. Die Hilfsklasse AudioClip nannte ich. Die vollständige Quelle ist auf GitHub im Rahmen der Apache 2.0-Lizenz verfügbar. Ich werde später auf die Details des Kurses eingehen. Zunächst jedoch einige Hintergrundinformationen zur Web Audio API:

Web-Audiodiagramme

Das Erste, was das Web Audio API komplexer und leistungsfähiger macht als das HTML5 Audio-Element, ist die Fähigkeit, Audioinhalte zu verarbeiten / zu mischen, bevor sie an den Nutzer ausgegeben werden. Obwohl es zwar leistungsstark ist, aber die Tatsache, dass jede Audiowiedergabe ein Diagramm beinhaltet, macht die Dinge in einfachen Szenarien etwas komplexer. Die Leistungsfähigkeit des Web Audio APIs wird anhand des folgenden Diagramms veranschaulicht:

Einfaches Web-Audiodiagramm
Basic Web Audio Graph

Obwohl das obige Beispiel die Leistungsfähigkeit der Web Audio API zeigt, habe ich den größten Teil dieser Leistung in meinem Szenario nicht benötigt. Ich wollte nur einen Ton abspielen. Dies erfordert zwar eine Grafik, diese ist jedoch sehr einfach.

Graphen können einfach sein

Das Erste, was das Web Audio API komplexer und leistungsfähiger macht als das HTML5 Audio-Element, ist die Fähigkeit, Audioinhalte zu verarbeiten / zu mischen, bevor sie an den Nutzer ausgegeben werden. Obwohl es zwar leistungsstark ist, aber die Tatsache, dass jede Audiowiedergabe ein Diagramm beinhaltet, macht die Dinge in einfachen Szenarien etwas komplexer. Die Leistungsfähigkeit des Web Audio APIs wird anhand des folgenden Diagramms veranschaulicht:

Trivial Web Audio Graph
Trivial Web Audio Graph

Die einfache Grafik oben kann alles erreichen, was zum Abspielen, Pausieren oder Anhalten eines Tons erforderlich ist.

Aber machen Sie sich keine Gedanken über den Graphen

Das Diagramm ist zwar gut zu verstehen, aber ich möchte mich nicht jedes Mal darum kümmern, wenn ich einen Ton abspiele. Deshalb habe ich die einfache Wrapper-Klasse „AudioClip“ geschrieben. Diese Klasse verwaltet diesen Graphen intern, stellt aber eine wesentlich einfachere für den Nutzer sichtbare API dar.

AudioClip
AudioClip

Diese Klasse ist nichts weiter als eine Web Audio-Grafik und ein Hilfszustand, aber ermöglicht mir, viel einfacheren Code zu verwenden, als wenn ich eine Web Audio-Grafik erstellen müsste, um alle Töne abzuspielen.

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

// Later
sound.play();

Details zur Implementierung

Sehen wir uns kurz den Code der Hilfsklasse an: Konstruktor: Der Konstruktor übernimmt das Laden der Tondaten mithilfe einer XHR. Ein HTML5-Audioelement kann auch als Quellknoten verwendet werden, auch wenn es hier nicht angezeigt wird (um das Beispiel einfach zu halten). Dies ist besonders bei großen Stichproben hilfreich. Beachten Sie, dass die Web Audio API erfordert, dass wir diese Daten als "Arraybuffer" abrufen. Sobald die Daten empfangen sind, erstellen wir einen Web Audio-Puffer aus diesen Daten (decodieren sie aus ihrem ursprünglichen Format in ein Laufzeit-PCM-Format).

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

Wiedergabe – Das Abspielen unseres Tons erfolgt in zwei Schritten: Einrichten der Wiedergabegrafik und Aufrufen einer Version von „noteOn“ in der Quelle der Grafik. Eine Quelle kann nur einmal wiedergegeben werden. Daher muss die Quelle/das Diagramm bei jeder Wiedergabe neu erstellt werden. Diese Funktion ist zum Großteil auf Anforderungen zurückzuführen, die zum Fortsetzen eines pausierten Clips (this.pauseTime_ > 0) erforderlich sind. Um die Wiedergabe eines pausierten Clips fortzusetzen, verwenden wir noteGrainOn. Damit wird die Wiedergabe einer Teilregion eines Zwischenspeichers ermöglicht. Leider interagiert noteGrainOn nicht auf die für dieses Szenario gewünschte Weise mit Schleifen. Es führt eine Schleife für die Unterregion aus, nicht den gesamten Zwischenspeicher. Daher müssen wir das Problem umgehen, indem wir den Rest des Clips mit noteGrainOn wiedergeben und den Clip dann mit aktivierter Schleife von vorn beginnen.

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

Als Soundeffekt abspielen - Die oben beschriebene Wiedergabefunktion verhindert, dass der Audioclip mehrfach mit Überlappung abgespielt wird. Eine zweite Wiedergabe ist nur möglich, wenn der Clip beendet oder angehalten wurde. Manchmal möchte ein Spiel einen Ton mehrmals abspielen, ohne auf den Abschluss der einzelnen Wiedergaben warten zu müssen, z. B. Münzen in einem Spiel zu sammeln. Die AudioClip-Klasse hat dazu die Methode playAsSFX(). Da mehrere Wiedergaben gleichzeitig stattfinden können, ist die Wiedergabe von playAsSFX() nicht 1:1 an den AudioClip gebunden. Daher kann die Wiedergabe nicht angehalten, pausiert oder abgefragt werden. Schleifen sind ebenfalls deaktiviert, da es keine Möglichkeit gibt, eine solche Schleife zu stoppen.

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

Stoppen, Pausieren und Abfragestatus: Die übrigen Funktionen sind ziemlich einfach und erfordern nicht viel Erklärung:

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

Audio-Fazit

Hoffentlich ist diese Hilfsklasse für Entwickler hilfreich, die mit denselben Audioproblemen zu tun haben wie ich. Außerdem scheint eine solche Klasse ein vernünftiger Ausgangspunkt zu sein, selbst wenn Sie einige der leistungsstärkeren Funktionen des Web Audio APIs hinzufügen müssen. In jedem Fall entsprach diese Lösung den Anforderungen von Bouncy Mouse und wurde ein echtes HTML5-Spiel ohne Bedingungen.

Leistung

Ein weiterer Bereich, der mir Sorgen machte, war die Leistungsfähigkeit eines JavaScript-Ports. Nachdem ich v1 meines Ports fertiggestellt hatte, stellte ich fest, dass auf meinem Quad-Core-Desktop alles einwandfrei funktionierte. Auf einem Netbook oder Chromebook lief das Problem leider nicht. In diesem Fall hat mir der Profiler von Chrome gespart, weil genau angezeigt wurde, wo die Zeit in meinen Programmen verbracht wurde. Meine Erfahrung zeigt, wie wichtig die Profilerstellung ist, bevor eine Optimierung vorgenommen wird. Ich hatte erwartet, dass Box2D-physik oder vielleicht der Renderingcode eine deutliche Verlangsamung verursachen würde, aber die meiste Zeit verbrachte ich mit meiner Matrix.clone()-Funktion. Angesichts des mathematischen Charakters meines Spiels wusste ich, dass ich viele Matrixerstellung/-klonen durchführte, aber ich hätte nie erwartet, dass das ein Engpass sein würde. Am Ende stellte sich heraus, dass die CPU-Nutzung durch eine einfache Änderung um mehr als das Dreifache reduziert werden konnte – von 6–7% auf meinem Desktop-Computer auf 2%. Vielleicht gehört dies zu den meisten JavaScript-Entwicklern, aber als C++-Entwickler hat mich dieses Problem überrascht. Deshalb werde ich darauf genauer eingehen. Meine ursprüngliche Matrixklasse war im Grunde eine 3x3-Matrix: ein Array aus 3 Elementen, wobei jedes Element ein Array mit 3 Elementen enthielt. Leider bedeutete dies, dass ich beim Klonen der Matrix vier neue Arrays erstellen musste. Die einzige Änderung, die ich vornehmen musste, war, diese Daten in ein einzelnes Array mit 9 Elementen zu verschieben und meine Mathematik entsprechend zu aktualisieren. Diese eine Änderung war allein für die dreifache Reduzierung der CPU-Leistung verantwortlich, und die Änderung war auf allen meinen Testgeräten akzeptabel.

Weitere Optimierung

Meine Leistung war zwar akzeptabel, aber es gab immer noch ein paar kleine Probleme. Nach einer etwas mehr Profilerstellung wurde mir klar, dass dies an der automatischen Speicherbereinigung von JavaScript lag. Meine App lief mit 60 fps, was bedeutet, dass jeder Frame nur 16 ms zum Zeichnen zur Verfügung stand. Leider nimmt die automatische Speicherbereinigung auf einem langsameren Computer manchmal bis zu 10 Millisek. auf. Dies führte zu einem Ruckeln aller Sekunden, da für das Spiel fast die vollen 16 ms benötigt wurden, um einen ganzen Frame zu zeichnen. Um besser zu verstehen, warum ich so viel Müll erzeugt habe, habe ich den Heap-Profiler von Chrome verwendet. Zu meiner Verzweiflung stellte sich heraus, dass der Großteil des Mülls (über 70%) von Box2D erzeugt wurde. Die Beseitigung von Müll in JavaScript ist eine schwierige Aufgabe und die Umformulierung von Box2D kam nicht infrage, also fiel mir auf, dass ich in eine Ecke gestanden hatte. Zum Glück hatte ich immer noch einen der ältesten Tricks in dem Buch: Wenn du keine 60 fps erreichen kannst, solltest du mit 30 fps laufen. Es ist ziemlich einvernehmlich, dass ein Betrieb bei konstanten 30 fps viel besser ist als bei rauschenden 60 fps. Tatsächlich habe ich immer noch keine Beschwerde oder einen Kommentar dazu erhalten, dass das Spiel mit 30 fps läuft (das ist schwer zu sagen, wenn man nicht die beiden Versionen nebeneinander vergleicht). Diese zusätzlichen 16 ms pro Frame bedeuteten, dass ich selbst bei einer hässlichen Speicherbereinigung noch genügend Zeit hatte, um den Frame zu rendern. Obwohl die Ausführung mit 30 fps durch die von mir verwendete Timing-API nicht explizit aktiviert wurde (die hervorragende requestAnimationFrame-Funktion von WebKit), kann dies auf sehr einfache Weise umgesetzt werden. Auch wenn 30 fps vielleicht nicht so elegant wie eine explizite API erreicht werden können, sollten Sie wissen, dass das Intervall von RequestAnimationFrame an die VSYNC des Monitors (normalerweise 60 fps) ausgerichtet ist. Das bedeutet, dass wir einfach jeden weiteren Callback ignorieren müssen. Wenn Sie einen Callback „Tick“ haben, der jedes Mal aufgerufen wird, wenn „RequestAnimationFrame“ ausgelöst wird, können Sie dies so erreichen:

var skip = false;

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

// OTHER CODE
}

Wenn Sie besonders vorsichtig sein möchten, sollten Sie überprüfen, ob die VSYNC-Funktion des Computers beim Start nicht bereits 30 fps erreicht oder darunter liegt, und das Überspringen in diesem Fall deaktivieren. Ich habe dies jedoch noch nicht bei den von mir getesteten Desktop-/Laptop-Konfigurationen gesehen.

Vertrieb und Monetarisierung

Ein letzter Punkt, der mich am Chrome-Port von Bouncy Mouse überrascht hat, war die Monetarisierung. Im Rahmen dieses Projekts sah ich HTML5-Spiele als ein interessantes Experiment zum Erlernen neuer Technologien vor. Mir war nicht bewusst, dass mit der Plattform eine sehr große Zielgruppe erreicht werden kann und dass ein hohes Monetarisierungspotenzial bestehen würde.

Bouncy Mouse wurde Ende Oktober im Chrome Web Store auf den Markt gebracht. Durch die Veröffentlichung im Chrome Web Store konnte ich ein bestehendes System für Sichtbarkeit, Community-Interaktion, Rankings und andere Funktionen nutzen, an die ich mich auf mobilen Plattformen gewöhnt hatte. Was mich überrascht hat, war die Reichweite des Ladens. Innerhalb eines Monats nach der Veröffentlichung hatte ich fast 400.000 Installationen erzielt und profitierte bereits von den Interaktionen mit der Community (Fehlerberichte, Feedback). Was mich außerdem überrascht hat, ist das Monetarisierungspotenzial einer Web-App.

Bouncy Mouse bietet eine einfache Monetarisierungsmethode: eine Banneranzeige neben dem Spielinhalt. Aufgrund der großen Reichweite des Spiels stellte ich jedoch fest, dass mit dieser Banneranzeigen erhebliche Einnahmen erzielt werden konnten. Während der Spitzenzeit hat die App Einnahmen erzielt, die mit meiner erfolgreichsten Plattform, Android, vergleichbar sind. Ein Faktor, der dazu beiträgt, ist, dass mit den größeren AdSense-Anzeigen in der HTML5-Version ein wesentlich höherer Umsatz pro Impression erzielt wird als mit den kleineren AdMob-Anzeigen unter Android. Außerdem ist die Banneranzeige in der HTML5-Version viel weniger aufdringlich als in der Android-Version, was ein klareres Spielerlebnis ermöglicht. Insgesamt war ich sehr angenehm überrascht von diesem Ergebnis.

Normalisierte Einnahmen im Zeitverlauf.
Normalisierte Einnahmen im Zeitverlauf

Die Einnahmen des Spiels waren zwar viel besser als erwartet, doch die Reichweite des Chrome Web Store ist immer noch geringer als die von etablierteren Plattformen wie Android Market. Bouncy Mouse gelang es zwar, schnell auf Platz 9 der beliebtesten Spiele im Chrome Web Store zu klettern, doch die Zahl der neuen Nutzer, die die Website besucht haben, ist seit der ersten Veröffentlichung erheblich zurückgegangen. Das Spiel wächst jedoch weiterhin stetig und ich bin gespannt auf die Entwicklung der Plattform.

Fazit

Die Portierung von Bouncy Mouse zu Chrome lief wesentlich reibungsloser als erwartet. Abgesehen von einigen geringfügigen Audio- und Leistungsproblemen stellte ich fest, dass Chrome eine absolut kompatible Plattform für ein vorhandenes Smartphonespiel war. Ich kann alle Entwickler, die sich davor scheuen, davon überzeugen, es einmal auszuprobieren. Ich bin sowohl mit dem Portierungsprozess als auch mit dem neuen Gaming-Publikum sehr zufrieden, das mich durch ein HTML5-Spiel begeistert hat. Falls Sie Fragen haben, können Sie sich jederzeit per E-Mail an mich wenden. Oder hinterlasst einfach einen Kommentar unten, ich werde das regelmäßig prüfen.