Étude de cas : Bouncy Mouse

Introduction

Souris rebondissante

Après avoir publié Bouncy Mouse sur iOS et Android à la fin de l'année dernière, j'ai tiré quelques enseignements très importants. Parmi les principaux points, il a souligné qu'il est difficile de pénétrer un marché établi. Sur le marché saturé de l'iPhone, il a été très difficile de gagner du terrain. Sur Android Marketplace, moins saturé, les progrès ont été plus faciles, mais pas toujours simples. Cette expérience m'a permis de voir une opportunité intéressante sur le Chrome Web Store. Bien que le Web Store ne soit pas vide, son catalogue de jeux HTML5 de haute qualité ne fait que commencer à mûrir. Pour un nouveau développeur d'applications, cela signifie que les classements et la visibilité sont beaucoup plus faciles à obtenir. Avec cette opportunité à l'esprit, je me suis lancé dans le portage de Bouncy Mouse vers HTML5 dans l'espoir de proposer ma dernière expérience de jeu à une nouvelle base d'utilisateurs. Dans cette étude de cas, je vais parler un peu du processus général de portage de Bouncy Mouse vers HTML5, puis je vais examiner plus en détail trois domaines qui se sont révélés intéressants: l'audio, les performances et la monétisation.

Portage d'un jeu C++ vers HTML5

