Estudo de caso: Bouncy Mouse

Introdução

Rato saltitante

Depois de publicar o Bouncy Mouse no iOS e no Android, no final do ano passado, aprendi algumas lições muito importantes. Um deles é que é difícil entrar em um mercado estabelecido. No mercado completamente saturado do iPhone, ganhar força era muito difícil. No Android Marketplace, que era menos saturado, o progresso foi mais fácil, mas ainda não foi fácil. Tendo essa experiência, vi uma oportunidade interessante na Chrome Web Store. Embora a Web Store não esteja vazia, seu catálogo de jogos de alta qualidade baseados em HTML5 está apenas começando a amadurecer. Para um novo desenvolvedor de apps, isso significa que criar as tabelas de classificação e ganhar visibilidade é muito mais fácil. Com essa oportunidade em mente, decidi transferir o Bouncy Mouse para HTML5 na esperança de poder entregar minha experiência de jogo mais recente a uma nova e empolgante base de usuários. Neste estudo de caso, falarei um pouco sobre o processo geral de transferência do Bouncy Mouse para HTML5 e, em seguida, abordarei três áreas que se mostraram interessantes: áudio, desempenho e monetização.

Como transferir um jogo C++ para HTML5

No momento, o Bouncy Mouse está disponível para Android(C++), iOS (C++), Windows Phone 7 (C#) e Chrome (JavaScript). Isso às vezes faz a pergunta: como criar um jogo que pode ser facilmente transferido para várias plataformas? Tenho a sensação de que as pessoas esperam uma solução mágica que possa ser usada para atingir esse nível de portabilidade sem recorrer a ferramentas manuais. Infelizmente, não tenho certeza se essa solução ainda existe. A solução mais próxima provavelmente é o framework PlayN do Google ou o mecanismo Unity, mas nenhum deles atinge todos os objetivos que me interessam. Minha abordagem era, na verdade, uma porta de mão. Primeiro, escrevi a versão iOS/Android em C++ e, em seguida, fiz a portabilidade desse código para cada nova plataforma. Embora isso possa parecer muito trabalho, as versões WP7 e Chrome não levaram mais do que duas semanas para serem concluídas. Agora, a pergunta é: algo pode ser feito para tornar uma base de código fácil de portabilidade? Fiz algumas coisas que ajudaram nisso:

Mantenha a base de código pequena

Embora isso possa parecer óbvio, esse foi o principal motivo para eu conseguir transferir o jogo tão rapidamente. O código de cliente do Bouncy Mouse tem apenas cerca de 7.000 linhas de C++. 7.000 linhas de código não são nada, mas são pequenas o suficiente para serem gerenciáveis. As versões C# e JavaScript do código do cliente ficaram praticamente do mesmo tamanho. Manter minha base de código pequena basicamente equivale a duas práticas importantes: não escreva nenhum código em excesso e faça o máximo possível no código de pré-processamento (sem ambiente de execução). Pode parecer óbvio não escrever nenhum código excessivo, mas é algo que sempre brigo consigo mesmo. Muitas vezes, tenho vontade de criar uma classe/função auxiliar para qualquer coisa que possa ser incluída em um auxiliar. No entanto, a menos que você planeje usar um auxiliar várias vezes, isso geralmente sobrecarrega seu código. Com o Bouncy Mouse, eu tomei o cuidado de nunca escrever um auxiliar a menos que eu fosse usá-lo pelo menos três vezes. Quando criei uma aula auxiliar, tentei torná-la limpa, portátil e reutilizável para 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 codificação da 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, era fazer o máximo possível para as etapas de pré-processamento. Se você puder migrar uma tarefa do ambiente de execução para uma de pré-processamento, o jogo vai ser executado com mais rapidez e não vai ser necessário transferir o código para cada plataforma nova. Por exemplo, armazenei meus dados de geometria de nível originalmente como um formato bastante não processado, montando os buffers de vértice do OpenGL/WebGL no tempo de execução. Isso exigiu um pouco de configuração e algumas centenas de linhas de código no ambiente de execução. Mais tarde, movai esse código para uma etapa de pré-processamento, escrevendo buffers de vértice do OpenGL/WebGL totalmente compactados no tempo de compilação. A quantidade real de código era quase a mesma, mas essas poucas centenas de linhas foram movidas para uma etapa de pré-processamento, o que significa que nunca tive que transferi-las para novas plataformas. Há vários exemplos disso no Bouncy Mouse, e as possibilidades variam de acordo com o jogo, mas fique de olho no que não precisa acontecer durante a execução.

Não adquira dependências de que você não precisa

Outra razão pela qual o Bouncy Mouse é fácil de transferir é porque ele quase não tem dependências. O gráfico a seguir resume as principais dependências da biblioteca do Bouncy Mouse por plataforma:

Android iOS HTML5 WP7
Gráficos OpenGL ES OpenGL ES WebGL ANEXO
Som OpenSL ES OpenAL Áudio da Web ANEXO
Física Box2D Box2D Box2D.js Box2D.xna

É basicamente isso. Nenhuma grande biblioteca de terceiros foi usada, exceto o Box2D, que tem portabilidade em todas as plataformas. Para gráficos, tanto o WebGL quanto o XNA mapeiam quase 1:1 com o OpenGL, então esse não foi um grande problema. Somente a área do som era diferente das bibliotecas. No entanto, o código de som no Bouncy Mouse é pequeno (cerca de cem linhas de código específico da plataforma), então esse não foi um grande problema. Manter o Bouncy Mouse livre de grandes bibliotecas não portáteis 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 linguagem). Além disso, evita que fiquemos presos a uma cadeia de ferramentas não portátil. Tenho me perguntado se a programação com o OpenGL/WebGL causa um aumento direto da complexidade em comparação com o uso de uma biblioteca como Cocos2D ou Unity. Também existem alguns auxiliares de WebGL por aí. Na verdade, acredito no contrário. A maioria dos jogos para celular / HTML5 (pelo menos aqueles como o Bouncy Mouse) são muito simples. Na maioria dos casos, o jogo desenha apenas alguns sprites e talvez alguma geometria texturizada. A soma total do código específico do OpenGL no Bouncy Mouse provavelmente é inferior a mil linhas. Eu ficaria surpreso se o uso de uma biblioteca auxiliar realmente reduzisse esse número. Mesmo que corte esse número pela metade, eu precisaria passar um tempo significativo aprendendo novas bibliotecas/ferramentas para economizar 500 linhas de código. Além disso, ainda não encontrei uma biblioteca auxiliar que possa ser usada em todas as plataformas em que estou interessado, então essa dependência prejudica muito a portabilidade. Se eu estivesse escrevendo um jogo 3D que precisasse de lightmaps, LOD dinâmico, animação de skin e assim por diante, minha resposta certamente mudaria. Nesse caso, eu estaria inventando a roda para tentar codificar manualmente todo o meu motor em relação ao OpenGL. Na minha opinião, a maioria dos jogos para dispositivos móveis/HTML5 não estão (ainda) nessa categoria, então não é preciso complicar as coisas antes que seja necessário.

