Case study - Bouncy Mouse

Introduzione

Mouse rimbalzante

Dopo aver pubblicato Bouncy Mouse su iOS e Android alla fine dello scorso anno, ho imparato alcune lezioni molto importanti. Uno di loro fondamentale è che non è facile introdursi in un mercato consolidato. Nel mercato degli iPhone completamente saturo, acquisirsi è stato molto difficile; sul mercato Android meno saturo, il progresso è stato più facile, ma non ancora facile. Data questa esperienza, ho visto un'opportunità interessante sul Chrome Web Store. Anche se il Web Store non è affatto vuoto, il suo catalogo di giochi basati su HTML5 di alta qualità sta solo iniziando a crescere fino a raggiungere la maturità. Per un nuovo sviluppatore di app, ciò significa che rendere i grafici di ranking e ottenere visibilità è molto più facile. Tenendo conto di questa opportunità, ho deciso di trasferire Bouncy Mouse in HTML5 nella speranza di poter offrire la mia ultima esperienza di gameplay a una nuova ed entusiasmante base utenti. In questo case study, parlerò un po' del processo generale di portabilità di Bouncy Mouse in HTML5, poi approfondirò tre aree che si sono rivelate interessanti: audio, prestazioni e monetizzazione.

Trasferimento di un gioco C++ a HTML5