Bouncy Mouse est actuellement disponible sur Android(C++), iOS (C++), Windows Phone 7 (C#) et Chrome (JavaScript). Cette question peut parfois se poser: comment écrire un jeu qui peut être facilement porté sur plusieurs plates-formes ? J'ai le sentiment que les utilisateurs espèrent trouver une solution miracle pour obtenir ce niveau de portabilité sans avoir à effectuer une conversion manuelle. Malheureusement, je ne suis pas sûr qu'une telle solution existe déjà (le plus proche est probablement le framework PlayN de Google ou le moteur Unity, mais aucun de ces deux éléments ne répond à toutes les cibles qui m'intéressent). Mon approche était en fait un port manuel. J'ai d'abord écrit la version iOS/Android en C++, puis porté ce code sur chaque nouvelle plate-forme. Cela peut sembler beaucoup de travail, mais les versions WP7 et Chrome n'ont pas pris plus de deux semaines chacune. La question est donc de savoir s'il est possible de rendre un codebase facilement portable. J'ai pris quelques mesures pour y parvenir:

Limiter la taille du codebase

Même si cela peut sembler évident, c'est la principale raison pour laquelle j'ai pu porter le jeu si rapidement. Le code client de Bouncy Mouse ne compte que 7 000 lignes de code C++. 7 000 lignes de code ne sont pas rien, mais c'est assez peu pour être gérable. Les versions C# et JavaScript du code client ont fini par avoir à peu près la même taille. Pour réduire la taille de mon codebase, j'ai suivi deux pratiques clés: ne pas écrire de code superflu et faire le plus possible dans le code de prétraitement (hors exécution). Écrire du code sans excès peut sembler évident, mais c'est une chose que je me reproche toujours. J'ai souvent envie d'écrire une classe/fonction d'assistance pour tout ce qui peut être intégré à une assistance. Toutefois, sauf si vous prévoyez d'utiliser une aide plusieurs fois, elle finit généralement par gonfler votre code. Avec Bouncy Mouse, je me suis efforcé de ne jamais écrire d'aide sauf si je l'utilisais au moins trois fois. Lorsque j'ai écrit une classe d'assistance, j'ai essayé de la rendre propre, portable et réutilisable pour mes futurs projets. En revanche, lorsque j'ai écrit du code uniquement pour Bouncy Mouse, avec une faible probabilité de réutilisation, mon objectif était d'accomplir la tâche de codage aussi simplement et rapidement que possible, même si ce n'était pas la façon la plus "joli" d'écrire le code. La deuxième partie, et la plus importante, pour réduire la taille du codebase a consisté à transférer autant que possible les étapes de prétraitement. Si vous pouvez prendre une tâche d'exécution et la déplacer vers une tâche de prétraitement, votre jeu s'exécutera non seulement plus rapidement, mais vous n'aurez pas à porter le code sur chaque nouvelle plate-forme. Par exemple, j'ai initialement stocké mes données de géométrie de niveau dans un format peu traité, en assemblant les tampons de sommets OpenGL/WebGL réels au moment de l'exécution. Cela a nécessité un peu de configuration et quelques centaines de lignes de code d'exécution. Plus tard, j'ai déplacé ce code vers une étape de prétraitement, en écrivant des tampons de sommets OpenGL/WebGL entièrement empaquetés au moment de la compilation. La quantité de code réelle était à peu près la même, mais ces quelques centaines de lignes avaient été déplacées vers une étape de prétraitement, ce qui signifie que je n'ai jamais eu à les porter vers de nouvelles plates-formes. Il existe de nombreux exemples de ce type dans Bouncy Mouse. Les possibilités varient d'un jeu à l'autre, mais gardez un œil sur tout ce qui n'a pas besoin de se produire au moment de l'exécution.

Ne prenez pas de dépendances dont vous n'avez pas besoin

Bouncy Mouse est également facile à porter, car il ne comporte presque aucune dépendance. Le graphique suivant récapitule les principales dépendances de bibliothèques de Bouncy Mouse par plate-forme:

Android iOS HTML5 WP7
Graphiques OpenGL ES OpenGL ES WebGL XNA
Son OpenSL ES OpenAL Web Audio XNA
Physique Box2D Box2D Box2D.js Box2D.xna

C'est à peu près tout. Aucune grande bibliothèque tierce n'a été utilisée, à l'exception de Box2D, qui est portable sur toutes les plates-formes. Pour les graphismes, WebGL et XNA se mappent presque à l'échelle 1:1 avec OpenGL. Ce n'était donc pas un gros problème. Seule la bibliothèque de sons était différente. Toutefois, le code audio de Bouncy Mouse est petit (environ une centaine de lignes de code spécifique à la plate-forme), ce qui n'a pas constitué un problème majeur. En gardant Bouncy Mouse exempt de grandes bibliothèques non portables, la logique du code d'exécution peut être presque la même d'une version à l'autre (malgré le changement de langage). De plus, cela nous évite d'être bloqués dans une chaîne d'outils non portable. On m'a demandé si le codage directement avec OpenGL/WebGL entraînait une complexité accrue par rapport à l'utilisation d'une bibliothèque comme Cocos2D ou Unity (il existe également des assistants WebGL). En fait, je pense tout le contraire. La plupart des jeux pour téléphone mobile / HTML5 (du moins ceux comme Bouncy Mouse) sont très simples. Dans la plupart des cas, le jeu ne dessine que quelques sprites et peut-être une géométrie texturée. Le nombre total de lignes de code spécifique à OpenGL dans Bouncy Mouse est probablement inférieur à 1 000. Je serais surpris si l'utilisation d'une bibliothèque d'assistance réduisait réellement ce nombre. Même si ce nombre était réduit de moitié, je devrais passer beaucoup de temps à apprendre de nouvelles bibliothèques/outils pour économiser 500 lignes de code. De plus, je n'ai pas encore trouvé de bibliothèque d'assistance portable sur toutes les plates-formes qui m'intéressent. Une telle dépendance nuirait donc considérablement à la portabilité. Si j'écrivais un jeu 3D nécessitant des cartes de lumière, des niveaux de détail dynamiques, des animations avec skinning, etc., ma réponse changerait certainement. Dans ce cas, je réinventerais la roue en essayant de coder manuellement l'intégralité de mon moteur avec OpenGL. Je veux dire que la plupart des jeux mobiles/HTML5 ne font (pas encore) partie de cette catégorie. Il n'est donc pas nécessaire de compliquer les choses avant que ce ne soit nécessaire.

Ne sous-estimez pas les similitudes entre les langues

Un dernier conseil qui m'a fait gagner beaucoup de temps lors du portage de mon codebase C++ vers un nouveau langage a été de réaliser que la plupart du code est presque identique entre chaque langage. Certains éléments clés peuvent changer, mais ils sont bien moins nombreux que les éléments qui ne changent pas. En fait, pour de nombreuses fonctions, passer de C++ à JavaScript impliquait simplement d'exécuter quelques remplacements d'expressions régulières sur mon codebase C++.

Conclusions sur le portage

C'est à peu près tout pour le processus de transfert. Je vais aborder quelques défis spécifiques au code HTML5 dans les prochaines sections, mais le message principal est que, si vous gardez votre code simple, le portage ne sera qu'une petite corvée, et non un cauchemar.

Audio

L'audio a été un problème pour moi (et apparemment pour tout le monde). Sur iOS et Android, un certain nombre de choix audio fiables sont disponibles (OpenSL, OpenAL), mais dans le monde du HTML5, les choses étaient plus sombres. Bien que l'audio HTML5 soit disponible, j'ai constaté qu'il présentait des problèmes rédhibitoires lorsqu'il était utilisé dans les jeux. Même sur les navigateurs les plus récents, je rencontrais fréquemment des comportements étranges. Chrome, par exemple, semble limiter le nombre d'éléments audio (source) simultanés que vous pouvez créer. De plus, même lorsque le son était diffusé, il était parfois déformé de manière inexpliquée. J'étais un peu inquiet. Une recherche en ligne m'a révélé que presque tout le monde avait le même problème. La solution que j'ai initialement trouvée était une API appelée SoundManager2. Cette API utilise HTML5 Audio lorsqu'elle est disponible, et utilise Flash dans les situations difficiles. Bien que cette solution fonctionne, elle reste buggée et imprévisible (mais moins que l'audio HTML5 pur). Une semaine après le lancement, j'ai discuté avec des membres de l'équipe Google, qui m'ont orienté vers l'API Web Audio de Webkit. J'avais initialement envisagé d'utiliser cette API, mais je l'ai évitée en raison de la complexité inutile (pour moi) qu'elle semblait avoir. Je voulais simplement lire quelques sons: avec HTML5 Audio, cela revient à quelques lignes de code JavaScript. Cependant, lors de mon bref examen de Web Audio, j'ai été frappé par sa spécification énorme (70 pages), la faible quantité d'exemples sur le Web (typique pour une nouvelle API) et l'absence de fonction "play", "pause" ou "stop" dans les spécifications. Google m'ayant assuré que mes inquiétudes n'étaient pas fondées, je me suis à nouveau penché sur l'API. Après avoir examiné quelques autres exemples et effectué quelques recherches supplémentaires, j'ai constaté que Google avait raison : l'API peut certainement répondre à mes besoins, et ce sans les bugs qui affectent les autres API. L'article Premiers pas avec l'API Web Audio est particulièrement utile si vous souhaitez mieux comprendre l'API. Mon vrai problème est que, même après avoir compris et utilisé l'API, elle me semble toujours conçue pour "jouer simplement quelques sons". Pour contourner ce doute, j'ai écrit une petite classe d'assistance qui me permet d'utiliser l'API comme je le souhaite : lire, mettre en pause, arrêter et interroger l'état d'un son. J'ai appelé cette classe d'assistance AudioClip. Le code source complet est disponible sur GitHub sous licence Apache 2.0. Je vais vous expliquer les détails de la classe ci-dessous. Mais d'abord, voici quelques informations sur l'API Web Audio:

Graphiques Web Audio

La première chose qui rend l'API Web Audio plus complexe (et plus puissante) que l'élément audio HTML5 est sa capacité à traiter / mélanger l'audio avant de le diffuser auprès de l'utilisateur. Bien que puissant, le fait que toute lecture audio implique un graphique rend les choses un peu plus complexes dans des scénarios simples. Pour illustrer la puissance de l'API Web Audio, examinons le graphique suivant:

Graphique Web Audio de base
Graphique audio Web de base

Bien que l'exemple ci-dessus montre la puissance de l'API Web Audio, je n'ai pas eu besoin de la plupart de cette puissance dans mon scénario. Je voulais juste diffuser un son. Bien que cela nécessite toujours un graphique, il s'agit d'un graphique très simple.

Les graphiques peuvent être simples

La première chose qui rend l'API Web Audio plus complexe (et plus puissante) que l'élément audio HTML5 est sa capacité à traiter / mélanger l'audio avant de le diffuser auprès de l'utilisateur. Bien que puissant, le fait que toute lecture audio implique un graphique rend les choses un peu plus complexes dans des scénarios simples. Pour illustrer la puissance de l'API Web Audio, examinons le graphique suivant:

Graphique audio Web trivial
Graphique audio Web trivial

Le graphique trivial présenté ci-dessus peut effectuer tout ce qui est nécessaire pour lire, mettre en pause ou arrêter un son.

Mais ne nous préoccupons pas du graphique.

Bien que comprendre le graphique soit utile, ce n'est pas quelque chose que je veux gérer à chaque fois que je lance un son. J'ai donc écrit une classe de wrapper simple "AudioClip". Cette classe gère ce graphique en interne, mais présente une API beaucoup plus simple pour l'utilisateur.

AudioClip
AudioClip

Cette classe n'est rien d'autre qu'un graphique Web Audio et un état d'assistance, mais elle me permet d'utiliser un code beaucoup plus simple que si je devais créer un graphique Web Audio pour lire chaque son.

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

// Later
sound.play();

Détails de mise en œuvre

Examinons rapidement le code de la classe d'assistance : Constructeur : le constructeur gère le chargement des données audio à l'aide d'un XHR. Bien que cela ne soit pas illustré ici (pour simplifier l'exemple), un élément Audio HTML5 peut également être utilisé comme nœud source. Cela est particulièrement utile pour les échantillons volumineux. Notez que l'API Web Audio exige que nous récupérions ces données en tant que "arraybuffer". Une fois les données reçues, nous créons un tampon Web Audio à partir de ces données (en les décodant à partir de leur format d'origine en format PCM d'exécution).

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

Lecture : la lecture de notre son implique deux étapes : configurer le graphique de lecture et appeler une version de "noteOn" sur la source du graphique. Une source ne peut être lue qu'une seule fois. Nous devons donc recréer la source/le graphique à chaque lecture. La plupart de la complexité de cette fonction provient des exigences requises pour reprendre un extrait mis en pause (this.pauseTime_ > 0). Pour reprendre la lecture d'un extrait mis en pause, nous utilisons noteGrainOn, qui permet de lire une sous-région d'un tampon. Malheureusement, noteGrainOn n'interagit pas avec la boucle de la manière souhaitée pour ce scénario (il boucle la sous-région, et non l'ensemble du tampon). Nous devons donc contourner ce problème en lisant le reste de l'extrait avec noteGrainOn, puis en le redémarrant depuis le début avec la boucle activée.

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

Lire en tant qu'effet sonore : la fonction de lecture ci-dessus ne permet pas de lire le clip audio plusieurs fois en superposition (une deuxième lecture n'est possible que lorsque le clip est terminé ou arrêté). Il arrive qu'un jeu souhaite lire un son plusieurs fois sans attendre la fin de chaque lecture (collecter des pièces dans un jeu, etc.). Pour ce faire, la classe AudioClip dispose d'une méthode playAsSFX(). Étant donné que plusieurs lectures peuvent se produire simultanément, la lecture à partir de playAsSFX() n'est pas liée à l'AudioClip. Par conséquent, la lecture ne peut pas être arrêtée, mise en pause ni interrogée sur son état. La boucle est également désactivée, car il n'existe aucun moyen d'arrêter un son en boucle joué de cette manière.

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

Arrêt, mise en pause et état de la requête : le reste des fonctions est assez simple et ne nécessite pas beaucoup d'explications :

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

Conclusion audio

J'espère que cette classe d'assistance sera utile aux développeurs qui rencontrent les mêmes problèmes audio que moi. De plus, une classe comme celle-ci semble être un point de départ raisonnable, même si vous devez ajouter certaines des fonctionnalités les plus puissantes de l'API Web Audio. Dans tous les cas, cette solution a répondu aux besoins de Bouncy Mouse et a permis au jeu d'être un véritable jeu HTML5, sans aucune condition.

Performances

Un autre aspect qui m'inquiétait concernant un port JavaScript était les performances. Une fois la version 1 de mon port terminée, j'ai constaté que tout fonctionnait correctement sur mon ordinateur de bureau quadricœur. Malheureusement, les performances n'étaient pas optimales sur un netbook ou un Chromebook. Dans ce cas, le profileur de Chrome m'a sauvé en m'indiquant exactement où était dépensé tout le temps de mes programmes. Mon expérience souligne l'importance du profilage avant toute optimisation. Je m'attendais à ce que la physique Box2D ou peut-être le code de rendu soient une source majeure de ralentissement. Cependant, la majeure partie de mon temps était en fait consacrée à ma fonction Matrix.clone(). Étant donné la nature mathématique de mon jeu, je savais que j'avais créé/clonné de nombreuses matrices, mais je ne m'attendais pas à ce que ce soit le goulot d'étranglement. En fin de compte, une modification très simple a permis au jeu de réduire son utilisation du processeur de plus de trois fois, passant de 6 à 7% de CPU sur mon ordinateur de bureau à 2%. Il s'agit peut-être d'une connaissance courante pour les développeurs JavaScript, mais en tant que développeur C++, ce problème m'a surpris. Je vais donc entrer un peu plus dans les détails. En gros, ma classe de matrice d'origine était une matrice 3x3: un tableau de trois éléments, chacun contenant un tableau de trois éléments. Malheureusement, cela signifiait que lorsque j'ai voulu cloner la matrice, j'ai dû créer quatre nouveaux tableaux. La seule modification que j'ai dû apporter était de déplacer ces données dans un tableau à 9 éléments et de mettre à jour mes calculs en conséquence. Cette modification était entièrement responsable de la réduction de 3 fois de l'utilisation du processeur que j'ai constatée. Après ce changement, mes performances étaient acceptables sur tous mes appareils de test.

Optimisation supplémentaire

Mes performances étaient acceptables, mais je constatais encore quelques petits problèmes. Après un peu plus de profilage, j'ai réalisé que cela était dû à la récupération de mémoire de JavaScript. Mon application fonctionnait à 60 fps, ce qui signifie que chaque frame n'avait que 16 ms pour être dessiné. Malheureusement, lorsque la récupération de mémoire était lancée sur une machine plus lente, elle pouvait parfois prendre environ 10 ms. Cela entraînait un à-coup toutes les quelques secondes, car le jeu nécessitait presque les 16 ms complets pour dessiner un frame complet. Pour mieux comprendre pourquoi je générais autant de déchets, j'ai utilisé le profileur de tas de Chrome. À mon grand désespoir, il s'est avéré que la grande majorité des déchets (plus de 70%) étaient générés par Box2D. Éliminer les déchets en JavaScript est une tâche délicate, et réécrire Box2D était hors de question. Je me suis donc rendu compte que j'étais dans une impasse. Heureusement, je disposais encore de l'un des trucs les plus anciens du livre: lorsque vous ne pouvez pas atteindre 60 FPS, exécutez-le à 30 FPS. Il est généralement admis qu'une fréquence d'images de 30 FPS constante est bien meilleure qu'une fréquence de 60 FPS saccadée. En fait, je n'ai encore reçu aucune plainte ni aucun commentaire concernant le fait que le jeu fonctionne à 30 FPS (c'est vraiment difficile à dire, sauf si vous comparez les deux versions côte à côte). Ces 16 ms supplémentaires par frame signifiaient que même en cas de récupération de mémoire inesthétique, j'avais encore suffisamment de temps pour afficher le frame. Bien que l'exécution à 30 FPS ne soit pas explicitement activée par l'API de temporisation que j'utilisais (l'excellente requestAnimationFrame de WebKit), elle peut être effectuée de manière très simple. Bien que ce ne soit peut-être pas aussi élégant qu'une API explicite, vous pouvez obtenir 30 FPS en sachant que l'intervalle de RequestAnimationFrame est aligné sur la fréquence VSYNC de l'écran (généralement 60 FPS). Cela signifie que nous n'avons qu'à ignorer tous les autres rappels. En gros, si vous avez un rappel "Tick" qui est appelé chaque fois que "RequestAnimationFrame" est déclenché, vous pouvez procéder comme suit:

var skip = false;

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

// OTHER CODE
}

