Einführung
Nachdem ich Bouncy Mouse Ende letzten Jahres für iOS und Android veröffentlicht hatte, habe ich einige sehr wichtige Erkenntnisse gewonnen. Einer der wichtigsten Punkte war, dass es schwierig ist, in einen etablierten Markt einzudringen. Auf dem übersättigten iPhone-Markt war es sehr schwierig, an Fahrt zu gewinnen. Auf dem weniger gesättigten Android-Markt waren die Fortschritte einfacher, aber immer noch nicht einfach. Aufgrund dieser Erfahrung sah ich im Chrome Web Store eine interessante Chance. Der Web Store ist zwar keineswegs leer, aber sein Katalog mit hochwertigen HTML5-basierten Spielen ist noch relativ jung. Für neue App-Entwickler bedeutet das, dass es viel einfacher ist, in die Bestenlisten zu kommen und Sichtbarkeit zu erlangen. In der Hoffnung, dass ich mit meinem neuesten Spiel eine neue Nutzerbasis erreichen könnte, habe ich Bouncy Mouse in HTML5 portiert. In dieser Fallstudie werde ich kurz über den allgemeinen Prozess der Portierung von Bouncy Mouse nach HTML5 sprechen und dann drei Bereiche genauer untersuchen, die sich als interessant erwiesen haben: Audio, Leistung und Monetarisierung.
C++-Spiele in HTML5 portieren
Bouncy Mouse ist derzeit für Android(C++), iOS (C++), Windows Phone 7 (C#) und Chrome (Javascript) verfügbar. Das wirft gelegentlich die Frage auf: Wie entwirft man ein Spiel, das sich leicht auf mehrere Plattformen übertragen lässt? Ich habe das Gefühl, dass die Leute auf eine Art Wunderwaffe hoffen, mit der sie diese Mobilität erreichen können, ohne auf einen manuellen Anschluss zurückgreifen zu müssen. Leider bin ich mir nicht sicher, ob es eine solche Lösung bereits gibt. Am ehesten kommt das PlayN-Framework von Google oder die Unity-Engine infrage, aber keines dieser Tools erfüllt alle Anforderungen, die ich hatte. Ich habe tatsächlich eine manuelle Portierung durchgeführt. Ich habe zuerst die iOS-/Android-Version in C++ geschrieben und dann diesen Code auf jede neue Plattform portiert. Das mag nach viel Arbeit klingen, aber die WP7- und Chrome-Versionen haben jeweils nicht länger als zwei Wochen gedauert. Die Frage ist nun, ob es etwas gibt, was eine Codebasis einfach tragbar macht. Ich habe dabei ein paar Dinge getan, die mir geholfen haben:
Codebasis klein halten
Das mag zwar offensichtlich erscheinen, ist aber der Hauptgrund, warum ich das Spiel so schnell portieren konnte. Der Clientcode von Bouncy Mouse besteht aus nur etwa 7.000 C++-Codezeilen. 7.000 Codezeilen sind zwar keine Kleinigkeit, aber sie sind überschaubar. Sowohl die C#- als auch die JavaScript-Version des Clientcodes hatten ungefähr die gleiche Größe. Um die Codebasis klein zu halten, habe ich im Grunde zwei wichtige Praktiken angewendet: keinen überflüssigen Code schreiben und so viel wie möglich in der Vorverarbeitung (nicht zur Laufzeit) erledigen. Es mag offensichtlich erscheinen, keinen überflüssigen Code zu schreiben, aber das ist etwas, womit ich immer hadere. Ich habe oft den Drang, eine Hilfsklasse/-funktion für alles zu schreiben, was in einen Helfer einfließen kann. Wenn Sie einen Helfer jedoch nicht mehrmals verwenden möchten, führt dies in der Regel nur dazu, dass Ihr Code aufgebläht wird. Bei Bouncy Mouse habe ich darauf geachtet, nur dann Hilfsfunktionen zu schreiben, wenn ich sie mindestens dreimal verwenden würde. Als ich eine Hilfsklasse schrieb, habe ich versucht, sie sauber, portabel und für meine zukünftigen Projekte wiederverwendbar zu gestalten. Beim Schreiben von Code nur für Bouncy Mouse, der mit geringer Wahrscheinlichkeit wiederverwendet wird, lag mein Fokus dagegen darauf, die Programmieraufgabe 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 wichtigere Teil, um die Codebasis klein zu halten, bestand darin, so viel wie möglich in Vorverarbeitungsschritte zu verschieben. Wenn Sie eine Laufzeitaufgabe in eine Vorverarbeitungsaufgabe verschieben können, läuft Ihr Spiel nicht nur schneller, sondern Sie müssen den Code auch nicht auf jede neue Plattform portieren. Beispielsweise habe ich meine Levelgeometriedaten ursprünglich in einem relativ unverarbeiteten Format gespeichert und die tatsächlichen OpenGL-/WebGL-Vertex-Buffer zur Laufzeit zusammengestellt. Das erforderte etwas Einrichtung und einige hundert Zeilen Laufzeitcode. Später habe ich diesen Code in einen Vorverarbeitungsschritt verschoben und zur Kompilierungszeit vollständig verpackte OpenGL-/WebGL-Vertex-Buffer geschrieben. Die tatsächliche Codemenge war ungefähr gleich, aber diese paar hundert Zeilen wurden in einen Vorverarbeitungsschritt verschoben, sodass ich sie nie auf neue Plattformen portieren musste. In Bouncy Mouse gibt es unzählige Beispiele dafür. Was möglich ist, hängt von Spiel zu Spiel ab. Achten Sie einfach auf alles, was nicht zur Laufzeit passieren muss.
Nicht benötigte Abhängigkeiten vermeiden
Ein weiterer Grund, warum Bouncy Mouse einfach zu portieren ist, ist, dass es fast keine Abhängigkeiten hat. Im folgenden Diagramm sind die wichtigsten Bibliotheksabhängigkeiten von Bouncy Mouse pro Plattform zusammengefasst:
Das war es im Grunde. Außer Box2D, das plattformübergreifend einsetzbar ist, wurden keine großen Drittanbieterbibliotheken verwendet. Bei Grafiken werden sowohl WebGL als auch XNA fast 1:1 mit OpenGL zugeordnet, sodass dies kein großes Problem war. Nur im Bereich Ton waren die tatsächlichen Bibliotheken unterschiedlich. Der Soundcode in Bouncy Mouse ist jedoch klein (ungefähr hundert Zeilen plattformspezifischen Codes), sodass dies kein großes Problem war. Da Bouncy Mouse keine großen nicht portablen Bibliotheken enthält, kann die Logik des Laufzeitcodes zwischen den Versionen trotz der Sprachänderung fast gleich sein. Außerdem sind wir nicht an eine nicht portable Tool-Chain gebunden. Ich wurde gefragt, ob das direkte Codieren mit OpenGL/WebGL im Vergleich zur Verwendung einer Bibliothek wie Cocos2D oder Unity zu einer erhöhten Komplexität führt. Es gibt auch einige WebGL-Hilfsprogramme. Ich glaube genau das Gegenteil. Die meisten Smartphone-/HTML5-Spiele (zumindest solche wie Bouncy Mouse) sind sehr einfach. In den meisten Fällen werden im Spiel nur ein paar Sprites und eventuell etwas texturierte Geometrie gezeichnet. Die Gesamtzahl der OpenGL-spezifischen Codezeilen in Bouncy Mouse beträgt wahrscheinlich weniger als 1.000. Ich wäre überrascht, wenn sich diese Zahl durch die Verwendung einer Hilfsbibliothek tatsächlich verringern würde. Selbst wenn sich diese Zahl halbieren würde, müsste ich viel Zeit darauf verwenden, neue Bibliotheken und Tools zu lernen, nur um 500 Zeilen Code zu sparen. Außerdem habe ich noch keine Hilfsbibliothek gefunden, die auf allen Plattformen, an denen ich interessiert bin, lauffähig ist. Eine solche Abhängigkeit würde die Portabilität erheblich beeinträchtigen. Wenn ich ein 3D-Spiel entwerfen würde, für das Lightmaps, dynamische LODs, Skin-Animationen usw. erforderlich sind, würde sich meine Antwort sicherlich ändern. In diesem Fall würde ich das Rad neu erfinden, wenn ich versuchen würde, meine gesamte Engine von Hand für OpenGL zu programmieren. Mein Punkt ist, dass die meisten Mobile-/HTML5-Spiele (noch) nicht in dieser Kategorie sind. Es ist also nicht nötig, die Dinge zu verkomplizieren, bevor es notwendig ist.
Ähnlichkeiten zwischen Sprachen nicht unterschätzen
Ein letzter Trick, der mir beim Portieren meiner C++-Codebasis in eine neue Sprache viel Zeit ersparte, war die Erkenntnis, dass der Großteil des Codes in jeder Sprache fast identisch ist. Einige wichtige Elemente können sich ändern, aber es gibt weitaus mehr Dinge, die sich nicht ändern. Bei vielen Funktionen war es sogar möglich, mit nur wenigen regulären Ausdrucks-Ersetzungen in meiner C++-Codebasis von C++ auf JavaScript umzustellen.
Schlussfolgerungen zur Portierung
Das war es im Wesentlichen mit dem Portierungsvorgang. In den nächsten Abschnitten gehe ich auf einige HTML5-spezifische Herausforderungen ein. Die Hauptbotschaft ist jedoch, dass die Portierung kein Albtraum, sondern nur ein kleiner Ärger ist, wenn Sie Ihren Code einfach halten.
Audio
Ein Bereich, der mir (und anscheinend allen anderen) einige Probleme bereitet hat, war der Ton. Auf iOS- und Android-Geräten gibt es eine Reihe solider Audiooptionen (OpenSL, OpenAL), aber in der HTML5-Welt sah es düster aus. HTML5-Audio ist zwar verfügbar, aber ich habe festgestellt, dass es bei der Verwendung in Spielen einige Probleme gibt, die einen Deal platzen lassen. Selbst in den neuesten Browsern kam es häufig zu merkwürdigem Verhalten. In Chrome ist beispielsweise die Anzahl der gleichzeitigen Audioelemente (source), die Sie erstellen können, begrenzt. Außerdem war der Ton manchmal unerklärlich verzerrt, selbst wenn er abgespielt wurde. Insgesamt war ich etwas besorgt. Bei der Onlinesuche stellte sich heraus, dass fast jeder das gleiche Problem hat. Die Lösung, die ich ursprünglich gefunden habe, war eine API namens SoundManager2. Diese API verwendet nach Möglichkeit HTML5-Audio und greift in schwierigen Situationen auf Flash zurück. Diese Lösung funktionierte zwar, war aber immer noch fehlerhaft und unvorhersehbar (nur weniger als reines HTML5-Audio). Eine Woche nach der Veröffentlichung sprach ich mit einigen hilfreichen Mitarbeitern von Google, die mich auf die Web Audio API von Webkit aufmerksam machten. Ich hatte ursprünglich überlegt, diese API zu verwenden, aber aufgrund der für mich unnötigen Komplexität der API davon abgesehen. Ich wollte nur ein paar Töne abspielen: Mit HTML5-Audio sind dafür nur ein paar Zeilen JavaScript erforderlich. Bei einem kurzen Blick auf Web Audio fiel mir jedoch die riesige Spezifikation (70 Seiten), die geringe Anzahl von Samples im Web (typisch für eine neue API) und das Fehlen einer Funktion zum „Abspielen“, „Pausieren“ oder „Anhalten“ auf. Da Google mir versicherte, dass meine Bedenken unbegründet waren, habe ich mich noch einmal mit der API beschäftigt. Nachdem ich mir einige weitere Beispiele angesehen und ein wenig mehr recherchiert hatte, stellte ich fest, dass Google recht hatte: Die API kann meine Anforderungen definitiv erfüllen und das ohne die Fehler, die die anderen APIs plagen. Besonders hilfreich ist der Artikel Erste Schritte mit der Web Audio API. Er bietet einen guten Einstieg, wenn Sie mehr über die API erfahren möchten. Mein eigentliches Problem ist, dass die API, auch nachdem ich sie verstanden und verwendet habe, immer noch nicht für die „Wiedergabe einiger Töne“ geeignet erscheint. Um dieses Problem zu umgehen, habe ich eine kleine Hilfsklasse geschrieben, mit der ich die API genau so verwenden kann, wie ich es möchte: zum Abspielen, Pausieren, Stoppen und Abfragen des Status eines Tons. Ich habe diese Hilfsklasse „AudioClip“ genannt. Der vollständige Quellcode ist auf GitHub unter der Apache 2.0-Lizenz verfügbar. Die Details der Klasse werden unten erläutert. Zuerst aber einige Hintergrundinformationen zur Web Audio API:
Web-Audiodiagramme
Die Web Audio API ist komplexer (und leistungsfähiger) als das HTML5-Audioelement, weil sie Audio verarbeiten und mischen kann, bevor es an den Nutzer gesendet wird. Die Tatsache, dass jede Audiowiedergabe ein Diagramm erfordert, macht die Sache in einfachen Szenarien zwar leistungsfähig, aber auch etwas komplizierter. Die folgende Grafik veranschaulicht die Leistungsfähigkeit der Web Audio API:
Das obige Beispiel zeigt die Leistungsfähigkeit der Web Audio API, aber in meinem Szenario benötigte ich den Großteil dieser Funktionen nicht. Ich wollte nur einen Ton abspielen. Auch dafür ist ein Diagramm erforderlich, das aber sehr einfach ist.
Diagramme können einfach sein
Die Web Audio API ist komplexer (und leistungsfähiger) als das HTML5-Audioelement, weil sie Audio verarbeiten und mischen kann, bevor es an den Nutzer gesendet wird. Die Tatsache, dass jede Audiowiedergabe ein Diagramm erfordert, macht die Sache in einfachen Szenarien zwar leistungsfähig, aber auch etwas komplizierter. Die folgende Grafik veranschaulicht die Leistungsfähigkeit der Web Audio API:
Mit dem einfachen Diagramm oben können Sie alles tun, was zum Abspielen, Pausieren oder Stoppen eines Tons erforderlich ist.
Aber lassen Sie uns nicht einmal über die Grafik nachdenken.
Es ist zwar schön, das Diagramm zu verstehen, aber ich möchte nicht jedes Mal, wenn ich einen Ton abspiele, damit umgehen müssen. Deshalb habe ich eine einfache Wrapper-Klasse namens „AudioClip“ geschrieben. Diese Klasse verwaltet diesen Graphen intern, bietet aber eine viel einfachere API für Nutzer.
Diese Klasse ist nichts weiter als ein Web Audio-Graph und ein Hilfsstatus, ermöglicht mir aber die Verwendung viel einfacherer Codes, als wenn ich für jeden Ton einen Web Audio-Graph erstellen müsste.
// At startup time
var sound = new AudioClip("ping.wav");
// Later
sound.play();
Details zur Implementierung
Sehen wir uns den Code der Hilfsklasse an: Konstruktor: Der Konstruktor lädt die Audiodaten über XHR. Zur Vereinfachung des Beispiels wird hier kein HTML5-Audioelement verwendet. Es könnte aber auch als Quellknoten verwendet werden. Das ist besonders bei großen Stichproben hilfreich. Die Web Audio API erfordert, dass wir diese Daten als „Arraybuffer“ abrufen. Sobald die Daten empfangen wurden, erstellen wir daraus einen Web Audio-Puffer, indem wir sie aus dem ursprünglichen Format in ein Laufzeit-PCM-Format decodieren.
/**
* 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 des Tons umfasst zwei Schritte: das Einrichten des Wiedergabe-Graphen und das Aufrufen einer Version von „noteOn“ an der Quelle des Graphen. Eine Quelle kann nur einmal wiedergegeben werden. Daher müssen wir die Quelle/den Graphen jedes Mal neu erstellen, wenn wir sie wiedergeben.
Die Komplexität dieser Funktion ergibt sich hauptsächlich aus den Anforderungen, die für die Wiedergabe eines pausierten Clips (this.pauseTime_ > 0
) erforderlich sind. Dazu wird noteGrainOn
verwendet, mit dem eine Teilregion eines Buffers wiedergegeben werden kann. Leider interagiert noteGrainOn
in diesem Szenario nicht wie gewünscht mit dem Looping. Es wird die Unterregion und nicht der gesamte Puffer wiederholt.
Daher müssen wir das Problem umgehen, indem wir den Rest des Clips mit noteGrainOn
abspielen und den Clip dann von vorn mit aktiviertem Looping wieder starten.
/**
* 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 Toneffekt abspielen: Mit der Wiedergabefunktion oben kann der Audioclip nicht mehrmals überlappend abgespielt werden. Eine zweite Wiedergabe ist nur möglich, wenn der Clip beendet oder angehalten wurde. Manchmal soll in einem Spiel ein bestimmter Ton mehrmals abgespielt werden, ohne dass die Wiedergabe jedes Mal abgeschlossen werden muss (z. B. beim Sammeln von Münzen in einem Spiel). Dazu gibt es in der AudioClip-Klasse die Methode playAsSFX()
.
Da mehrere Wiedergaben gleichzeitig erfolgen können, ist die Wiedergabe von playAsSFX()
nicht 1:1 mit dem AudioClip verknüpft. Daher kann die Wiedergabe nicht angehalten, pausiert oder auf den Status geprüft werden. Auch das Looping ist deaktiviert, da es keine Möglichkeit gibt, einen auf diese Weise abgespielten Looping-Ton zu beenden.
/**
* 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 Status abfragen: Die restlichen 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
Ich hoffe, dass diese Hilfsklasse für Entwickler nützlich ist, die mit denselben Audioproblemen zu kämpfen haben wie ich. Außerdem ist ein Kurs wie dieser ein guter Ausgangspunkt, auch wenn Sie einige der leistungsfähigeren Funktionen der Web Audio API hinzufügen müssen. In jedem Fall erfüllte diese Lösung die Anforderungen von Bouncy Mouse und ermöglichte es, das Spiel ohne Einschränkungen als HTML5-Spiel zu entwickeln.
Leistung
Ein weiterer Bereich, der mir bei einem JavaScript-Port Sorgen machte, war die Leistung. Nachdem ich Version 1 meines Ports fertiggestellt hatte, stellte ich fest, dass alles auf meinem Quad-Core-Desktop einwandfrei funktionierte. Auf einem Netbook oder Chromebook war die Leistung leider nicht ganz zufriedenstellend. In diesem Fall hat mir der Profiler von Chrome geholfen, da er mir genau zeigte, wo die Zeit meiner Programme verbracht wurde.
Meine Erfahrung zeigt, wie wichtig das Profiling vor jeder Optimierung ist. Ich hatte erwartet, dass die Box2D-Physik oder vielleicht der Rendering-Code eine Hauptursache für die Verlangsamung sein würde. Die meiste Zeit verbrachte ich jedoch mit der Matrix.clone()
-Funktion. Aufgrund der mathematischen Natur meines Spiels wusste ich, dass ich viele Matrizen erstellen und klonen musste, aber ich hätte nie erwartet, dass dies das Nadelöhr sein würde. Letztendlich stellte sich heraus, dass durch eine sehr einfache Änderung die CPU-Auslastung des Spiels um mehr als das Dreifache reduziert werden konnte, von 6–7% CPU auf meinem Desktop auf 2%.
Vielleicht ist das für JavaScript-Entwickler Allgemeinwissen, aber als C++-Entwickler hat mich dieses Problem überrascht. Deshalb gehe ich etwas näher darauf ein. Die ursprüngliche Matrixklasse war eine 3 × 3-Matrix: ein Array mit drei Elementen, wobei jedes Element ein Array mit drei Elementen enthält. Leider musste ich beim Klonen der Matrix vier neue Arrays erstellen. Die einzige Änderung, die ich vornehmen musste, war, diese Daten in ein einzelnes Array mit 9 Elementen zu verschieben und meine Berechnungen entsprechend zu aktualisieren. Diese Änderung war allein für die dreifache CPU-Reduktion verantwortlich. Nach dieser Änderung war die Leistung auf allen meinen Testgeräten akzeptabel.
Weitere Optimierung
Die Leistung war zwar akzeptabel, aber es gab immer noch einige kleinere Probleme. Nach etwas mehr Profiling stellte ich fest, dass dies auf die automatische Speicherbereinigung von JavaScript zurückzuführen war. Meine App lief mit 60 fps, was bedeutete, dass jeder Frame nur 16 ms zum Zeichnen hatte. Leider dauerte die Garbage Collection auf einem langsameren Computer manchmal etwa 10 ms. Das führte alle paar Sekunden zu Rucklern, da das Spiel fast die vollen 16 ms benötigte, um einen vollständigen Frame zu zeichnen. Um besser nachvollziehen zu können, warum so viel ungenutzter Speicher erzeugt wurde, habe ich den Heap-Profiler von Chrome verwendet. Zu meiner großen Enttäuschung stellte sich heraus, dass der Großteil des Mülls (über 70%) von Box2D generiert wurde. Das Entfernen von Müll in JavaScript ist eine heikle Angelegenheit und das Umschreiben von Box2D kam nicht infrage. Ich erkannte, dass ich mich in eine Sackgasse manövriert hatte. Glücklicherweise hatte ich noch einen der ältesten Tricks auf Lager: Wenn ich 60 fps nicht erreiche, kann ich auch mit 30 fps arbeiten. Es ist weitgehend unbestritten, dass eine konstante Bildrate von 30 fps weitaus besser ist als eine ruckelige Bildrate von 60 fps. Tatsächlich habe ich noch keine Beschwerde oder einen Kommentar erhalten, dass das Spiel mit 30 fps läuft. Das ist wirklich schwer zu erkennen, es sei denn, man vergleicht die beiden Versionen direkt miteinander. Diese zusätzlichen 16 ms pro Frame bedeuteten, dass ich auch bei einer langen Garbage Collection noch genügend Zeit zum Rendern des Frames hatte. Die von mir verwendete Timing-API (die hervorragende requestAnimationFrame von WebKit) unterstützt zwar nicht explizit eine Ausführung mit 30 fps, aber das ist ganz einfach möglich. 30 fps sind zwar nicht so elegant wie eine explizite API, aber wenn Sie wissen, dass das Intervall von RequestAnimationFrame mit dem VSYNC des Monitors (normalerweise 60 fps) übereinstimmt, können Sie diese Framerate erreichen. Das bedeutet, dass wir alle anderen Rückrufe einfach ignorieren müssen. Wenn Sie einen Rückruf „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 du besonders vorsichtig sein möchtest, solltest du prüfen, ob die VSYNC-Rate des Computers beim Starten nicht bereits bei oder unter 30 fps liegt. In diesem Fall solltest du das Überspringen deaktivieren. Ich habe das jedoch bei keiner der von mir getesteten Desktop-/Laptop-Konfigurationen beobachtet.
Bereitstellung und Monetarisierung
Ein letzter Bereich, der mich beim Chrome-Port von Bouncy Mouse überrascht hat, war die Monetarisierung. Als ich an dieses Projekt herangegangen bin, sah ich HTML5-Spiele als interessanten Test an, um neue Technologien kennenzulernen. Ich hatte nicht erwartet, dass der Port ein sehr großes Publikum erreichen und ein erhebliches Monetarisierungspotenzial haben würde.
Bouncy Mouse wurde Ende Oktober im Chrome Web Store veröffentlicht. Durch die Veröffentlichung im Chrome Web Store konnte ich ein bestehendes System für Sichtbarkeit, Community-Engagement, Rankings und andere Funktionen nutzen, die ich von mobilen Plattformen gewohnt war. Überrascht hat mich, wie groß die Reichweite des Shops war. Innerhalb eines Monats nach der Veröffentlichung hatte ich fast 400.000 Installationen erreicht und profitierte bereits vom Engagement der Community (Fehlermeldungen, Feedback). Was mich auch überrascht hat, war das Monetarisierungspotenzial einer Webanwendung.
Bouncy Mouse bietet eine einfache Monetarisierungsmethode: eine Banneranzeige neben den Spielinhalten. Aufgrund der großen Reichweite des Spiels konnte ich jedoch feststellen, dass diese Banneranzeige erhebliche Einnahmen generieren konnte. In der Hochphase erzielte die App Einnahmen, die mit denen meiner erfolgreichsten Plattform, Android, vergleichbar waren. Ein Grund dafür ist, dass die größeren AdSense-Anzeigen, die in der HTML5-Version ausgeliefert werden, deutlich höhere Umsätze pro Impression erzielen als die kleineren AdMob-Anzeigen, die auf Android-Geräten ausgeliefert werden. Außerdem ist die Banneranzeige in der HTML5-Version viel weniger aufdringlich als in der Android-Version, was ein ununterbrochenes Gameplay ermöglicht. Insgesamt war ich sehr angenehm überrascht von diesem Ergebnis.

Die Einnahmen aus dem Spiel waren zwar viel höher als erwartet, aber die Reichweite des Chrome Web Store ist immer noch geringer als die von etablierteren Plattformen wie dem Android Market. Bouncy Mouse konnte sich zwar schnell zum neuntbeliebtesten Spiel im Chrome Web Store hocharbeiten, die Anzahl der neuen Nutzer auf der Website hat sich seit der Erstveröffentlichung jedoch deutlich verlangsamt. Das Spiel verzeichnet jedoch weiterhin ein stetiges Wachstum und ich bin gespannt, wie sich die Plattform weiterentwickelt.
Fazit
Ich würde sagen, dass die Portierung von Bouncy Mouse auf Chrome viel einfacher war als erwartet. Abgesehen von einigen kleineren Audio- und Leistungsproblemen war Chrome eine leistungsstarke Plattform für ein bestehendes Smartphone-Spiel. Ich kann allen Entwicklern, die sich bisher nicht an die Funktion herangetraut haben, nur raten, es zu versuchen. Ich war sowohl mit dem Portierungsvorgang als auch mit der neuen Zielgruppe sehr zufrieden, die ich durch ein HTML5-Spiel erreicht habe. Bei Fragen kannst du dich jederzeit gern an mich wenden. Du kannst auch einfach einen Kommentar unten hinterlassen. Ich versuche, diese regelmäßig zu lesen.