Gerenciamento simples de recursos para jogos HTML5

Introdução

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

Neste artigo, vamos mostrar como criar um componente simples de gerenciamento de recursos para seu jogo HTML5. Sem um gerenciador de recursos, seu jogo terá dificuldade para compensar os tempos de download desconhecidos e o carregamento de imagens assíncronas. Confira um exemplo de gerenciador de recursos simples para seus jogos HTML5.

O problema

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

A maneira básica de carregar uma imagem em um navegador da Web é com o seguinte código:

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 é iniciado. Como você sabe quando todas as 100 imagens estão prontas? Todos eles foram carregados? Quando o jogo deve começar?

A solução

Deixe que um gerenciador de recursos gerencie a fila de recursos e informe o jogo quando tudo estiver pronto. Um gerenciador de recursos generaliza a lógica de carregamento de recursos pela rede e oferece uma maneira fácil de verificar o status.

Nosso gerenciador de recursos simples tem os seguintes requisitos:

  • enfileirar downloads;
  • iniciar downloads
  • acompanhar sucessos e falhas
  • sinalizar quando tudo estiver pronto
  • Recuperação fácil de recursos

Enfileiramento

O primeiro requisito é colocar os downloads em fila. Esse design permite declarar os recursos necessários sem 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 tem esta aparência:

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

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

Iniciar downloads

Depois de colocar todos os recursos na fila para download, você pode pedir ao gerenciador de recursos para iniciar o download de tudo.

Felizmente, o navegador da Web pode fazer isso em paralelo, geralmente até quatro conexões por host. Uma maneira de acelerar o download de recursos é usar uma variedade de nomes de domínio para hospedagem de recursos. Por exemplo, em vez de veicular tudo em assets.example.com, use 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 considera servidores separados e aumenta o número de conexões usadas para fazer o download de recursos. Saiba mais sobre essa técnica em Dividir componentes entre domínios nas Práticas recomendadas para acelerar seu site.

Nosso método de inicialização de download é chamado de downloadAll(). Vamos desenvolver isso ao longo do tempo. Por enquanto, esta é 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 para o evento de carregamento é adicionado e o src da imagem é definido, o que aciona o download real.

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

Como acompanhar o sucesso e a falha

Outro requisito é acompanhar os sucessos e as falhas, porque, infelizmente, nem tudo funciona perfeitamente. Até agora, o código só rastreia os recursos que foram baixados. Ao adicionar um listener de eventos para o evento de erro, você poderá capturar cenários de sucesso e de 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;
  }
}

O gerenciador de recursos precisa saber quantos sucessos e falhas encontramos. Caso contrário, ele nunca saberá quando o jogo pode começar.

Primeiro, vamos adicionar os contadores ao objeto no construtor, que agora fica assim:

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 têm esta aparência:

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 rastreia os recursos carregados e com falha.

Sinalização quando concluído

Depois que o jogo colocar os recursos em fila para download e solicitar que o gerenciador de recursos faça o download de todos eles, o jogo precisa ser informado quando todos os recursos forem transferidos. Em vez de perguntar várias vezes se os recursos foram baixados, o gerenciador de recursos pode sinalizar o jogo.

O gerenciador de recursos precisa saber primeiro quando todos os recursos são concluídos. Vamos adicionar um método isDone agora:

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

Ao comparar o successCount + errorCount com o tamanho da downloadQueue, o gerenciador de recursos sabe se todos os recursos foram concluídos ou tiveram algum tipo de erro.

Claro, saber se o trabalho foi concluído é apenas metade da batalha. O gerenciador de recursos também precisa verificar esse método. Vamos adicionar essa verificação nos 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, vamos verificar se esse foi o último recurso na fila. Se o gerenciador de recursos realmente terminar o download, o que devemos fazer?

Se o gerenciador de recursos terminar de fazer o download de todos os recursos, vamos chamar um método de callback. Vamos mudar downloadAll() e adicionar um parâmetro para o callback:

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

Vamos chamar o método downloadCallback dentro dos 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 está pronto para o último requisito.

Recuperação fácil de recursos

Depois que o jogo receber o sinal de que pode ser iniciado, ele vai começar a renderizar imagens. O gerenciador de recursos é responsável por fazer o download e rastrear os recursos, além de fornecê-los ao jogo.

Nosso requisito final implica algum tipo de método getAsset. Vamos adicioná-lo agora:

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

Esse objeto de cache é inicializado no construtor, que agora tem esta 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 bug

Você encontrou o bug? Como escrito acima, o método isDone só é chamado quando os eventos de carregamento ou erro são acionados. Mas e se o gerenciador de recursos não tiver recursos na fila de download? O método isDone nunca é acionado, e o jogo nunca começa.

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. O bug foi corrigido.

Exemplo de uso

O uso desse gerenciador de recursos no seu jogo HTML5 é bastante simples. 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 gerenciador de recursos
  2. Enfileirar recursos para download
  3. Iniciar os downloads com downloadAll()
  4. Sinalizar quando os recursos estiverem prontos invocando a função de callback
  5. Extrair recursos com getAsset()

Áreas para melhoria

Você vai precisar de um gerenciador de recursos mais avançado à medida que desenvolve seu jogo, mas espero que este tenha sido um começo básico. Os recursos futuros podem incluir:

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

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

Fonte completa

O código-fonte desse gerenciador de recursos e do jogo que ele abstrai é de código aberto sob a Licença Apache e pode ser encontrado na conta do GitHub da Bad Aliens. O jogo Bad Aliens pode ser jogado no seu navegador compatível com HTML5. Esse jogo foi o tema da minha palestra no Google IO, intitulada Super Browser 2 Turbo HD Remix: Introdução ao desenvolvimento de jogos em HTML5 (slides, vídeo).

Resumo

A maioria dos jogos tem algum tipo de gerenciador de recursos, mas os jogos HTML5 exigem um gerenciador que carregue os recursos por uma rede e lide com falhas. Este artigo descreveu um gerenciador de recursos simples que pode ser usado e adaptado para seu próximo jogo HTML5. Divirta-se e deixe sua opinião nos comentários abaixo. Valeu!