Étude de cas : Bouncy Mouse

Présentation

Souris rebondissante

Après avoir publié Bouncy Mouse sur iOS et Android à la fin de l'année dernière, j'ai appris quelques enseignements très importants. L'un d'entre eux, c'est qu'il est difficile de pénétrer un marché bien établi. Sur le marché très saturé de l'iPhone, la montée en popularité a été très difficile. Sur l'Android Market, moins saturé, les progrès ont été plus faciles, mais pas faciles. Au vu de cette expérience, j'ai vu une opportunité intéressante sur le Chrome Web Store. Le Web Store est en aucun cas vide, mais son catalogue de jeux HTML5 de grande qualité commence tout juste à atteindre sa maturité. Pour un nouveau développeur d'applications, il est beaucoup plus facile d'améliorer les classements et d'améliorer la visibilité. Compte tenu de cette opportunité, j'ai décidé de transférer Bouncy Mouse en HTML5. J'espère pouvoir proposer ma dernière expérience de jeu à une nouvelle base d'utilisateurs passionnante. Dans cette étude de cas, je vais vous présenter le processus général de portage de Bouncy Mouse en HTML5. Ensuite, j'aborderai plus en détail trois domaines qui se sont avérés intéressants: l'audio, les performances et la monétisation.

Portage d'un jeu C++ en HTML5

