Gerenciamento simples de recursos para jogos HTML5

Introdução

O HTML5 oferece muitas APIs úteis para criar aplicativos da Web modernos, responsivos e avançados no navegador. Isso é ótimo, mas você quer muito criar e jogar! Felizmente, o HTML5 também inaugurou uma nova era de desenvolvimento de jogos que usa APIs como Canvas e potentes mecanismos JavaScript para entregar jogos diretamente no navegador sem precisar de plug-ins.

Este artigo mostra como criar um componente simples de gerenciamento de recursos para seu jogo HTML5. Sem um gerenciador de recursos, o jogo terá dificuldade em compensar os tempos de download desconhecidos e o carregamento assíncrono de imagem. Acompanhe para conferir um exemplo de um gerenciador de recursos simples para seus jogos HTML5.

O problema

Os jogos HTML5 não podem presumir que seus recursos, como imagens ou áudio, estarão na máquina local do jogador, já que os jogos HTML5 sugerem ser jogados em um navegador da Web com recursos transferidos por download via HTTP. Como a rede está envolvida, o navegador não tem certeza de quando os recursos do jogo serão transferidos por download e disponibilizados.

A maneira básica de carregar programaticamente uma imagem em um navegador da Web é a seguinte:

var image = new Image();
image.addEventListener("success", function(e) {
  // do stuff with the image
});
image.src = "/some/image.png";

Agora, imagine ter cem imagens que precisam ser carregadas e exibidas quando o jogo for iniciado. Como você sabe quando as cem imagens estão prontas? O carregamento foi concluído? Quando o jogo deve começar?

A solução

Deixe um gerente de recursos cuidar da fila de recursos e informar ao jogo quando tudo estiver pronto. Um gerenciador de ativos generaliza a lógica para carregar recursos pela rede e fornece uma maneira fácil de verificar o status.

Nosso gerenciador de ativos simples tem os seguintes requisitos:

  • enfileirar downloads
  • iniciar downloads
  • acompanhar o sucesso e o fracasso
  • sinalizar quando tudo estiver pronto
  • recuperação fácil de recursos

Enfileiramento

O primeiro requisito é enfileirar os downloads. Esse design permite declarar os recursos necessários sem realmente fazer o download deles. Isso pode ser útil se, por exemplo, você quiser declarar todos os recursos de um nível do jogo em um arquivo de configuração.

O código do construtor e da fila é semelhante a:

function AssetManager() {
  this.downloadQueue = [];
}

AssetManager.prototype.queueDownload = function(path) {
    this.downloadQueue.push(path);
}

Iniciar downloads

Depois de colocar todos os ativos na fila para download, você pode pedir ao gerenciador de ativos para começar a fazer o download de tudo.

Por sorte, o navegador da Web pode carregar os downloads em paralelo, geralmente com até quatro conexões por host. Uma maneira de acelerar o download de recursos é usar uma variedade de nomes de domínio para a hospedagem de recursos. Por exemplo, em vez de veicular tudo de assets.example.com, tente usar assets1.example.com, assets2.example.com, assets3.example.com e assim por diante. Mesmo que cada um desses nomes de domínio seja simplesmente um CNAME para o mesmo servidor da Web, o navegador da Web os vê como servidores separados e aumenta o número de conexões usadas para o download dos recursos. Saiba mais sobre essa técnica em Dividir componentes em domínios em "Práticas recomendadas para acelerar seu site".

Nosso método de inicialização é chamado de downloadAll(). Vamos continuar construindo isso com o tempo. Por enquanto, aqui está a primeira lógica para iniciar os downloads.

AssetManager.prototype.downloadAll = function() {
    for (var i = 0; i < this.downloadQueue.length; i++) {
        var path = this.downloadQueue[i];
        var img = new Image();
        var that = this;
        img.addEventListener("load", function() {
            // coming soon
        }, false);
        img.src = path;
    }
}

Como você pode ver no código acima, downloadAll() simplesmente itera pela downloadQueue e cria um novo objeto Image. Um listener de eventos é adicionado para o evento de carregamento e o src da imagem é definido, o que aciona o download real.

Com esse método, você pode iniciar os downloads.

Acompanhamento de sucesso e falha

Outro requisito é acompanhar os sucessos e os fracassos, porque, infelizmente, nem sempre tudo funciona perfeitamente. Até o momento, o código rastreia apenas os recursos transferidos por download. Ao adicionar um listener de eventos para o evento de erro, você poderá capturar os cenários de sucesso e falha.

AssetManager.prototype.downloadAll = function(downloadCallback) {
  for (var i = 0; i < this.downloadQueue.length; i++) {
    var path = this.downloadQueue[i];
    var img = new Image();
    var that = this;
    img.addEventListener("load", function() {
        // coming soon
    }, false);
    img.addEventListener("error", function() {
        // coming soon
    }, false);
    img.src = path;
  }
}

Nosso gerente de recursos precisa saber quantos sucessos e fracassos encontramos ou nunca vai saber quando o jogo pode começar.

Primeiro, vamos adicionar os contadores ao objeto no construtor, que agora tem a seguinte aparência:

function AssetManager() {
<span class="highlight">    this.successCount = 0;
    this.errorCount = 0;</span>
    this.downloadQueue = [];
}

Em seguida, incremente os contadores nos listeners de eventos, que agora estão assim:

img.addEventListener("load", function() {
    <span class="highlight">that.successCount += 1;</span>
}, false);
img.addEventListener("error", function() {
    <span class="highlight">that.errorCount += 1;</span>
}, false);