Não subestime as semelhanças entre 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 em cada linguagem. Embora alguns elementos-chave possam mudar, eles são muito menos do que as coisas que não mudam. Na verdade, para muitas funções, ir do C++ para JavaScript envolvia apenas a execução de algumas substituições de expressões regulares na minha base de código C++.

Conclusão da portabilidade

Esse é o caso do processo de portabilidade. Falarei sobre alguns desafios específicos do HTML5 nas próximas seções, mas a mensagem principal é que, se você mantiver 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 todos os outros) foi o áudio. No iOS e no Android, há diversas opções de áudio disponíveis (OpenSL, OpenAL), mas no mundo do HTML5 as coisas pareciam mais sombrias. Embora o áudio HTML5 esteja disponível, descobri que ele apresenta alguns problemas quando usado em jogos. Até nos navegadores mais recentes, com frequência eu me deparava com comportamentos estranhos. O Chrome, por exemplo, parece ter um limite no número de elementos de áudio simultâneos (fonte) que você pode criar. Além disso, mesmo quando o som era tocado, às vezes ficava inexplicavelmente distorcido. No geral, fiquei um pouco preocupado. Pesquisar online revelou que quase todos têm o mesmo problema. A solução que acabei de usar foi uma API chamada SoundManager2. Essa API usa áudio HTML5 quando disponível, voltando ao Flash em situações complicadas. Embora essa solução funcione, ela ainda apresentava bugs e era imprevisível (um pouco menos do que o áudio em HTML5 puro). Uma semana após o lançamento, conversei com algumas das pessoas úteis do Google que me indicaram para a API Web Audio do Webkit. Inicialmente, pensei em usar essa API, mas afastei-se devido à complexidade (para mim) desnecessária que ela parecia ter. Eu só queria tocar alguns sons: com áudio HTML5, isso equivale a algumas linhas de JavaScript. No entanto, ao dar uma olhada rápida no Web Audio, fiquei impressionado com sua enorme especificação (70 páginas), a pequena quantidade de amostras na Web (típica para uma nova API) e a omissão de uma função “reproduzir”, “pausar” ou “parar” em qualquer lugar da especificação. Com a garantia do Google de que minhas preocupações não estavam bem fundamentadas, aprofundei a API de novo. Depois de analisar mais alguns exemplos e fazer mais pesquisas, descobri que o Google estava certo: a API com certeza pode atender às minhas necessidades, e isso é possível sem os bugs que afetam as outras APIs. O artigo Getting Started with Web Audio API é especialmente útil se você quiser entender melhor a API. Meu verdadeiro problema é que, mesmo depois de entender e usar a API, ela ainda parece ser uma API não projetada para "apenas tocar alguns sons". Para contornar esse problema, criei uma pequena classe auxiliar que me permitia usar a API da maneira que queria: reproduzir, pausar, parar e consultar o estado de um som. Chamei essa classe auxiliar de AudioClip. A fonte completa está disponível no GitHub (em inglês) sob a licença Apache 2.0. Falarei sobre os detalhes da aula abaixo. Mas, primeiro, algumas informações básicas sobre a API de áudio da Web:

Gráficos de áudio da web

A primeira coisa que torna a API de áudio da Web mais complexa (e mais poderosa) do que o elemento de áudio HTML5 é sua capacidade de processar / mixar áudio antes de enviá-lo ao usuário. Embora seja eficiente, o fato de que toda reprodução de áudio envolve um gráfico torna as coisas um pouco mais complexas em cenários simples. Para ilustrar o poder da API de áudio da Web, considere o gráfico a seguir:

Web Audio Graph básico
Basic Web Audio Graph

Embora o exemplo acima mostre o poder da API de áudio da Web, não precisei de mais dessa capacidade no meu cenário. Eu só queria fazer um som. Embora ainda exija um gráfico, o gráfico é muito simples.

Os gráficos podem ser simples

A primeira coisa que torna a API de áudio da Web mais complexa (e mais poderosa) do que o elemento de áudio HTML5 é sua capacidade de processar / mixar áudio antes de enviá-lo ao usuário. Embora seja eficiente, o fato de que toda reprodução de áudio envolve um gráfico torna as coisas um pouco mais complexas em cenários simples. Para ilustrar o poder da API de áudio da Web, considere o gráfico a seguir:

Trivial Web Audio Graph
Trivial Web Audio Graph

O gráfico trivial mostrado acima pode realizar tudo o que é necessário para reproduzir, pausar ou interromper um som.

Mas não precisamos nos preocupar com o gráfico

Entender o gráfico é bom, mas não é algo com o qual quero lidar cada vez que toco um som. Por isso, criei uma classe wrapper simples, o "AudioClip". Essa classe gerencia esse gráfico internamente, mas apresenta uma API muito mais simples para o usuário.

AudioClip
AudioClip

Essa classe é nada mais do que um gráfico de áudio da Web e algum estado auxiliar, mas me permite usar um código muito mais simples do que se eu tivesse que criar um gráfico de áudio da Web 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 isso não seja mostrado aqui (para simplificar o exemplo), um elemento de áudio HTML5 também pode ser usado como um nó de origem. Isso é especialmente útil para amostras grandes. A API Web Audio exige a busca desses dados como um "arraybuffer". Depois que os dados são recebidos, criamos um buffer do Web Audio com base neles, decodificando-os do formato original para um formato PCM de tempo 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();
}

