Introduzione
Dopo aver pubblicato Bouncy Mouse su iOS e Android alla fine dello scorso anno, ho imparato alcune lezioni molto importanti. Tra le principali, è emerso che è difficile entrare in un mercato consolidato. Nel mercato saturo degli iPhone, è stato molto difficile ottenere visibilità. In Android Marketplace, meno saturo, i progressi sono stati più facili, ma non semplici. In base a questa esperienza, ho visto un'opportunità interessante nel Chrome Web Store. Anche se il Web Store non è affatto vuoto, il suo catalogo di giochi basati su HTML5 di alta qualità sta appena iniziando a maturare. Per un nuovo sviluppatore di app, questo significa che è molto più facile scalare le classifiche e ottenere visibilità. Tenendo presente questa opportunità, ho iniziato a eseguire il porting di Bouncy Mouse in HTML5 con la speranza di poter offrire la mia ultima esperienza di gioco a una nuova base di utenti. In questo caso studio, parlerò un po' della procedura generale di porting di Bouncy Mouse in HTML5, per poi approfondire tre aree che si sono rivelate interessanti: audio, rendimento e monetizzazione.
Portare un gioco C++ su HTML5
Bouncy Mouse è attualmente disponibile su Android(C++), iOS (C++), Windows Phone 7 (C#) e Chrome (Javascript). A volte ci si chiede: come si scrive un gioco che può essere facilmente portato su più piattaforme? Ho la sensazione che le persone sperino in una soluzione magica che possa essere utilizzata per raggiungere questo livello di portabilità senza ricorrere a una porta di ricarica manuale. Purtroppo, non so se una soluzione del genere esista già (la più simile è probabilmente il framework PlayN di Google o il motore Unity, ma nessuno dei due soddisfa tutti i target che mi interessano). Il mio approccio era, infatti, una porta manuale. Ho scritto prima la versione per iOS/Android in C++, poi ho portato questo codice su ogni nuova piattaforma. Sembra un sacco di lavoro, ma le versioni per WP7 e Chrome non hanno richiesto più di due settimane. Ora la domanda è: si può fare qualcosa per rendere una base di codice facilmente trasferibile? Ho fatto un paio di cose che mi hanno aiutato:
Mantieni la base di codice piccola
Anche se può sembrare ovvio, è il motivo principale per cui ho potuto eseguire il porting del gioco così rapidamente. Il codice client di Bouncy Mouse è composto da circa 7000 righe di C++. 7000 righe di codice non sono poche, ma sono abbastanza piccole da essere gestibili. Sia le versioni C# che quelle JavaScript del codice client hanno avuto dimensioni approssimativamente uguali. Mantenere la base di codice piccola si è tradotto in due pratiche chiave: non scrivere codice in eccesso e fare il più possibile nel codice di pre-elaborazione (non in fase di esecuzione). Non scrivere codice in eccesso può sembrare ovvio, ma è una cosa che mi faccio sempre un gran problema. Spesso ho la necessità di scrivere una classe/funzione di supporto per qualsiasi cosa possa essere presa in considerazione in un helper. Tuttavia, a meno che tu non preveda di utilizzare un helper più volte, di solito il codice ne risulta appesantito. Con Bouncy Mouse, facevo attenzione a non scrivere mai un helper, a meno che non lo avessi intenzione di utilizzare almeno tre volte. Quando ho scritto una classe di assistenza, ho cercato di renderla chiara, portabile e riutilizzabile per i miei progetti futuri. D'altra parte, quando scrivo codice solo per Bouncy Mouse, con scarsa probabilità di riutilizzo, il mio obiettivo è completare l'attività di codifica nel modo più semplice e rapido possibile, anche se non è il modo più "bello" per scrivere il codice. La seconda e più importante parte per mantenere la base di codice piccola è stata quella di spingere il più possibile i passaggi di preelaborazione. Se puoi spostare un'attività di runtime in un'attività di preelaborazione, il tuo gioco non solo funzionerà più velocemente, ma non dovrai eseguire il porting del codice su ogni nuova piattaforma. Per fare un esempio, inizialmente ho archiviato i dati della geometria del livello in un formato abbastanza non elaborato, assemblando gli attuali buffer di vertici OpenGL/WebGL 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 un passaggio di preelaborazione, scrivendo gli buffer di vertici OpenGL/WebGL completamente pacchettizzati in fase di compilazione. La quantità effettiva di codice era all'incirca la stessa, ma quelle poche centinaia di righe erano state spostate in un passaggio di preelaborazione, il che significa che non ho mai dovuto eseguirne il porting su nuove piattaforme. Esistono tantissimi esempi di questo in Bouncy Mouse e le possibilità variano da un gioco all'altro, ma tieni d'occhio tutto ciò che non deve accadere in fase di esecuzione.
Non utilizzare dipendenze non necessarie
Un altro motivo per cui Bouncy Mouse è facile da eseguire il porting è che non ha quasi dipendenze. Il seguente grafico riassume le principali dipendenze delle librerie di Bouncy Mouse per piattaforma:
È tutto qui. Non sono state utilizzate librerie di terze parti di grandi dimensioni, ad eccezione di Box2D, che è portabile su tutte le piattaforme. Per la grafica, sia WebGL che XNA hanno una mappatura quasi 1:1 con OpenGL, quindi non si è trattato di un problema grave. Solo nell'area audio le librerie effettive erano diverse. Tuttavia, il codice audio di Bouncy Mouse è ridotto (circa cento righe di codice specifico per la piattaforma), quindi non si è trattato di un problema enorme. Mantenere Bouncy Mouse privo di librerie non portabili di grandi dimensioni significa che la logica del codice di runtime può essere quasi la stessa tra le versioni (nonostante il cambio di linguaggio). Inoltre, ci evita di rimanere bloccati in una catena di strumenti non portatile. Mi è stato chiesto se la programmazione diretta con OpenGL/WebGL comporta una maggiore complessità rispetto all'utilizzo di una libreria come Cocos2D o Unity (esistono anche alcuni helper WebGL). In realtà, credo esattamente il contrario. La maggior parte dei giochi per smartphone / HTML5 (almeno quelli come Bouncy Mouse) sono molto semplici. Nella maggior parte dei casi, il gioco disegna solo alcuni sprite e forse un po' di geometria con texture. La somma totale del codice specifico di OpenGL in Bouncy Mouse è probabilmente inferiore a 1000 righe. Mi sorprenderei se l'utilizzo di una libreria di supporto riducesse effettivamente questo numero. Anche se dimezzassi questo numero, dovrei dedicare molto tempo all'apprendimento di nuove librerie/nuovi strumenti solo per risparmiare 500 righe di codice. Inoltre, non ho ancora trovato una libreria di supporto portabile su tutte le piattaforme che mi interessano, quindi l'adozione di una dipendenza del genere danneggerebbe notevolmente la portabilità. Se dovessi scrivere un gioco 3D che richiede lightmap, LOD dinamico, animazione con skin e così via, la mia risposta cambierebbe sicuramente. In questo caso, dovrei reinventare la ruota per provare a codificare manualmente l'intero motore per OpenGL. Il mio punto è che la maggior parte dei giochi mobile/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 mi ha fatto risparmiare molto tempo durante il porting del mio codice C++ a un nuovo linguaggio è stato capire che la maggior parte del codice è quasi identica in ogni linguaggio. Anche se alcuni elementi chiave possono cambiare, sono molto meno di quelli che non cambiano. Infatti, per molte funzioni, il passaggio da C++ a JavaScript ha comportato semplicemente l'esecuzione di alcune sostituzioni di espressioni regolari nel mio codice di base C++.
Conclusioni sul trasferimento
Questo è praticamente tutto per il processo di porting. Nelle prossime sezioni tratterò alcune sfide specifiche di HTML5, ma il messaggio principale è che, se mantieni il codice semplice, il porting sarà un piccolo mal di testa, non un incubo.
Audio
Un aspetto che mi ha causato (e apparentemente a tutti gli altri) qualche problema è l'audio. Su iOS e Android sono disponibili diverse opzioni audio solide (OpenSL, OpenAL), ma nel mondo di HTML5 le cose sembravano meno rosee. Sebbene l'audio HTML5 sia disponibile, ho riscontrato alcuni problemi che ne impediscono l'utilizzo nei giochi. Anche sui browser più recenti, ho riscontrato spesso comportamenti strani. Chrome, ad esempio, sembra avere un limite al numero di elementi audio (source) simultanei che puoi creare. Inoltre, anche quando l'audio veniva riprodotto, a volte risultava inspiegabilmente distorto. Nel complesso, ero un po' preoccupato. Da una ricerca online è emerso che quasi tutti hanno lo stesso problema. La soluzione che ho scelto inizialmente era un'API chiamata SoundManager2. Questa API utilizza HTML5 Audio, se disponibile, e passa a Flash in situazioni complicate. Sebbene questa soluzione funzionasse, presentava ancora dei bug ed era imprevedibile (meno rispetto all'audio HTML5 puro). Una settimana dopo il lancio, ho parlato con alcuni dei gentili collaboratori di Google, che mi hanno indicato l'API Web Audio di Webkit. Inizialmente avevo preso in considerazione l'utilizzo di questa API, ma l'ho evitata a causa della complessità non necessaria (per me) che sembrava avere. Volevo solo riprodurre alcuni suoni: con HTML5 Audio bastano un paio di righe di JavaScript. Tuttavia, nel mio breve sguardo a Web Audio, mi ha colpito la sua enorme specifica (70 pagine), la scarsa quantità di sample sul web (tipico di una nuova API) e l'omissione di una funzione "play", "pause" o "stop" in nessuna parte della specifica. Dopo aver ricevuto da Google la certezza che le mie preoccupazioni non erano fondate, ho esaminato di nuovo l'API. Dopo aver esaminato altri esempi e aver fatto qualche altra ricerca, ho scoperto che Google aveva ragione: l'API può sicuramente soddisfare le mie esigenze e può farlo senza i bug che affliggono le altre API. Particolarmente utile è l'articolo Introduzione all'API Web Audio, che è un ottimo punto di partenza se vuoi acquisire una conoscenza più approfondita dell'API. Il mio vero problema è che, anche dopo aver compreso e utilizzato l'API, mi sembra ancora un'API non progettata per "riprodurre solo alcuni suoni". Per ovviare a questo dubbio, ho scritto una piccola classe di supporto che mi ha permesso di utilizzare l'API esattamente come volevo: riprodurre, mettere in pausa, interrompere e eseguire query sullo stato di un suono. Ho chiamato questa classe di supporto AudioClip. Il codice sorgente completo è disponibile su GitHub sotto la licenza Apache 2.0 e di seguito parlerò dei dettagli della classe. Prima di tutto, ecco 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 / mischiare l'audio prima di eseguirlo per l'utente. Sebbene 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:
Sebbene l'esempio riportato sopra mostri la potenza dell'API Web Audio, non mi serviva la maggior parte di questa potenza nel mio scenario. Volevo solo riprodurre un suono. Sebbene sia comunque necessario un grafico, questo è 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 / mischiare l'audio prima di eseguirlo per l'utente. Sebbene 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:
Il grafico banale mostrato sopra può fare tutto il necessario per riprodurre, mettere in pausa o interrompere un suono.
Ma non preoccuparti nemmeno del grafico
È bello capire il grafico, ma non è qualcosa che voglio gestire ogni volta che riproduco un suono. Di conseguenza, ho scritto una semplice classe wrapper "AudioClip". Questa classe gestisce questo grafico internamente, ma presenta un'API rivolta agli utenti molto più semplice.
Questa classe non è altro che un grafico Web Audio e alcuni stati di supporto, ma mi consente di utilizzare un codice molto più semplice rispetto a quello che dovrei utilizzare per creare un grafico Web Audio per riprodurre ogni suono.
// At startup time
var sound = new AudioClip("ping.wav");
// Later
sound.play();
Dettagli sull'implementazione
Diamo un'occhiata al codice della classe di assistenza: Costruttore: il costruttore gestisce il caricamento dei dati audio utilizzando un XHR. Sebbene non sia mostrato qui (per semplificare l'esempio), un elemento Audio HTML5 potrebbe essere utilizzato anche come nodo di origine. Questo è particolarmente utile per i campioni di grandi dimensioni. Tieni presente che l'API Web Audio richiede di recuperare questi dati come "arraybuffer". Una volta ricevuti i dati, creiamo un buffer Web Audio da questi dati (decodificandolo dal 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();
}
Riproduci: la riproduzione dell'audio prevede due passaggi: la configurazione del grafico di riproduzione e l'attivazione di una versione di "noteOn" nell'origine del grafico. Un'origine può essere riprodotta una sola volta, quindi dobbiamo ricreare l'origine/il grafico ogni volta che riproduciamo.
La maggior parte della complessità di questa funzione deriva dai requisiti necessari per riprendere un clip in pausa (this.pauseTime_ > 0
). Per riprendere la riproduzione di un clip in pausa, utilizziamo noteGrainOn
, che consente di riprodurre una sottoregione di un buffer. Purtroppo, noteGrainOn
non interagisce con il looping nel modo desiderato per questo scenario (verrà eseguito il looping della sottoregione, non dell'intero buffer).
Pertanto, dobbiamo aggirare il problema riproducendo il resto del clip con noteGrainOn
, quindi riavviando il clip dall'inizio con il looping abilitato.
/**
* 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 il clip audio più volte con sovrapposizione (una seconda riproduzione è possibile solo al termine o all'interruzione del clip). A volte un gioco vuole riprodurre un suono molte volte senza attendere il completamento di ogni riproduzione (raccogliere monete in un gioco e così via). Per abilitare questa funzionalità, la classe AudioClip dispone di un metodo playAsSFX()
.
Poiché possono verificarsi più riproduzioni contemporaneamente, la riproduzione da playAsSFX()
non è vincolata 1:1 all'AudioClip. Pertanto, la riproduzione non può essere interrotta, messa in pausa o interrogata per lo stato. Anche il loop è disattivato, in quanto non sarebbe possibile interrompere un suono in loop riprodotto 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);
}
}
Interrompi, metti in pausa e stato delle query: le altre funzioni sono abbastanza 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 classe di assistenza sia utile agli sviluppatori che hanno gli stessi problemi di audio che ho riscontrato. Inoltre, un corso come questo sembra un punto di partenza ragionevole 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 consentito al gioco di essere un vero gioco HTML5, senza costi aggiuntivi.
Prestazioni
Un altro aspetto che mi preoccupava in merito a una porta JavaScript era il rendimento. Dopo aver completato la versione 1 della mia porta, ho scoperto che tutto funzionava correttamente sul mio computer desktop quad-core. Purtroppo, le cose non andavano benissimo su un netbook o un Chromebook. In questo caso, il profiler di Chrome mi ha salvato mostrandomi esattamente dove veniva speso il tempo di tutti i miei programmi.
La mia esperienza mette in evidenza l'importanza del profiling prima di eseguire qualsiasi ottimizzazione. Mi aspettavo che la fisica di Box2D o forse il codice di rendering fossero una fonte principale di rallentamento; tuttavia, la maggior parte del mio tempo veniva effettivamente spesa nella funzione Matrix.clone()
. Data la natura matematica del mio gioco, sapevo di aver creato/clonato molte matrici, ma non mi aspettavo che questo fosse il collo di bottiglia. Alla fine, è emerso che una modifica molto semplice ha consentito al gioco di ridurre l'utilizzo della CPU di oltre tre volte, passando dal 6-7% della CPU sul mio computer al 2%.
Forse è una conoscenza comune per gli sviluppatori JavaScript, ma come sviluppatore C++ questo problema mi ha sorpreso, quindi entrerò un po' più nei dettagli. Fondamentalmente, la mia classe di matrici originale era una matrice 3x3: un array di 3 elementi, ciascuno contenente un array di 3 elementi. Purtroppo, questo significava che quando è stato il momento di clonare la matrice, ho dovuto creare 4 nuovi array. L'unica modifica che ho dovuto apportare è stata spostare questi dati in un singolo array di 9 elementi e aggiornare la matematica di conseguenza. Questa singola modifica è stata interamente responsabile della riduzione della CPU di tre volte che ho riscontrato e, dopo questa modifica, il rendimento è stato accettabile su tutti i miei dispositivi di test.
Altre ottimizzazioni
Anche se il mio rendimento era accettabile, ho riscontrato alcuni piccoli problemi. Dopo un po' di profilazione, ho capito che il problema era dovuto alla raccolta dei rifiuti di JavaScript. La mia app era in esecuzione a 60 fps, il che significa che ogni frame aveva solo 16 ms per essere disegnato. Purtroppo, quando la raccolta dei rifiuti veniva attivata su una macchina più lenta, a volte consumava circa 10 ms. Ciò ha comportato uno scatto ogni pochi secondi, poiché il gioco richiedeva quasi tutti i 16 ms per disegnare un frame completo. Per capire meglio perché stavo generando così tanto spazzatura, ho utilizzato lo strumento di profilazione dell'heap di Chrome. Con mio grande sconforto, è emerso che la maggior parte dei dati inutilizzati (oltre il 70%) era generata da Box2D. Eliminare i dati non validi in JavaScript è un'operazione complicata e non era possibile riscrivere Box2D, quindi mi sono reso conto di avermi messo in un angolo. Fortunatamente, avevo ancora a disposizione uno dei trucchi più vecchi: se non riesci a raggiungere i 60 fps, esegui il rendering a 30 fps. È abbastanza noto che una frequenza di 30 fps costante è molto meglio di una frequenza di 60 fps con balzi. Infatti, non ho ancora ricevuto un reclamo o un commento in merito al fatto che il gioco funziona 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 significavano che, anche in caso di una raccolta dei rifiuti spiacevole, avevo ancora molto tempo per eseguire il rendering del frame. Sebbene l'esecuzione a 30 fps non sia esplicitamente abilitata dall'API di temporizzazione che stavo utilizzando (l'eccellente requestAnimationFrame di WebKit), può essere eseguita in modo molto semplice. Anche se forse non è elegante come un'API esplicita, è possibile ottenere 30 fps sapendo che l'intervallo di RequestAnimationFrame è allineato al VSYNC del monitor (di solito 60 fps). Ciò significa che dobbiamo ignorare tutti gli altri callback. In sostanza, 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
}
Per maggiore cautela, devi verificare che la sincronizzazione verticale del computer non sia già a 30 fps o meno all'avvio e disattivare il salto in questo caso. Tuttavia, non ho ancora riscontrato questo problema nelle configurazioni di computer/laptop che ho testato.
Distribuzione e monetizzazione
Un'ultima area che mi ha sorpreso della porta di Chrome di Bouncy Mouse è stata la monetizzazione. Quando ho iniziato questo progetto, immaginavo i giochi HTML5 come un esperimento interessante per imparare le tecnologie emergenti. Non avevo però compreso che la porta avrebbe raggiunto un pubblico molto ampio e avrebbe avuto un potenziale significativo per la monetizzazione.
Bouncy Mouse è stato lanciato a fine ottobre sul Chrome Web Store. Grazie al rilascio sul Chrome Web Store, ho potuto sfruttare un sistema esistente per la visibilità, il coinvolgimento della community, i ranking e altre funzionalità a cui ero abituato sulle piattaforme mobile. A sorprendermi è stata l'ampia copertura del negozio. Entro un mese dal lancio avevo raggiunto quasi quattrocentomila 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 ha 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 a quelle della mia piattaforma di maggior successo, Android. Un fattore che contribuisce a questo fenomeno è che gli annunci AdSense più grandi mostrati nella versione HTML5 generano entrate per impressione significativamente più elevate rispetto agli annunci AdMob più piccoli mostrati su Android. Non solo, ma l'annuncio banner nella versione HTML5 è molto meno invadente rispetto alla versione per Android, il che consente un'esperienza di gioco più chiara. Nel complesso, questo risultato mi ha molto sorpreso.

Sebbene gli utili del gioco siano 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. Anche se Bouncy Mouse è riuscito a scalare rapidamente la classifica fino a diventare il nono gioco più popolare sul Chrome Web Store, il tasso di nuovi utenti che visitano il sito è rallentato notevolmente dalla prima release. Detto questo, il gioco continua a registrare una crescita costante e non vedo l'ora di scoprire cosa diventerà la piattaforma.
Conclusione
Direi che il porting di Bouncy Mouse a Chrome è andato molto più liscio del previsto. A parte alcuni piccoli problemi di audio e prestazioni, ho riscontrato che Chrome è una piattaforma perfettamente adatta per un gioco per smartphone esistente. Incoraggio tutti gli sviluppatori che non hanno ancora provato l'esperienza a farlo. Sono molto soddisfatto sia della procedura di porting sia del nuovo pubblico di giocatori che ho raggiunto grazie a un gioco HTML5. Non esitare a inviarmi un'email in caso di domande. In alternativa, lascia un commento qui sotto. Cercherò di controllare regolarmente i commenti.