O gerenciador de recursos agora está rastreando os recursos carregados e com falha.

Sinalização concluída

Depois que o jogo colocar seus recursos na fila para download e solicitar que o gerente de ativos faça o download de todos eles, o jogo precisará ser avisado quando todos os recursos forem transferidos. Em vez de o jogo perguntar várias vezes se o download dos recursos foi feito, o gerenciador de recursos pode sinalizar para o jogo de novo.

O gerente de recursos precisa saber primeiro quando cada recurso foi concluído. Agora, vamos adicionar um método isDone:

AssetManager.prototype.isDone = function() {
    return (this.downloadQueue.length == this.successCount + this.errorCount);
}

Ao comparar o sucessoCount + errorCount com o tamanho da downloadQueue, o gerenciador de recursos sabe se cada recurso foi concluído com êxito ou teve algum tipo de erro.

Saber se tudo foi feito é apenas metade do caminho. O gerente de ativos também precisa verificar esse método. Adicionaremos essa verificação aos nossos dois manipuladores de eventos, conforme mostrado no código abaixo:

img.addEventListener("load", function() {
    console.log(this.src + ' is loaded');
    that.successCount += 1;
    if (that.isDone()) {
        // ???
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
if (that.isDone()) {
        // ???
    }
}, false);

Depois que os contadores forem incrementados, veremos se esse foi o último recurso na fila. Se o gerente de recursos realmente tiver feito o download, o que devemos fazer exatamente?

Se o gerente de ativos tiver feito o download de todos os recursos, é claro que chamaremos um método de callback. Vamos mudar o downloadAll() e adicionar um parâmetro para o callback:

AssetManager.prototype.downloadAll = function(downloadCallback) {
    ...

Chamaremos o método downloadCallback dentro de nossos listeners de eventos:

img.addEventListener("load", function() {
    that.successCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);

O gerenciador de recursos finalmente está pronto para o último requisito.

Fácil recuperação de ativos

Quando receber a sinalização do jogo, ele começará a renderizar imagens. O gerente de ativos não é responsável apenas por baixar e rastrear os ativos, mas também por fornecê-los ao jogo.

Nosso requisito final implica algum tipo de método getAsset, portanto, vamos adicioná-lo agora:

AssetManager.prototype.getAsset = function(path) {
    return this.cache[path];
}

Esse objeto de cache é inicializado no construtor, que agora tem a seguinte aparência:

function AssetManager() {
    this.successCount = 0;
    this.errorCount = 0;
    this.cache = {};
    this.downloadQueue = [];
}

O cache é preenchido no final de downloadAll(), conforme mostrado abaixo:

AssetManager.prototype.downloadAll = function(downloadCallback) {
  ...
      img.addEventListener("error", function() {
          that.errorCount += 1;
          if (that.isDone()) {
              downloadCallback();
          }
      }, false);
      img.src = path;
      <span class="highlight">this.cache[path] = img;</span>
  }
}

Bônus: correção de bugs

Você encontrou o bug? Como mencionado acima, o método isDone só é chamado quando eventos load ou de erro são acionados. Mas e se o gerenciador de ativos não tiver nenhum ativo na fila para download? O método isDone nunca é acionado, e o jogo nunca é iniciado.

Para acomodar esse cenário, adicione o seguinte código a downloadAll():

AssetManager.prototype.downloadAll = function(downloadCallback) {
    if (this.downloadQueue.length === 0) {
      downloadCallback();
  }
 ...

Se nenhum recurso estiver na fila, o callback será chamado imediatamente. Bug corrigido.

Exemplo de uso

É bem fácil usar esse gerenciador de recursos no seu jogo HTML5. Esta é a maneira mais básica de usar a biblioteca:

var ASSET_MANAGER = new AssetManager();

ASSET_MANAGER.queueDownload('img/earth.png');

ASSET_MANAGER.downloadAll(function() {
    var sprite = ASSET_MANAGER.getAsset('img/earth.png');
    ctx.drawImage(sprite, x - sprite.width/2, y - sprite.height/2);
});

O código acima ilustra:

  1. Cria um novo administrador de recursos
  2. Adicionar recursos à fila para download
  3. Iniciar os downloads com downloadAll()
  4. Sinalize quando os recursos estão prontos invocando a função de callback
  5. Recuperar recursos com getAsset()

Áreas para melhoria

Você certamente vai superar esse gerenciador de ativos simples enquanto cria seu jogo, mas espero que tenha sido um começo básico. Os recursos futuros podem incluir:

  • indicando qual recurso apresentou erro
  • callbacks para indicar o progresso
  • recuperar recursos da API File System

Poste melhorias, bifurcações e links para códigos nos comentários abaixo.

Fonte completa

A fonte desse gerenciador de ativos e do jogo é abstraído por meio de código aberto, sob a licença Apache, e pode ser encontrado na conta do GitHub do Bad Aliens (em inglês). Você pode jogar Bad Aliens em um navegador compatível com HTML5. Este jogo foi o assunto de minha palestra no Google I/O intitulada Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development (slides, vídeo).

Resumo

A maioria dos jogos tem algum tipo de gerenciador de recursos, mas os jogos HTML5 exigem um gerenciador que carregue recursos em uma rede e resolva falhas. Neste artigo, descrevemos um gerenciador de recursos simples que precisa ser fácil de usar e adaptar para seu próximo jogo HTML5. Divirta-se e deixe sua opinião nos comentários abaixo. Nossa equipe agradece!