Reproduzir – A reprodução do 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. Por isso, precisamos recriá-la sempre que a reprodução for feita. 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 o loop da maneira desejada nesse cenário (ele repetirá a sub-região, não o buffer inteiro). Portanto, precisamos contornar isso reproduzindo o restante do clipe com noteGrainOn e reiniciando o clipe desde o início com o loop ativado.

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

Reproduzir 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, você vai querer tocar um som várias vezes sem esperar que cada reprodução termine (coletar moedas em um jogo etc.). Para ativar esse recurso, a classe AudioClip tem um método playAsSFX(). Como várias reproduções podem ocorrer simultaneamente, a reprodução de playAsSFX() não é vinculada 1:1 ao AudioClip. Portanto, a reprodução não pode ser interrompida, pausada ou consultada em busca do estado. O looping também é desativado, já que não seria possível interromper um som em loop dessa maneira.

/**
* 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 parada, pausa e consulta: o restante das funções é bastante simples 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 de áudio

Espero que esta aula auxiliar seja útil para desenvolvedores que enfrentam os mesmos problemas de áudio que eu. Além disso, uma aula como essa parece ser um bom lugar para começar mesmo se você precisar adicionar alguns dos recursos mais poderosos da API de áudio da Web. De qualquer forma, essa solução atendeu às necessidades do Bouncy Mouse e permitiu que o jogo fosse um verdadeiro HTML5, sem compromisso!

Desempenho

Outra área que me preocupava em relação a uma porta JavaScript era o desempenho. Depois de concluir a v1 da minha porta, descobri que tudo estava funcionando bem na minha área de trabalho quad-core. Infelizmente, as coisas estavam um pouco menos complicadas em um netbook ou Chromebook. Nesse caso, o criador de perfil do Chrome economizou ao mostrar exatamente onde todo o tempo dos meus programas era gasto. Minha experiência destaca a importância da criação de perfis antes de qualquer otimização. Eu esperava que a física do Box2D ou talvez o código de renderização fosse uma grande fonte de lentidão. No entanto, a maior parte do meu tempo estava sendo gasto na função Matrix.clone(). Como meu jogo é cheio de matemática, eu sabia que tinha criado/clonagem de muitas matrizes, mas nunca esperava que isso fosse um gargalo. No fim, uma mudança muito simples permitiu que o jogo reduzisse o uso da CPU em mais de três vezes, passando de 6-7% para 2% no meu computador. Talvez isso seja de conhecimento comum para desenvolvedores de JavaScript, mas, como desenvolvedor de C++, esse problema me surpreendeu, por isso vou entrar em mais detalhes. Basicamente, minha classe de matriz original era 3x3: uma matriz de 3 elementos, cada elemento contendo uma matriz de 3 elementos. Infelizmente, isso significava que, quando era hora de clonar a matriz, eu tive que criar 4 novas matrizes. A única mudança que eu precisava fazer foi mover esses dados para uma única matriz de 9 elementos e atualizar meu cálculo. Essa mudança foi totalmente responsável por essa redução de 3x da CPU que vi. Depois dessa mudança, meu desempenho foi aceitável em todos os dispositivos de teste.

Mais otimização

Embora meu desempenho estivesse aceitável, ainda havia alguns pequenos contratempos. Depois de um pouco mais de caracterização de perfil, percebi que isso se deve à coleta de lixo do JavaScript. Meu app estava sendo executado a 60 fps, o que significava que cada frame tinha apenas 16 ms para ser desenhado. Infelizmente, quando a coleta de lixo entrava em uma máquina mais lenta, às vezes ela consumia cerca de 10 ms. Isso resultava em falhas temporárias em alguns segundos, já que o jogo exigia quase 16 ms para renderizar um frame completo. Para ter uma ideia melhor do motivo pelo qual eu estava gerando tanto lixo, usei o criador de perfil de alocação heap do Chrome. Para meu desespero, descobrimos que a grande maioria do lixo (mais de 70%) estava sendo gerada pelo Box2D. Eliminar lixo em JavaScript é um negócio complicado, e reescrever o Box2D estava fora de questão, então percebi que havia me perdido. Felizmente, eu ainda tinha um dos truques mais antigos do livro disponível para mim: quando não conseguir atingir 60 fps, corra a 30 fps. É bem comum que executar a uma taxa consistente de 30 fps é muito melhor do que executar a uma instabilidade de 60 fps. Na verdade, ainda não recebi nenhuma reclamação ou comentário informando que o jogo funciona a 30 fps (é muito difícil de afirmar a menos que você compare as duas versões lado a lado). Esses 16 ms extras por frame significavam que, mesmo no caso de uma coleta de lixo ruim, eu ainda tinha muito tempo para renderizar o frame. Embora a execução a 30 fps não seja explicitamente ativada pela API de tempo que eu estava usando (o excelente requestAnimationFrame do WebKit), isso pode ser realizado de uma maneira muito trivial. Embora não seja tão elegante quanto uma API explícita, 30 fps podem ser alcançados sabendo que o intervalo de RequestAnimationFrame está alinhado ao VSYNC do monitor (geralmente 60 fps). Isso significa que precisamos ignorar todos os outros callbacks. Basicamente, se você tiver um "Tick" de retorno de chamada que é chamado sempre que "RequestAnimationFrame" é disparado, isso pode ser feito da seguinte maneira:

var skip = false;

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

// OTHER CODE
}

Se você quiser ser ainda mais cauteloso, verifique se o VSYNC do computador não está em 30 fps na inicialização ou abaixo dele. Nesse caso, desative o recurso de pular. No entanto, ainda não vi isso em nenhuma configuração de desktop/laptop que testei.

Distribuição e monetização

Uma última área que me surpreendeu sobre a portabilidade do Bouncy Mouse para o Chrome foi a monetização. Ao entrar nesse projeto, pensei nos jogos em HTML5 como uma experiência interessante para aprender novas tecnologias. Não percebi que a transferência teria um potencial significativo de monetização e alcançaria um público muito grande.

O Bouncy Mouse foi lançado no final de outubro na Chrome Web Store. Ao lançar na Chrome Web Store, eu pude aproveitar um sistema existente para descoberta, envolvimento com a comunidade, classificação e outros recursos aos quais me acostumei a me acostumar em plataformas móveis. O que me surpreendeu foi o alcance da loja. Um mês após o lançamento, alcancei quase 400 mil instalações e já estava aproveitando o envolvimento da comunidade (relatórios de bugs, feedback). Outra coisa que me surpreendeu foi o potencial de monetização de um aplicativo da web.

O Bouncy Mouse tem um método simples de monetização: um anúncio de banner ao lado do conteúdo do jogo. No entanto, considerando o amplo alcance do jogo, descobri que esse anúncio de banner foi capaz de gerar uma renda significativa e, durante o período de pico, o aplicativo gerou renda compatível com minha plataforma de maior sucesso, o Android. Um fator que contribui para isso é que os anúncios maiores do Google AdSense exibidos na versão HTML5 geram uma receita por impressão significativamente maior do que os anúncios menores da AdMob exibidos no Android. Não apenas isso, mas o anúncio de banner na versão HTML5 é muito menos invasivo do que na versão Android, permitindo uma experiência de jogabilidade mais limpa. Em geral, fiquei muito agradavelmente surpreendido com o resultado.

Ganhos normalizados ao longo do tempo.
Ganhos normalizados ao longo do tempo

Embora os ganhos do jogo tenham sido muito melhores do que o esperado, é importante notar que o alcance da Chrome Web Store ainda é menor do que o de plataformas mais maduras, como o Android Market. Embora o Bouncy Mouse tenha sido rapidamente lançado para o nono jogo mais popular da Chrome Web Store, a taxa de novos usuários que acessou o site diminuiu consideravelmente desde o lançamento inicial. Por isso, o jogo ainda está crescendo constantemente, e estou ansiosa para ver como a plataforma vai se desenvolver!

Conclusão

Eu diria que a transferência do Bouncy Mouse para o Chrome foi muito mais fácil do que eu esperava. Além de alguns pequenos problemas de áudio e desempenho, achei que o Google Chrome era uma plataforma perfeitamente capaz para um jogo de smartphone existente. Encorajamos os desenvolvedores que estão desistindo da experiência a tentar. Estou muito feliz com o processo de portabilidade e com o público de jogos a que ter um jogo HTML5 me conectou. Fique à vontade para me enviar um e-mail se tiver alguma dúvida. Ou deixe um comentário abaixo. Tentarei verificar isso regularmente.