Pour plus de prudence, vérifiez que la fréquence d'images VSYNC de l'ordinateur n'est pas déjà inférieure ou égale à 30 FPS au démarrage, et désactivez le saut dans ce cas. Cependant, je n'ai pas encore vu cela sur les configurations de bureau/ordinateur portable que j'ai testées.

Distribution et monétisation

Un dernier point qui m'a surpris concernant le port Chrome de Bouncy Mouse est la monétisation. Avant de me lancer dans ce projet, j'ai envisagé les jeux HTML5 comme une expérience intéressante pour apprendre les technologies émergentes. Je ne m'attendais pas à ce que le portage atteigne une très large audience et présente un potentiel de monétisation important.

Bouncy Mouse a été lancée fin octobre sur le Chrome Web Store. En lançant mon application sur le Chrome Web Store, j'ai pu exploiter un système existant pour la visibilité, l'engagement de la communauté, le classement et d'autres fonctionnalités auxquelles j'étais habitué sur les plates-formes mobiles. Ce qui m'a surpris, c'est la couverture du magasin. Un mois après la sortie, j'avais atteint près de 400 000 installations et je bénéficiais déjà de l'engagement de la communauté (signalement de bugs, commentaires). Un autre point qui m'a surpris est le potentiel de monétisation d'une application Web.