Bouncy Mouse est actuellement disponible sur Android(C++), iOS (C++), Windows Phone 7 (C#) et Chrome (JavaScript). Cela pose parfois la question suivante: comment créer un jeu facile à transférer sur plusieurs plates-formes ? J'ai l'impression que les gens espèrent trouver une solution miracle qu'ils pourront utiliser pour atteindre ce niveau de portabilité sans passer par un port manuel. Malheureusement, je ne suis pas sûr qu'une telle solution existe encore (le plus proche est probablement le framework PlayN de Google ou le moteur Unity, mais aucune de ces solutions n'atteint les objectifs qui m'intéressaient). Mon approche était en fait une approche manuelle. J'ai d'abord écrit la version iOS/Android en C++, puis porté ce code sur chaque nouvelle plateforme. Bien que cela puisse représenter beaucoup de travail, les versions WP7 et Chrome n'ont pas pris plus de deux semaines chacune. La question est donc la suivante : est-il possible de faire quelque chose pour rendre un codebase facilement portable ? J'ai fait deux choses qui m'ont aidé:

Limiter la taille du codebase

Même si cela peut sembler évident, c'est la principale raison pour laquelle j'ai pu transférer le jeu aussi rapidement. Le code client de Bouncy Mouse n'est composé que d'environ 7 000 lignes de C++. 7 000 lignes de code ne sont rien, mais il est suffisamment petit pour être gérable. Les versions C# et JavaScript du code client ont fini par avoir à peu près la même taille. Réduire la taille de mon codebase équivaudrait à deux pratiques clés: ne pas écrire de code en excès et faire autant que possible dans le code de prétraitement (hors exécution). Il peut sembler évident de ne pas écrire de code en trop, mais c'est une chose que je me bat toujours avec moi-même. J'ai souvent envie d'écrire une classe/fonction d'assistance pour tout ce qui peut être intégré dans une aide. Toutefois, à moins que vous ne prévoyiez d'utiliser un assistant plusieurs fois, il finit généralement par surcharger votre code. Avec Bouncy Mouse, je n'avais jamais écrit d'aide, sauf si je l'utilise au moins trois fois. Lorsque j'écrivais une classe d'assistance, j'essayais de la rendre propre, portable et réutilisable pour mes futurs projets. En revanche, lorsque j'écrivais 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 méthode la plus "splendide" d'écrire le code. Le deuxième point, et le plus important, pour maintenir la petite taille du codebase, consistait à pousser le plus 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, non seulement votre jeu s'exécutera plus rapidement, mais vous n'aurez pas à transférer le code sur chaque nouvelle plate-forme. Pour vous donner un exemple, j'ai initialement stocké mes données de géométrie du niveau dans un format relativement non traité, en assemblant les tampons de sommets OpenGL/WebGL réels au moment de l'exécution. Cela a demandé un peu de configuration et quelques centaines de lignes de code d'exécution. J'ai ensuite 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é réelle de code était à peu près identique, 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 transférer sur une nouvelle plate-forme. Il existe de très nombreux exemples dans Bouncy Mouse, et les possibilités varient d'un jeu à l'autre, mais restez à l'affût de tout ce qui n'est pas nécessaire au moment de l'exécution.

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

Une autre raison pour laquelle Bouncy Mouse est facile à transférer, car elle n'a pratiquement aucune dépendance. Le tableau suivant récapitule les principales dépendances de la bibliothèque Bouncy Mouse par plate-forme:

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

C'est à peu près tout. Aucune bibliothèque tierce volumineuse n'a été utilisée, à l'exception de Box2D, qui est portable sur toutes les plates-formes. Pour les graphismes, WebGL et XNA sont mappés quasiment en 1:1 avec OpenGL. Ce n'était donc pas un problème majeur. C'est seulement dans le domaine du son que les bibliothèques réelles étaient différentes. Cependant, le code audio de Bouncy Mouse est petit (environ une centaine de lignes de code spécifique à une plate-forme), ce qui n'a donc pas posé problème. En évitant à Bouncy Mouse d'utiliser des bibliothèques non portables volumineuses, 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 de nous retrouver coincés dans une chaîne d'outils non portable. On m'a demandé si le codage avec OpenGL/WebGL accroît directement la complexité par rapport à l'utilisation d'une bibliothèque comme Cocos2D ou Unity (il existe également des assistants WebGL). En fait, je crois qu'il s'agit du contraire. La plupart des jeux pour téléphones mobiles / 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 forme géométrique texturée. La somme totale du code spécifique à OpenGL dans Bouncy Mouse est probablement inférieure à 1 000 lignes. Je serais surpris si l'utilisation d'une bibliothèque d'aide permettrait de réduire ce nombre. Même si cela avait permis de diviser ce nombre par deux, il me faudrait passer beaucoup de temps à apprendre de nouvelles bibliothèques et de nouveaux outils pour économiser 500 lignes de code. En plus de cela, je n'ai pas encore trouvé de bibliothèque d'aide portable sur toutes les plates-formes qui m'intéressent. Une telle dépendance serait donc considérablement nuisible à la portabilité. Si j'écrivais un jeu en 3D nécessitant des lightmaps, un LOD dynamique, une animation à l'habillage, etc., ma réponse changerait certainement. Dans ce cas, je réinventerais la roue pour essayer de coder manuellement l'ensemble de mon moteur avec OpenGL. Ce que je veux dire, c'est que la plupart des jeux pour mobile/HTML5 ne figurent pas (encore) dans cette catégorie. Il est donc inutile de compliquer les choses avant que cela ne soit nécessaire.

Ne sous-estimez pas les similitudes entre les langues

Une dernière astuce qui a permis de gagner beaucoup de temps lors du portage de mon codebase C++ vers un nouveau langage est de réaliser que la majeure partie du code est presque identique dans chaque langage. Bien que certains éléments clés puissent changer, ceux-ci sont beaucoup moins que des é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 dans mon codebase C++.

Conclusions sur le portage

C'est à peu près tout pour le processus de portage. Dans les sections suivantes, nous aborderons quelques difficultés spécifiques au format HTML5, mais sachez que, si vous faites simple, le portage sera un petit casse-tête, et non un cauchemar.

Audio

L'un des problèmes qui m'a causé (et apparemment tous les autres) était l'audio. Sur iOS et Android, un certain nombre de choix audio fiables sont disponibles (OpenSL, OpenAL), mais avec le langage HTML5, l'interface semblait plus sombre. Bien que l'audio HTML5 soit disponible, j'ai constaté qu'il présentait des problèmes majeurs lorsqu'il est utilisé dans des jeux. Même dans les navigateurs les plus récents, j'ai souvent eu un comportement étrange. Dans Chrome, par exemple, le nombre d'éléments audio simultanés (source) que vous pouvez créer est limité. De plus, même si le son était diffusé, il pouvait parfois se retrouver déformé de façon inexplicable. Dans l'ensemble, j'étais un peu inquiet. Une recherche en ligne a révélé que presque tout le monde rencontre le même problème. La solution à laquelle j'ai commencé par arriver était une API appelée SoundManager2. Cette API utilise l'audio HTML5 lorsqu'il est disponible et recourt au format Flash dans les situations délicates. Même si cette solution fonctionnait, elle présentait toujours des bugs et des imprévisibles (moins que l'audio HTML5 pur). Une semaine après le lancement, j'ai parlé à certaines des personnes serviables de Google, qui m'ont montré l'API Web Audio de Webkit. À l'origine, j'avais envisagé d'utiliser cette API, mais j'ai renoncé à l'utiliser en raison de sa complexité (pour moi) inutile. Je voulais juste écouter quelques sons: avec l'audio HTML5, cela équivaut à quelques lignes de code JavaScript. J'ai toutefois été frappé par son énorme spécification (70 pages), par la petite quantité d'échantillons sur le Web (généralement pour une nouvelle API) et par l'omission d'une fonction "play", "pause" ou "stop" dans les spécifications. Google m'ayant assuré que mes soucis n'étaient pas bien fondés, j'ai à nouveau utilisé l'API. Après avoir examiné quelques exemples supplémentaires et effectué quelques recherches supplémentaires, j'ai trouvé que Google avait raison : l'API peut vraiment répondre à mes besoins, sans les bugs affectant les autres API. L'article Getting Started with Web Audio API (Premiers pas avec l'API Web Audio) est particulièrement utile si vous souhaitez approfondir vos connaissances sur l'API. Mon vrai problème, c'est que même après avoir compris et utilisé l'API, il me semble toujours que cette API n'est pas conçue pour "lancer simplement quelques sons". Pour éviter ces doutes, j'ai écrit une petite classe d'assistance qui me permet d'utiliser l'API comme je le voulais : 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. Nous reviendrons plus en détail sur le cours ci-dessous. Tout d'abord, voici quelques informations sur l'API Web Audio:

Audio graphismes Web

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 et mixer des contenus audio avant de les transmettre à l'utilisateur. Bien que très efficace, 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, prenons le graphique suivant:

Audio Graphisme Web de base
Basic Web Audio Graph

Bien que l'exemple ci-dessus montre la puissance de l'API Web Audio, je n'avais pas besoin de la majeure partie de cette puissance dans mon scénario. Je voulais juste faire un son. Bien que cela nécessite toujours un graphique, celui-ci est 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 et mixer des contenus audio avant de les transmettre à l'utilisateur. Bien que très efficace, 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, prenons le graphique suivant:

Trivial Web Audio Graph
Trivial Web Audio Graph

Le graphique simple présenté ci-dessus permet d'exécuter tout ce qui est nécessaire pour lire, mettre en pause ou arrêter un son.

Mais ne nous soucions pas du graphique

Bien qu'il soit agréable de comprendre le graphique, ce n'est pas quelque chose que je veux gérer chaque fois que je fais un son. Par conséquent, j'ai écrit une classe 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'aide. 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 sonores à l'aide d'une requête XHR. Bien qu'il ne soit pas présenté ici (pour que cet exemple soit simple), un élément audio HTML5 peut également être utilisé comme nœud source. Cela est particulièrement utile pour les échantillons de grande taille. Notez que l'API Web Audio nécessite d'extraire ces données sous forme de "arraybuffer". Une fois les données reçues, nous créons un tampon Web Audio à partir de ces données (en les décodant de leur format d'origine au 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 du son comporte 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. Cette fonction est en grande partie complexe et répond aux 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 partie d'un tampon. Malheureusement, noteGrainOn n'interagit pas avec les boucles de la manière souhaitée pour ce scénario (elle mettra en boucle la sous-région, et non l'ensemble du tampon). Pour contourner ce problème, nous devons lire le reste de l'extrait avec noteGrainOn, puis le redémarrer depuis le début en activant la lecture en boucle.

/**
* 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 comme effet sonore : la fonction de lecture ci-dessus ne permet pas de lire le clip audio plusieurs fois avec chevauchement (une seconde lecture n'est possible que lorsque l'extrait est terminé ou arrêté). Parfois, un jeu peut vouloir faire sonner un jeu plusieurs fois sans attendre la fin de chaque lecture (collecte de pièces dans un jeu, etc.). Pour ce faire, la classe AudioClip dispose d'une méthode playAsSFX(). Étant donné que plusieurs lectures peuvent avoir lieu simultanément, la lecture de playAsSFX() n'est pas associée à l'AudioClip en mode 1:1. Par conséquent, la lecture ne peut pas être arrêtée, mise en pause ni interrogée pour connaître l'état. La lecture en boucle est également désactivée, car il n'existe aucun moyen d'arrêter la lecture en boucle d'un son émis 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);
}
}

État d'arrêt, de pause et de requête – Les autres fonctions sont assez simples et ne nécessitent 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'aide 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 bon point de départ, 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 de devenir un véritable jeu HTML5, sans conditions.

Performances

Un autre aspect qui m'inquiétait à propos d'un port JavaScript était les performances. Après avoir terminé la v1 de mon port, j'ai constaté que tout fonctionnait correctement sur mon ordinateur de bureau quatre cœurs. Malheureusement, les choses n'étaient pas correctes sur un netbook ou un Chromebook. Dans ce cas, le profileur Chrome m'a permis de gagner du temps en me montrant exactement où se consacraient le temps de tous mes programmes. Mon expérience a mis en évidence l'importance du profilage avant d'effectuer toute optimisation. Je m'attendais à ce que la physique Box2D ou le code de rendu soit une source de ralentissement majeure. Cependant, la majeure partie de mon temps était consacrée à la fonction Matrix.clone(). Étant donné le caractère mathématique de mon jeu, je savais que j'allais souvent créer et cloner des matrices, mais je ne m'attendais pas à ce que cela crée un goulot d'étranglement. Au final, un changement très simple a permis au jeu de diviser par plus de trois l'utilisation du processeur, en passant de 6 à 7% du processeur sur mon ordinateur de bureau à 2%. Les développeurs JavaScript savent peut-être que c'est bien connu, mais en tant que développeur C++, ce problème m'a surpris, alors je vais entrer dans les détails. En fait, ma classe matricielle d'origine était une matrice 3x3: un tableau à 3 éléments, chacun contenant un tableau de 3 éléments. Malheureusement, au moment de cloner la matrice, j'ai dû créer quatre tableaux. La seule modification à apporter a été de déplacer ces données dans un seul tableau à neuf éléments et de mettre à jour mes calculs en conséquence. Ce changement unique était à l'origine de la multiplication par trois de la réduction du processeur que j'ai constatée. Après ce changement, mes performances étaient acceptables sur tous mes appareils de test.

Optimisation accrue

Bien que mes performances soient acceptables, je remarquais 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 signifiait que chaque image n'avait que 16 ms pour dessiner. Malheureusement, lorsque la récupération de mémoire était lancée sur un ordinateur plus lent, elle prenait parfois environ 10 ms. Cela a entraîné un stuttering de quelques secondes, car le jeu a nécessité presque 16 ms pour dessiner un frame complet. Pour mieux comprendre pourquoi je générais autant de déchets, j'ai utilisé le profileur de segments de mémoire de Chrome. Au désespoir, il s'est avéré que la grande majorité des déchets (plus de 70%) étaient générés par Box2D. L'élimination des déchets en JavaScript n'est pas une mince affaire, et il n'était pas question de réécrire Box2D. J'ai donc réalisé que j'avais pris un virage. Heureusement, j'ai encore eu l'une des plus vieilles astuces du livre à ma disposition: lorsqu'on ne peut pas atteindre 60 ips, utiliser une fréquence de 30 ips. Il est assez convenu qu'une fréquence constante de 30 images par seconde est bien meilleure qu'une fréquence irrégulière de 60 images par seconde. En fait, je n'ai toujours pas reçu de réclamation ni de commentaire indiquant que le jeu fonctionne à 30 images par seconde (il est très difficile de le dire si vous ne comparez les deux versions côte à côte). Avec ces 16 ms supplémentaires par frame, j'avais encore beaucoup de temps pour afficher le frame, même dans le cas d'une récupération de mémoire brute. Même si l'exécution à 30 FPS n'est pas explicitement activée par l'API de synchronisation que j'utilisais (l'excellent requestAnimationFrame de WebKit), elle peut être effectuée de manière très simple. Bien que cela ne soit peut-être pas aussi élégant qu'une API explicite, il est possible d'atteindre une fréquence de 30 FPS en sachant que l'intervalle de RequestAnimationFrame est aligné sur le VSYNC de l'écran (généralement 60 FPS). Cela signifie que nous devons simplement ignorer tous les autres rappels. En résumé, 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 le VSYNC de l'ordinateur n'est pas déjà à 30 FPS ou inférieur à 30 FPS au démarrage, et dans ce cas, désactivez cette option. Cependant, je ne l'ai pas encore vu dans les configurations d'ordinateur portable ou de bureau que j'ai testées.

Distribution et monétisation

Un dernier aspect qui m'a surpris à propos du port Chrome de Bouncy Mouse est la monétisation. En participant à ce projet, j'ai imaginé les jeux HTML5 comme une expérience intéressante pour apprendre les technologies émergentes. Ce que je n'avais pas réalisé, c'est que le port toucherait une très large audience et présenterait un potentiel de monétisation important.

Bouncy Mouse a été lancée fin octobre sur le Chrome Web Store. Le lancement sur le Chrome Web Store m'a permis d'exploiter un système existant pour améliorer la visibilité, l'engagement de la communauté, les classements et d'autres fonctionnalités auxquelles j'avais l'habitude sur les plates-formes mobiles. Ce qui m'a surpris, c'est l'ampleur de l'audience du magasin. Dans le mois suivant la publication de l'application, j'avais atteint près de 400 000 installations et je bénéficiais déjà de l'engagement de la communauté (rapports de bugs, commentaires). Une autre chose qui m'a surpris est le potentiel de monétisation des applications Web.

Bouncy Mouse propose une méthode de monétisation simple : une bannière à côté du contenu du jeu. Cependant, étant donné la large portée du jeu, j'ai constaté que cette bannière pouvait générer d'importants revenus. Pendant son pic d'activité, l'application en a généré d'autres sur ma plate-forme la plus performante, Android. Cela s'explique notamment par le fait que les annonces AdSense plus grandes diffusées sur la version HTML5 génèrent des revenus par impression nettement plus élevés que les annonces AdMob de plus petite taille diffusées sur Android. De plus, la bannière est bien moins intrusive dans la version HTML5 que sur la version Android, ce qui offre une expérience de jeu plus claire. 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

Si les revenus générés par le jeu étaient nettement supérieurs à ceux attendus, il convient de noter que l'audience du Chrome Web Store reste inférieure à celle des plates-formes plus matures, telles que l'Android Market. Même si Bouncy Mouse a réussi à atteindre rapidement le neuvième jeu le plus populaire du Chrome Web Store, mais le taux d'arrivée sur le site a considérablement ralenti depuis la sortie initiale. Cela dit, le jeu continue de se développer de manière régulière, et j'ai hâte de voir ce que cette plate-forme deviendra.

Conclusion

Je dirais que le transfert de Bouncy Mouse vers Chrome s'est passé beaucoup plus facilement que prévu. À part quelques problèmes audio et de performances mineurs, j'ai constaté que Chrome était une plate-forme parfaitement adaptée à un jeu sur smartphone existant. J'encourage tous les développeurs qui hésitent à s'en servir à essayer l'IA générative. J'ai été très satisfait du processus de portage ainsi que du nouveau public de gamers auquel un jeu HTML5 m'a permis d'attirer. N'hésitez pas à me contacter par e-mail si vous avez des questions. Ou laissez-nous un commentaire ci-dessous, j'essaierai de vérifier régulièrement.