Bouncy Mouse è attualmente disponibile su Android(C++), iOS (C++), Windows Phone 7 (C#) e Chrome (JavaScript). A volte ciò ti chiede: come si fa a scrivere un gioco che possa essere facilmente trasferito su più piattaforme? Ho la sensazione che le persone sperano in qualche punto magico da usare per raggiungere questo livello di portabilità senza ricorrere a una manovra. Purtroppo, non credo che esista ancora una soluzione del genere (la cosa più vicina è probabilmente il framework PlayN di Google o il motore Unity, ma nessuna di queste soluzioni soddisfa tutti gli obiettivi che mi interessava). Il mio approccio è stato, in effetti, un trasferimento di mano. Prima ho scritto la versione per iOS/Android in C++, quindi ho trasferito questo codice in ogni nuova piattaforma. Anche se il lavoro richiede molto lavoro, ciascuna delle versioni WP7 e Chrome non ha richiesto più di due settimane. La domanda è: si può fare qualcosa per rendere un codebase facilmente portatile? Due cose che ho fatto mi sono state d'aiuto:

Mantieni il codebase piccolo

Anche se può sembrare ovvio, è davvero il motivo principale per cui sono riuscito a trasferire il gioco così rapidamente. Il codice client di Bouncy Mouse contiene solo circa 7.000 righe di C++. 7.000 righe di codice non sono niente, ma è abbastanza piccolo da essere gestibile. Entrambe le versioni C# e JavaScript del codice client avevano all'incirca le stesse dimensioni. Mantenere piccolo il mio codebase equivaleva a due pratiche chiave: non scrivere codice in eccesso e fai il più possibile nella pre-elaborazione del codice (non in runtime). Non scrivere codice in eccesso può sembrare ovvio, ma è una cosa per cui litigo sempre con me stesso. Spesso ho l'impulso di scrivere una classe/una funzione di supporto per qualsiasi cosa possa essere presa in considerazione in un assistente. Tuttavia, a meno che tu non abbia intenzione di utilizzare un helper più volte, di solito finirà per esaurire il codice. Con Bouncy Mouse, stavo attento a non scrivere mai un aiutante, a meno che non l'avrei usato almeno tre volte. Quando ho scritto un corso di supporto, cercavo di renderlo pulito, portatile e riutilizzabile per i miei progetti futuri. D'altra parte, quando scrivevo codice solo per Bouncy Mouse, con la bassa probabilità di riutilizzarlo, il mio obiettivo era svolgere l'attività di programmazione nel modo più semplice e rapido possibile, anche se questo non era il modo più "migliore" per scrivere il codice. La seconda e più importante parte di una piccola base di codice è stata quella di inserire il più possibile i passaggi per la pre-elaborazione. Se puoi svolgere un'attività di runtime e spostarla in un'attività di pre-elaborazione, non solo il gioco verrà eseguito più velocemente, ma non dovrai trasferire il codice su ogni nuova piattaforma. Per fare un esempio, in origine memorizzavo i dati della geometria del livello in un formato non elaborato, assemblando i buffer vertici OpenGL/WebGL effettivi in fase di esecuzione. Questa operazione ha richiesto un po' di configurazione e alcune centinaia di righe di codice di runtime. In seguito, ho spostato questo codice in una fase di pre-elaborazione, scrivendo i buffer vertici OpenGL/WebGL completamente compressi al momento della compilazione. La quantità effettiva di codice era praticamente la stessa, ma quelle poche centinaia di righe erano state spostate in una fase di pre-elaborazione, il che significa che non dovevo mai portarle su nessuna nuova piattaforma. In Bouncy Mouse esistono tantissimi esempi e ciò che è possibile varia da un gioco all'altro, ma tieni d'occhio ciò che non deve avvenire in fase di esecuzione.

Non assumere dipendenze di cui non hai bisogno

Un altro motivo per cui Bouncy Mouse è facile da trasferire è che non ha quasi nessuna dipendenza. Il grafico seguente riassume le principali dipendenze di libreria di Bouncy Mouse per piattaforma:

Android iOS HTML5 WP7
Elementi grafici OpenGL ES OpenGL ES WebGL XNA
Audio OpenSL ES OpenAL Audio web XNA
Fisica Box2D Box2D Box2D.js Box2D.xna

Ci siamo quasi. Non sono state utilizzate grandi librerie di terze parti ad eccezione di Box2D, che è portabile su tutte le piattaforme. Per la grafica, sia WebGL che XNA mappano quasi 1:1 con OpenGL, quindi non si è trattato di un grosso problema. Solo nella zona del suono le librerie effettive erano diverse. Tuttavia, il codice audio in Bouncy Mouse è piccolo (circa un centinaio di righe di codice specifico per la piattaforma), quindi non si trattava di un grosso problema. Mantenere Bouncy Mouse libero da grandi librerie non portatili significa che la logica del codice di runtime può essere quasi la stessa tra le versioni (nonostante la modifica del linguaggio). Inoltre, ci permette di evitare di essere vincolati a una catena di utensili non portatile. Mi è stato chiesto se la programmazione con OpenGL/WebGL causa direttamente una maggiore complessità rispetto all'utilizzo di una libreria come Cocos2D o Unity (esistono anche alcuni assistenti WebGL). Anzi, credo proprio il contrario. La maggior parte dei giochi per cellulari / HTML5 (almeno quelli come Bouncy Mouse) sono molto semplici. Nella maggior parte dei casi, il gioco disegna solo qualche sprite e forse qualche geometria strutturata. La somma totale del codice specifico per OpenGL in Bouncy Mouse è probabilmente inferiore a 1000 righe. Sono sorpreso se l'utilizzo di una libreria di supporto ridurrebbe effettivamente questo numero. Anche se si dimezzasse questo numero, avrei bisogno di molto tempo per imparare nuove librerie/strumenti solo per risparmiare 500 righe di codice. Inoltre, devo ancora trovare una libreria di supporto portatile su tutte le piattaforme che mi interessano, quindi prendere questa dipendenza danneggerebbe in modo significativo la portabilità. Se stessi scrivendo un gioco 3D che necessitava di mappe luminose, LOD dinamico, animazione con skin e così via, la mia risposta sarebbe certamente cambiata. In questo caso, riinventerei la ruota per provare a codificare manualmente il mio intero motore rispetto a OpenGL. La maggior parte dei giochi per dispositivi mobili/HTML5 non rientra (ancora) in questa categoria, quindi non c'è bisogno di complicare le cose prima che sia necessario.

Non sottovalutare le somiglianze tra le lingue

Un ultimo trucco che ha fatto risparmiare molto tempo durante il trasferimento del mio codebase C++ in un nuovo linguaggio è stata la consapevolezza che la maggior parte del codice è quasi identica in ciascun linguaggio. Anche se alcuni elementi chiave possono cambiare, questi sono molto meno rispetto a cose che non cambiano. Infatti, per molte funzioni, passare da C++ a JavaScript implicava semplicemente l'esecuzione di alcune sostituzioni di espressioni regolari sul mio codebase C++.

Conclusioni del trasferimento

Questo è tutto per il processo di portabilità. Nelle prossime sezioni parlerò di alcune sfide specifiche per HTML5, ma il messaggio principale è che, se il codice è semplice, il trasferimento sarà un piccolo grattacapo, non un incubo.

Audio

Un aspetto che ha causato problemi a me (e a tutti gli altri) è stato l'audio. Su iOS e Android sono disponibili numerose opzioni audio solide (OpenSL, OpenAL), ma nel mondo dell'HTML5 le cose sembrano più peculiari. Sebbene l'audio HTML5 sia disponibile, ho scoperto che presenta alcuni problemi determinanti quando viene utilizzato nei giochi. Anche con i browser più recenti, ho avuto spesso comportamenti strani. Ad esempio, sembra che Chrome abbia un limite al numero di elementi audio simultanei (origine) che puoi creare. Inoltre, anche quando viene riprodotto un suono, a volte viene inspiegabilmente distorto. Nel complesso, ero un po' preoccupata. Le ricerche online hanno rivelato che praticamente tutti hanno gli stessi problemi. La soluzione su cui sono arrivato inizialmente era un'API chiamata SoundManager2. Questa API utilizza l'audio HTML5, se disponibile, ricorrendo a Flash in situazioni difficili. Sebbene questa soluzione funzionasse, era ancora con bug e imprevedibile (poco meno dell'audio HTML5 puro). Una settimana dopo il lancio, ho parlato con alcune persone di Google che mi si sono rivolte all'API Web Audio di Webkit. All'inizio avevo pensato di usare questa API, ma l'avevo evitata a causa della complessità inutile (per me) che l'API sembrava avere. Volevo solo farti ascoltare alcuni suoni: con l'audio HTML5 questo equivale a un paio di righe di JavaScript. Tuttavia, nel mio breve sguardo a Web Audio, sono rimasto colpito dalle sue enormi specifiche (70 pagine), dalla piccola quantità di campioni sul web (tipica di una nuova API) e dall'omissione di una funzione di riproduzione, pausa o stop in qualsiasi punto delle specifiche. Con le garanzie di Google che le mie preoccupazioni non erano fondate, ho riesaminato l'API. Dopo aver esaminato altri esempi e aver effettuato ulteriori ricerche, ho scoperto che Google aveva ragione, l'API è in grado di soddisfare le mie esigenze e può farlo senza i bug che affliggono le altre API. Particolarmente utile è l'articolo Getting Started with Web Audio API, che è ideale per chi vuole acquisire una comprensione più approfondita dell'API. Il mio vero problema è che, anche dopo aver compreso e usato l'API, mi sembra che l'API non sia progettata per "riprodurre solo alcuni suoni". Per risolvere questo problema, ho scritto una piccola lezione di supporto che mi ha permesso di usare l'API nel modo che volevo: riprodurre, mettere in pausa, interrompere ed eseguire query sullo stato di un suono. Ho chiamato questa classe di supporto AudioClip. L'origine completa è disponibile su GitHub con la licenza Apache 2.0. Ne parleremo in dettaglio della classe di seguito. Prima però, alcune informazioni sull'API Web Audio:

Grafici audio web

La prima cosa che rende l'API Web Audio più complessa (e più potente) dell'elemento audio HTML5 è la sua capacità di elaborare / mescolare l'audio prima di inviarlo all'utente. Per quanto sia potente, il fatto che qualsiasi riproduzione audio implichi un grafico rende le cose un po' più complesse in scenari semplici. Per illustrare la potenza dell'API Web Audio, considera il seguente grafico:

Grafico audio web di base
Grafico audio web di base

Anche se l'esempio riportato sopra mostra la potenza dell'API Web Audio, nel mio scenario non ne avevo bisogno. Volevo solo sentire un suono. Sebbene a questo scopo sia comunque necessario un grafico, il grafico è molto semplice.

I grafici possono essere semplici

La prima cosa che rende l'API Web Audio più complessa (e più potente) dell'elemento audio HTML5 è la sua capacità di elaborare / mescolare l'audio prima di inviarlo all'utente. Per quanto sia potente, il fatto che qualsiasi riproduzione audio implichi un grafico rende le cose un po' più complesse in scenari semplici. Per illustrare la potenza dell'API Web Audio, considera il seguente grafico:

Grafico audio web trivial
Trivial Web Audio Graph

Il grafico qui sopra può svolgere tutte le attività necessarie per riprodurre, mettere in pausa o interrompere un suono.

Ma non dobbiamo preoccuparci del grafico

Anche se è bello comprendere il grafico, non voglio avere a che fare ogni volta che riproduco un suono. Ho quindi scritto una semplice classe wrapper "AudioClip". Questa classe gestisce il grafico internamente, ma presenta un'API molto più semplice rivolta all'utente.

AudioClip
AudioClip

Questo corso non è altro che un grafico audio web e uno stato di supporto, ma mi consente di utilizzare un codice molto più semplice rispetto a quanto accadrebbe se dovessi creare un grafico audio web per riprodurre ogni suono.

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

// Later
sound.play();

Dettagli implementazione

Diamo una rapida occhiata al codice della classe helper: Costruttore: il costruttore gestisce il caricamento dei dati audio tramite un XHR. Sebbene non venga mostrato qui (per semplificare l'esempio), è possibile utilizzare anche un elemento audio HTML5 come nodo di origine. Ciò è particolarmente utile per campioni di grandi dimensioni. Tieni presente che l'API Web Audio richiede il recupero di questi dati come "arraybuffer". Una volta ricevuti i dati, creiamo un buffer Web Audio a partire da questi dati (decodificandoli dal suo formato originale in un formato PCM di runtime).

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

Play – La riproduzione del nostro suono richiede due passaggi: la configurazione del grafico di riproduzione e il richiamo di una versione di “noteOn” sulla sorgente del grafico. Un'origine può essere riprodotta solo una volta, quindi dobbiamo ricreare l'origine o il grafico ogni volta che riproduciamo. Gran parte della complessità di questa funzione deriva dai requisiti necessari per riprendere la riproduzione di un clip in pausa (this.pauseTime_ > 0). Per riprendere la riproduzione di un clip in pausa, utilizziamo noteGrainOn, che consente la riproduzione di una regione secondaria di un buffer. Sfortunatamente, noteGrainOn non interagisce con il loop nel modo desiderato per questo scenario (eseguirà il loop della sottoregione, non dell'intero buffer). Di conseguenza, dobbiamo risolvere il problema riproducendo la parte rimanente del clip con noteGrainOn, quindi riavviando il clip dall'inizio con il loop attivato.

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

Riproduci come effetto sonoro - La funzione di riproduzione riportata sopra non consente di riprodurre più volte il clip audio con una sovrapposizione (è possibile riprodurre una seconda riproduzione solo quando il clip è terminato o interrotto). A volte potrebbe capitare che un gioco voglia riprodurre un suono più volte senza attendere il completamento di ogni riproduzione (raccolta di monete in un gioco e così via). Per attivare questa funzionalità, la classe AudioClip ha un metodo playAsSFX(). Dato che possono verificarsi più riproduzioni contemporaneamente, la riproduzione da playAsSFX() non viene associata all'audio clip 1:1. Pertanto, la riproduzione non può essere interrotta, messa in pausa o oggetto di query sullo stato. Anche la riproduzione in loop è disattivata, in quanto non sarebbe possibile interrompere la riproduzione in loop del suono in questo modo.

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

Stato di interruzione, pausa e query: le altre funzioni sono piuttosto semplici e non richiedono molte spiegazioni:

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

Conclusione audio

Spero che questa lezione di supporto sia utile agli sviluppatori che hanno difficoltà con i miei stessi problemi audio. Inoltre, una lezione come questa sembra un buon punto di partenza anche se devi aggiungere alcune delle funzionalità più potenti dell'API Web Audio. In ogni caso, questa soluzione ha soddisfatto le esigenze di Bouncy Mouse e ha permesso al gioco di diventare un vero gioco HTML5, senza vincoli!

Rendimento

Un'altra area che mi preoccupava per quanto riguarda la porta JavaScript sono state le prestazioni. Dopo aver terminato la versione v1 della porta, ho scoperto che tutto funzionava bene sul mio desktop quad-core. Purtroppo, le cose andavano un po' meno che a posto su un netbook o un Chromebook. In questo caso, il profiler di Chrome mi ha fatto risparmiare mostrandomi esattamente dove veniva trascorso tutto il tempo dei miei programmi. La mia esperienza evidenzia l'importanza della profilazione prima di qualsiasi ottimizzazione. Mi aspettavo che la fisica di Box2D o forse il codice di rendering fosse una delle principali fonti di rallentamento; tuttavia, la maggior parte del mio tempo è stata effettivamente dedicata alla funzione Matrix.clone(). Data la natura del gioco impegnativa in matematica, sapevo di dover creare/clonare molte matrici, ma non mi aspettavo mai che questo fosse un collo di bottiglia. Alla fine, si è scoperto che una modifica molto semplice ha permesso al gioco di ridurre l'utilizzo della CPU di oltre 3 volte, passando dal 6-7% della CPU sul desktop al 2%. Forse si tratta di una conoscenza comune degli sviluppatori JavaScript, ma in qualità di sviluppatore C++ questo problema mi ha sorpreso, quindi entrerò in dettaglio. Fondamentalmente, la mia classe di matrice originale era una matrice 3x3: un array di 3 elementi, ogni elemento contenente un array di 3 elementi. Sfortunatamente, ciò significava che quando è stato il momento di clonare la matrice, ho dovuto creare 4 nuovi array. L'unica modifica che dovevo apportare è stato spostare questi dati in un singolo array di 9 elementi e aggiornare i calcoli matematici di conseguenza. L'unica modifica è stata interamente responsabile di questa riduzione di 3 volte della CPU che ho riscontrato e, dopo questa modifica, le mie prestazioni erano accettabili su tutti i miei dispositivi di test.

Maggiore ottimizzazione

Anche se la mia performance era accettabile, continuavo a riscontrare qualche piccolo problema. Dopo un po' più di profilazione, mi sono resa conto che era dovuto alla raccolta di rifiuti di JavaScript. La mia app girava a 60 fps, il che significava che ogni fotogramma aveva solo 16 ms da disegnare. Purtroppo, quando la garbage collection si avviava su un computer più lento, a volte consumava circa 10 ms. Ciò ha causato un stuttering per pochi secondi, poiché il gioco ha richiesto quasi tutti i 16 ms per disegnare un frame completo. Per capire meglio perché generavo così tanti rifiuti, ho usato il profiler heap di Chrome. Con grande disperazione, ho scoperto che la stragrande maggioranza della spazzatura (oltre il 70%) era generata da Box2D. Eliminare la spazzatura in JavaScript è un'attività complicata e riscrivere Box2D era fuori discussione, quindi mi sono resa conto di essermi caduta in un angolo. Fortunatamente, ho ancora avuto uno dei trucchi più antichi del libro a mia disposizione: quando non puoi raggiungere i 60 fps, corri a 30 fps. È abbastanza ben d'accordo sul fatto che una corsa a 30 fps costante sia molto meglio che a una frequenza di 60 f/s. Infatti non ho ancora ricevuto reclami o commenti relativi al fatto che il gioco funzioni a 30 fps (è davvero difficile dirlo a meno che non confronti le due versioni una accanto all'altra). Questi 16 ms in più per frame hanno fatto sì che, anche in caso di pessima garbage collection, avevo tutto il tempo per eseguire il rendering del frame. Sebbene l'esecuzione a 30 fps non sia esplicitamente attivata dall'API di temporizzazione che stavo utilizzando (l'eccellente requestAnimationFrame di WebKit), si può fare in modo molto banale. Anche se non è così elegante come un'API esplicita, 30 fps si può raggiungere sapendo che l'intervallo di RequestAnimationFrame è allineato alla VSYNC del monitor (solitamente 60 fps). Ciò significa che dobbiamo ignorare tutti gli altri callback. In pratica, se hai un callback "Tick" che viene chiamato ogni volta che viene attivato "RequestAnimationFrame", puoi procedere nel seguente modo:

var skip = false;

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

// OTHER CODE
}

Se vuoi essere più prudente, controlla che la VSYNC del computer non sia già a 30 fps o inferiore all'avvio e disattiva in questo caso l'opzione per saltare l'annuncio. Tuttavia, non l'ho ancora notato nelle configurazioni di computer/laptop che ho testato.

Distribuzione e monetizzazione

Un'ultima area che mi ha sorpreso del trasferimento di Bouncy Mouse su Chrome è stata la monetizzazione. Partendo da questo progetto, ho pensato che i giochi HTML5 fossero un esperimento interessante per imparare le tecnologie emergenti. Quello che non avevo capito era che il trasferimento avrebbe raggiunto un pubblico molto ampio e avrebbe avuto un potenziale significativo di monetizzazione.

Bouncy Mouse è stato lanciato alla fine di ottobre sul Chrome Web Store. Con il rilascio sul Chrome Web Store ho potuto sfruttare un sistema esistente per la rilevabilità, il coinvolgimento della community, i ranking e altre funzionalità a cui mi ero abituato sulle piattaforme mobile. Quello che mi ha sorpreso è stata quanto fosse ampia la copertura del negozio. Entro un mese dal rilascio ho raggiunto quasi 400.000 installazioni e stavo già beneficiando del coinvolgimento della community (segnalazioni di bug, feedback). Un'altra cosa che mi ha sorpreso è il potenziale di monetizzazione di un'app web.

Bouncy Mouse utilizza un semplice metodo di monetizzazione: un annuncio banner accanto ai contenuti del gioco. Tuttavia, data l'ampia copertura del gioco, ho scoperto che questo annuncio banner è stato in grado di generare entrate significative e, durante il periodo di picco, l'app ha generato entrate paragonabili alla mia piattaforma di maggior successo, Android. Un fattore che contribuisce a ciò è che gli annunci AdSense più grandi mostrati nella versione HTML5 generano entrate per impressione significativamente maggiori rispetto agli annunci AdMob più piccoli pubblicati su Android. Non solo, ma l'annuncio banner nella versione HTML5 è molto meno invadente rispetto alla versione Android, consentendo un'esperienza di gameplay più pulita. Nel complesso sono rimasto piacevolmente sorpreso da questo risultato.

Utili normalizzati nel tempo.
Utili normalizzati nel tempo

Anche se gli utili generati dal gioco sono stati molto migliori del previsto, vale la pena notare che la copertura del Chrome Web Store è ancora inferiore a quella di piattaforme più mature come Android Market. Sebbene Bouncy Mouse sia riuscito a raggiungere rapidamente il gioco più popolare n. 9 sul Chrome Web Store, il tasso di nuovi utenti che hanno visitato il sito è diminuito notevolmente dopo il rilascio iniziale. Detto questo, il gioco continua a crescere in modo costante e non vedo l'ora di vedere come si sviluppa la piattaforma.

Conclusione

Direi che il trasferimento di Bouncy Mouse in Chrome è stato molto più fluido di quanto mi aspettassi. A parte alcuni piccoli problemi audio e di prestazioni, ho scoperto che Chrome era una piattaforma perfettamente adatta per un gioco per smartphone esistente. Consiglio sempre a tutti gli sviluppatori che si sono resi conto di questa esperienza di provarla. Sono stato molto soddisfatto sia del processo di trasferimento sia del nuovo pubblico di gaming che mi ha messo in contatto con un gioco HTML5. Per eventuali domande, non esitare a inviarmi un'email. In alternativa, lascia un commento qui sotto perché cercherò di controllare regolarmente questi aspetti.