Bouncy Mouse utilise une méthode de monétisation simple : une bannière publicitaire à côté du contenu du jeu. Toutefois, compte tenu de la large couverture du jeu, j'ai constaté que cette bannière publicitaire pouvait générer des revenus importants. Au pic de sa popularité, l'application a généré des revenus comparables à ceux de ma plate-forme la plus populaire, Android. Cela s'explique en partie par le fait que les annonces AdSense plus grandes diffusées dans la version HTML5 génèrent des revenus par impression nettement plus élevés que les annonces AdMob plus petites diffusées sur Android. De plus, la bannière publicitaire de la version HTML5 est beaucoup moins intrusive que celle de la version Android, ce qui offre une expérience de jeu plus fluide. Dans l'ensemble, j'ai été très agréablement surpris par ce résultat.

Revenus normalisés au fil du temps
Revenus normalisés au fil du temps

Bien que les revenus générés par le jeu aient été bien supérieurs aux attentes, il est intéressant de noter que la couverture du Chrome Web Store est encore inférieure à celle de plates-formes plus matures comme Android Market. Bien que Bouncy Mouse ait rapidement atteint la neuvième place des jeux les plus populaires sur le Chrome Web Store, le nombre de nouveaux utilisateurs sur le site a considérablement ralenti depuis la sortie initiale. Cela dit, le jeu continue de croître régulièrement, et je suis impatient de voir comment la plate-forme va évoluer.

Conclusion

Je dirais que le portage de Bouncy Mouse vers Chrome s'est déroulé beaucoup plus facilement que prévu. Mis à part quelques problèmes mineurs d'audio et de performances, j'ai trouvé que Chrome était une plate-forme parfaitement adaptée à un jeu pour smartphone existant. J'encourage tous les développeurs qui ont hésité à essayer cette expérience à s'y lancer. Je suis très satisfait du processus de portage et de la nouvelle audience de joueurs à laquelle j'ai pu accéder grâce à un jeu HTML5. N'hésitez pas à m'envoyer un e-mail si vous avez des questions. Vous pouvez également laisser un commentaire ci-dessous. Je les consulterai régulièrement.