Introdução
Depois de publicar o Bouncy Mouse para iOS e Android no final do ano passado, aprendi algumas lições muito importantes. O principal deles é que é difícil entrar em um mercado já estabelecido. No mercado de iPhones, que é muito saturado, foi muito difícil ganhar tração. No Android Marketplace, menos saturado, o progresso foi mais fácil, mas ainda não foi fácil. Com base nessa experiência, percebi uma oportunidade interessante na Chrome Web Store. Embora a Web Store não esteja vazia, o catálogo de jogos baseados em HTML5 de alta qualidade está apenas começando a crescer. Para um desenvolvedor de apps, isso significa que é muito mais fácil criar gráficos de classificação e ganhar visibilidade. Com essa oportunidade em mente, comecei a portar o Bouncy Mouse para HTML5, na esperança de oferecer minha experiência de jogo mais recente a uma nova base de usuários. Neste estudo de caso, vou falar um pouco sobre o processo geral de portar o Bouncy Mouse para HTML5 e, em seguida, vou me aprofundar um pouco em três áreas que se mostraram interessantes: áudio, performance e monetização.
Portar um jogo C++ para HTML5
O Bouncy Mouse está disponível no Android(C++), iOS (C++), Windows Phone 7 (C#) e Chrome (Javascript). Isso às vezes gera a pergunta: como escrever um jogo que pode ser facilmente transferido para várias plataformas? Tenho a sensação de que as pessoas esperam por uma solução mágica para alcançar esse nível de portabilidade sem recorrer a uma transferência manual. Infelizmente, não tenho certeza se essa solução existe. A mais próxima é provavelmente o framework PlayN do Google ou o motor Unity, mas nenhum deles atinge todos os objetivos em que eu estava interessado. Minha abordagem foi, na verdade, uma porta manual. Primeiro, escrevi a versão para iOS/Android em C++, depois transfiro esse código para cada nova plataforma. Embora isso pareça muito trabalho, as versões do WP7 e do Chrome não levaram mais do que duas semanas para serem concluídas. A questão agora é: é possível fazer algo para tornar uma base de código facilmente portátil? Fiz algumas coisas que ajudaram nisso:
Manter a base de código pequena
Embora isso pareça óbvio, é realmente o principal motivo pelo qual consegui portar o jogo tão rapidamente. O código do cliente do Bouncy Mouse tem apenas cerca de 7.000 linhas de C++. 7.000 linhas de código não é nada, mas é pequeno o suficiente para ser gerenciável. As versões C# e JavaScript do código do cliente acabaram tendo aproximadamente o mesmo tamanho. Manter minha base de código pequena basicamente se resumiu a duas práticas principais: não escrever código em excesso e fazer o máximo possível no código de pré-processamento (fora do tempo de execução). Não escrever código em excesso pode parecer óbvio, mas é algo que sempre me preocupo. Muitas vezes, tenho vontade de criar uma classe/função auxiliar para tudo o que pode ser fatorado em um auxiliar. No entanto, a menos que você planeje usar um auxiliar várias vezes, ele geralmente acaba aumentando o tamanho do código. Com o Bouncy Mouse, tive o cuidado de nunca criar um auxiliar, a menos que fosse usá-lo pelo menos três vezes. Quando escrevi uma classe auxiliar, tentei torná-la limpa, portátil e reutilizável para meus projetos futuros. Por outro lado, ao escrever código apenas para o Bouncy Mouse, com baixa probabilidade de reutilização, meu foco era realizar a tarefa de programação de forma mais simples e rápida possível, mesmo que essa não fosse a maneira mais bonita de escrever o código. A segunda e mais importante parte de manter a base de código pequena foi empurrar o máximo possível para as etapas de pré-processamento. Se você puder mover uma tarefa de execução para uma tarefa de pré-processamento, seu jogo será executado mais rápido e você não precisará portar o código para cada nova plataforma. Para dar um exemplo, eu armazenei os dados de geometria do nível como um formato não processado, montando os buffers de vértice OpenGL/WebGL reais no momento da execução. Isso exigiu um pouco de configuração e algumas centenas de linhas de código de execução. Mais tarde, mudei esse código para uma etapa de pré-processamento, escrevendo buffers de vértices OpenGL/WebGL totalmente compactados no momento da compilação. A quantidade real de código era mais ou menos a mesma, mas aquelas poucas centenas de linhas foram movidas para uma etapa de pré-processamento, o que significa que eu nunca precisei fazer a portabilidade delas para novas plataformas. Há muitos exemplos disso no Bouncy Mouse, e o que é possível varia de jogo para jogo, mas fique de olho em tudo o que não precisa acontecer no momento da execução.
Não use dependências desnecessárias
Outro motivo pelo qual o Bouncy Mouse é fácil de portar é porque ele quase não tem dependências. O gráfico a seguir resume as principais dependências de biblioteca do Bouncy Mouse por plataforma:
É isso. Nenhuma biblioteca de terceiros grande foi usada, exceto a Box2D, que é portátil em todas as plataformas. Para gráficos, o WebGL e o XNA são mapeados quase 1:1 com o OpenGL, então isso não foi um grande problema. Apenas na área de som as bibliotecas são diferentes. No entanto, o código de som no Bouncy Mouse é pequeno (cerca de cem linhas de código específico da plataforma), então isso não foi um grande problema. Manter o Bouncy Mouse livre de grandes bibliotecas não portáveis significa que a lógica do código de tempo de execução pode ser quase a mesma entre as versões (apesar da mudança de idioma). Além disso, ele evita que fiquemos presos a uma cadeia de ferramentas não portátil. Me perguntaram se a codificação direta do OpenGL/WebGL aumenta a complexidade em comparação com o uso de uma biblioteca como o Cocos2D ou o Unity (também há alguns auxiliares do WebGL). Na verdade, acredito exatamente o contrário. A maioria dos jogos para smartphone / HTML5 (pelo menos os como o Bouncy Mouse) são muito simples. Na maioria dos casos, o jogo apenas desenha alguns sprites e talvez algumas geometrias com texturas. A soma total do código específico do OpenGL no Bouncy Mouse provavelmente é menor que 1.000 linhas. Eu ficaria surpreso se o uso de uma biblioteca auxiliar realmente reduzisse esse número. Mesmo que esse número seja reduzido pela metade, eu precisaria passar muito tempo aprendendo novas bibliotecas/ferramentas apenas para salvar 500 linhas de código. Além disso, ainda não encontrei uma biblioteca auxiliar portátil em todas as plataformas em que tenho interesse. Portanto, essa dependência prejudicaria significativamente a portabilidade. Se eu estivesse escrevendo um jogo 3D que precisasse de lightmaps, LOD dinâmico, animação com skin e assim por diante, minha resposta certamente mudaria. Nesse caso, eu estaria reinventando a roda para tentar programar manualmente todo o mecanismo em relação ao OpenGL. O ponto aqui é que a maioria dos jogos para dispositivos móveis/HTML5 ainda não está nessa categoria. Portanto, não é necessário complicar as coisas antes que seja necessário.
Não subestime as semelhanças entre os idiomas
Um último truque que economizou muito tempo na portabilidade da minha base de código C++ para uma nova linguagem foi perceber que a maior parte do código é quase idêntica entre cada linguagem. Embora alguns elementos-chave possam mudar, eles são muito menos numerosos do que as coisas que não mudam. Na verdade, para muitas funções, a transição de C++ para JavaScript envolveu apenas a execução de algumas substituições de expressão regular na minha base de código C++.
Conclusões sobre a portabilidade
Isso é tudo sobre o processo de portabilidade. Vou abordar alguns desafios específicos do HTML5 nas próximas seções, mas a mensagem principal é que, se você manter seu código simples, a portabilidade será uma pequena dor de cabeça, não um pesadelo.
Áudio
Uma área que causou problemas para mim (e aparentemente para todos os outros) foi o áudio. No iOS e no Android, há várias opções de áudio sólidas disponíveis (OpenSL, OpenAL), mas no mundo do HTML5, as coisas pareciam mais sombrias. Embora o HTML5 Audio esteja disponível, descobri que ele tem alguns problemas importantes quando usado em jogos. Mesmo nos navegadores mais recentes, encontrava com frequência um comportamento estranho. O Chrome, por exemplo, parece ter um limite no número de elementos de áudio simultâneos (source) que você pode criar. Além disso, mesmo quando o som era reproduzido, ele às vezes ficava distorcido de forma inexplicável. No geral, fiquei um pouco preocupado. Uma pesquisa on-line revelou que quase todo mundo tem o mesmo problema. A solução que encontrei inicialmente foi uma API chamada SoundManager2. Essa API usa o áudio HTML5 quando disponível e usa o Flash em situações complicadas. Embora essa solução tenha funcionado, ela ainda tinha bugs e era imprevisível (menos do que o áudio HTML5 puro). Uma semana depois do lançamento, conversei com algumas pessoas úteis do Google, que me indicaram a API Web Audio do Webkit. Eu tinha pensado em usar essa API, mas desisti devido à complexidade desnecessária (para mim) que ela parecia ter. Eu só queria tocar alguns sons: com o HTML5 Audio, isso equivale a algumas linhas de JavaScript. No entanto, ao analisar rapidamente o Web Audio, fiquei impressionado com a especificação enorme (70 páginas), a pequena quantidade de amostras na Web (típico de uma nova API) e a omissão de uma função "reproduzir", "pausar" ou "parar" em qualquer lugar da especificação. Com as garantias do Google de que minhas preocupações não eram bem fundamentadas, mergulhei na API novamente. Depois de analisar mais alguns exemplos e fazer mais pesquisas, descobri que o Google estava certo: a API pode atender às minhas necessidades sem os bugs que afetam as outras APIs. O artigo Como começar a usar a API Web Audio é muito útil. Ele é um ótimo lugar para entender melhor a API. Meu problema real é que, mesmo depois de entender e usar a API, ela ainda parece não ter sido projetada para "apenas tocar alguns sons". Para contornar essa dúvida, criei uma pequena classe auxiliar que me permite usar a API da maneira que eu queria: tocar, pausar, parar e consultar o estado de um som. Chamei essa classe auxiliar de AudioClip. O código completo está disponível no GitHub sob a licença Apache 2.0, e vou discutir os detalhes da classe abaixo. Mas, primeiro, confira alguns antecedentes sobre a API Web Audio:
Gráficos de áudio da Web
A primeira coisa que torna a API Web Audio mais complexa (e mais poderosa) do que o elemento de áudio HTML5 é a capacidade de processar / misturar o áudio antes de exibi-lo ao usuário. Embora seja poderoso, o fato de qualquer reprodução de áudio envolver um gráfico torna as coisas um pouco mais complexas em cenários simples. Para ilustrar o poder da API Web Audio, considere o seguinte gráfico:
Embora o exemplo acima mostre o poder da API Web Audio, eu não precisei de grande parte desse poder no meu cenário. Só queria ouvir um som. Embora isso ainda exija um gráfico, ele é muito simples.
Gráficos podem ser simples
A primeira coisa que torna a API Web Audio mais complexa (e mais poderosa) do que o elemento de áudio HTML5 é a capacidade de processar / misturar o áudio antes de exibi-lo ao usuário. Embora seja poderoso, o fato de qualquer reprodução de áudio envolver um gráfico torna as coisas um pouco mais complexas em cenários simples. Para ilustrar o poder da API Web Audio, considere o seguinte gráfico:
O gráfico trivial mostrado acima pode fazer tudo o que é necessário para reproduzir, pausar ou parar um som.
Mas não se preocupe com o gráfico
Embora seja bom entender o gráfico, não é algo com que quero lidar toda vez que reproduzir um som. Portanto, escrevi uma classe wrapper simples "AudioClip". Essa classe gerencia esse gráfico internamente, mas apresenta uma API voltada ao usuário muito mais simples.
Essa classe é nada mais do que um gráfico do Web Audio e um estado auxiliar, mas permite que eu use um código muito mais simples do que se eu tivesse que criar um gráfico do Web Audio para reproduzir cada som.
// At startup time
var sound = new AudioClip("ping.wav");
// Later
sound.play();
Detalhes de implementação
Vamos dar uma olhada rápida no código da classe auxiliar: Construtor: o construtor processa o carregamento dos dados de som usando um XHR. Embora não seja mostrado aqui (para manter o exemplo simples), um elemento de áudio HTML5 também pode ser usado como um nó de origem. Isso é útil principalmente para amostras grandes. A API Web Audio exige que esses dados sejam buscados como um "arraybuffer". Depois que os dados são recebidos, criamos um buffer de áudio da Web com esses dados (decodificando-os do formato original para um formato PCM de execução).
/**
* 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: tocar o som envolve duas etapas: configurar o gráfico de reprodução e chamar uma versão de "noteOn" na origem do gráfico. Uma fonte só pode ser reproduzida uma vez, então precisamos recriar a fonte/gráfico toda vez que formos reproduzir.
A maior parte da complexidade dessa função vem dos requisitos necessários para retomar um clipe pausado (this.pauseTime_ > 0
). Para retomar a reprodução de um clipe pausado, usamos noteGrainOn
,
que permite reproduzir uma sub-região de um buffer. Infelizmente, noteGrainOn
não interage com a repetição da maneira desejada para esse cenário. Ela vai repetir a sub-região, não todo o buffer.
Portanto, precisamos contornar esse problema reproduzindo o restante do clipe com noteGrainOn
e, em seguida, reiniciando o clipe do início com a repetição ativada.
/**
* 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);
}
}
}
Tocar como efeito sonoro: a função de reprodução acima não permite que o clipe de áudio seja reproduzido várias vezes com sobreposição. Uma segunda reprodução só é possível quando o clipe é concluído ou interrompido. Às vezes, um jogo precisa reproduzir um som várias vezes sem esperar a conclusão de cada reprodução (coletar moedas em um jogo, etc.). Para ativar isso, a classe AudioClip tem um método playAsSFX()
.
Como várias reproduções podem ocorrer simultaneamente, a reprodução de playAsSFX()
não está vinculada 1:1 ao AudioClip. Portanto, não é possível parar, pausar ou consultar o estado da reprodução. O looping também é desativado, porque não há como interromper um som em loop reproduzido dessa forma.
/**
* 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);
}
}
Estado de interrupção, pausa e consulta: o restante das funções é bastante direto e não requer muita explicação:
/**
* 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));
}
Conclusão do áudio
Esperamos que essa classe auxiliar seja útil para desenvolvedores que enfrentam os mesmos problemas de áudio que eu. Além disso, uma classe como essa parece um bom lugar para começar, mesmo que você precise adicionar alguns dos recursos mais poderosos da API Web Audio. De qualquer forma, essa solução atendeu às necessidades do Bouncy Mouse e permitiu que o jogo fosse um verdadeiro jogo HTML5, sem amarras.
Desempenho
Outra área que me preocupou em relação a uma porta Javascript foi a performance. Depois de terminar a v1 da minha porta, descobri que tudo estava funcionando bem no meu computador quad-core. Infelizmente, as coisas não estavam muito boas em um netbook ou Chromebook. Nesse caso, o perfilador do Chrome me ajudou mostrando exatamente onde todo o tempo dos meus programas estava sendo gasto.
Minha experiência destaca a importância de criar perfis antes de fazer qualquer otimização. Eu esperava que a física do Box2D ou talvez o código de renderização fosse uma fonte importante de lentidão. No entanto, a maior parte do meu tempo foi gasto na função Matrix.clone()
. Dada a natureza matemática do meu jogo, eu sabia que fiz muita criação/clonagem de matrizes, mas nunca imaginei que esse seria o gargalo. No final, uma mudança muito simples permitiu que o jogo reduzisse o uso da CPU em mais de três vezes, passando de 6 a 7% de CPU no meu computador para 2%.
Talvez isso seja conhecimento comum para desenvolvedores de Javascript, mas, como desenvolvedor de C++, esse problema me surpreendeu, então vou entrar em mais detalhes. Basicamente, minha classe de matriz original era uma matriz 3x3: uma matriz de 3 elementos, cada elemento contendo uma matriz de 3 elementos. Isso significa que, quando chegou a hora de clonar a matriz, tive que criar quatro matrizes novas. A única mudança que precisei fazer foi mover esses dados para uma única matriz de 9 elementos e atualizar a matemática de acordo. Essa mudança foi totalmente responsável pela redução de três vezes na CPU que observei. Depois disso, o desempenho foi aceitável em todos os meus dispositivos de teste.
Mais otimização
Embora minha performance fosse aceitável, ainda havia alguns problemas menores. Depois de um pouco mais de caracterização de perfil, percebi que isso ocorreu devido à coleta de lixo do Javascript. Meu app estava rodando a 60 qps, o que significa que cada frame tinha apenas 16 ms para ser renderizado. Infelizmente, quando a coleta de lixo era iniciada em uma máquina mais lenta, às vezes ela consumia cerca de 10 ms. Isso resultou em um travamento a cada poucos segundos, já que o jogo exigia quase 16 ms para renderizar um frame completo. Para entender melhor por que eu estava gerando tanto lixo, usei o perfilador de heap do Chrome. Para meu desespero, a grande maioria do lixo (mais de 70%) estava sendo gerada pelo Box2D. Eliminar lixo em Javascript é uma tarefa complicada, e reescrever o Box2D não era uma opção, então percebi que estava em uma situação difícil. Felizmente, ainda tinha um dos truques mais antigos disponíveis: quando não é possível atingir 60 fps, execute a 30 fps. É bastante comum concordar que executar a 30 fps consistentes é muito melhor do que executar a 60 fps instáveis. Na verdade, ainda não recebi nenhuma reclamação ou comentário de que o jogo roda a 30 fps (é muito difícil dizer, a menos que você compare as duas versões lado a lado). Esses 16 ms extras por frame significam que, mesmo no caso de uma coleta de lixo ruim, ainda tinha tempo suficiente para renderizar o frame. Embora a execução a 30 fps não seja explicitamente ativada pela API de temporização que eu estava usando (a excelente requestAnimationFrame do WebKit), ela pode ser realizada de maneira muito simples. Embora não seja tão elegante quanto uma API explícita, é possível alcançar 30 fps sabendo que o intervalo de RequestAnimationFrame está alinhado à VSYNC do monitor (geralmente 60 fps). Isso significa que precisamos ignorar todos os outros callbacks. Basicamente, se você tiver um callback "Tick" que é chamado toda vez que "RequestAnimationFrame" for acionado, isso pode ser feito da seguinte maneira:
var skip = false;
function Tick() {
skip = !skip;
if (skip) {
return;
}
// OTHER CODE
}
Se você quiser ter mais cuidado, verifique se a VSYNC do computador não está em 30 fps ou abaixo disso na inicialização e desative a omissão nesse caso. No entanto, ainda não encontrei isso em nenhuma configuração de desktop/laptop que testei.
Distribuição e monetização
Uma área final que me surpreendeu na porta do Chrome do Bouncy Mouse foi a monetização. Ao iniciar este projeto, imaginei os jogos HTML5 como um experimento interessante para aprender sobre tecnologias emergentes. O que eu não percebi foi que a porta alcançaria um público muito grande e teria um potencial significativo de monetização.
O Bouncy Mouse foi lançado no final de outubro na Chrome Web Store. Ao lançar na Chrome Web Store, pude aproveitar um sistema existente para descoberta, engajamento da comunidade, classificações e outros recursos que já conhecia em plataformas móveis. O que me surpreendeu foi o alcance da loja. Em um mês após o lançamento, eu já tinha quase 400 mil instalações e já estava aproveitando o engajamento da comunidade (relatório de bugs, feedback). Outra coisa que me surpreendeu foi o potencial de monetização de um app da Web.
O Bouncy Mouse tem um método de monetização simples: um anúncio de banner ao lado do conteúdo do jogo. No entanto, devido ao grande alcance do jogo, descobri que esse anúncio de banner conseguia gerar receita significativa. Durante o período de pico, o app gerou receita comparável à minha plataforma de maior sucesso, o Android. Um dos fatores que contribuem para isso é que os anúncios maiores do Google AdSense mostrados na versão HTML5 geram uma receita por impressão significativamente maior do que os anúncios menores do AdMob mostrados no Android. Além disso, o anúncio de banner na versão HTML5 é muito menos intrusivo do que na versão do Android, permitindo uma experiência de jogo mais limpa. No geral, fiquei muito surpreso com esse resultado.

Embora os ganhos do jogo tenham sido muito melhores do que o esperado, o alcance da Chrome Web Store ainda é menor do que o de plataformas mais maduras, como o Android Market. Embora o Bouncy Mouse tenha conseguido subir rapidamente para o nono jogo mais popular na Chrome Web Store, a taxa de novos usuários acessando o site diminuiu consideravelmente desde o lançamento inicial. No entanto, o jogo ainda está crescendo de forma constante, e estou animado para ver como a plataforma vai se desenvolver.
Conclusão
A portabilidade do Bouncy Mouse para o Chrome foi muito mais fácil do que eu esperava. Além de alguns problemas menores de áudio e desempenho, descobri que o Chrome era uma plataforma perfeitamente capaz para um jogo de smartphone. Incentivamos todos os desenvolvedores que têm evitado a experiência a tentar. Estou muito feliz com o processo de portabilidade e com o novo público de jogos que ter um jogo em HTML5 me conectou. Se tiver alguma dúvida, envie um e-mail. Ou deixe um comentário abaixo. Vou tentar verificar isso regularmente.