Administración sencilla de recursos para juegos HTML5

HTML5 proporcionó muchas APIs útiles para compilar aplicaciones web modernas, responsivas y potentes en el navegador. Esto es genial, pero lo que realmente quieres es crear y jugar juegos. Por suerte, HTML5 también inauguró una nueva era de desarrollo de juegos que usa APIs como Canvas y potentes motores de JavaScript para ofrecer juegos directamente en tu navegador sin necesidad de complementos.

En este artículo, se explica cómo compilar un componente de administración de recursos simple para tu juego HTML5. Sin un administrador de recursos, tu juego tendrá dificultades para compensar los tiempos de descarga desconocidos y la carga de imágenes asíncrona. Sigue leyendo para ver un ejemplo de un administrador de recursos simple para tus juegos HTML5.

El problema

Los juegos HTML5 no pueden suponer que sus recursos, como imágenes o audio, estarán en la máquina local del jugador, ya que los juegos HTML5 implican que se reproducen en un navegador web con recursos descargados a través de HTTP. Debido a que la red está involucrada, el navegador no sabe con certeza cuándo se descargarán y estarán disponibles los recursos del juego.

La forma básica de cargar una imagen de forma programática en un navegador web es el siguiente código:

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

Ahora imagina tener cien imágenes que se deben cargar y mostrar cuando se inicia el juego. ¿Cómo sabes cuándo están listas las 100 imágenes? ¿Se cargaron correctamente? ¿Cuándo debería comenzar el partido?

La solución

Permite que un administrador de recursos controle la fila de recursos y le informe al juego cuando todo esté listo. Un administrador de recursos generaliza la lógica para cargar recursos a través de la red y proporciona una forma sencilla de verificar el estado.

Nuestro administrador de recursos simple tiene los siguientes requisitos:

  • poner en cola las descargas
  • iniciar descargas
  • hacer un seguimiento de los éxitos y las fallas
  • indicar cuando todo está listo
  • recuperación fácil de recursos

En cola

El primer requisito es poner las descargas en cola. Este diseño te permite declarar los recursos que necesitas sin descargarlos. Esto puede ser útil si, por ejemplo, quieres declarar todos los recursos de un nivel de juego en un archivo de configuración.

El código del constructor y de la cola se ve de la siguiente manera:

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

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

Iniciar descargas

Después de poner en cola todos los recursos que se descargarán, puedes pedirle al administrador de recursos que comience a descargarlos.

Por suerte, el navegador web puede realizar descargas en paralelo, por lo general, hasta 4 conexiones por host. Una forma de acelerar la descarga de recursos es usar un rango de nombres de dominio para alojarlos. Por ejemplo, en lugar de publicar todo desde assets.example.com, intenta usar assets1.example.com, assets2.example.com, assets3.example.com, etcétera. Incluso si cada uno de esos nombres de dominio es simplemente un CNAME para el mismo servidor web, el navegador web los ve como servidores independientes y aumenta la cantidad de conexiones que se usan para la descarga de recursos. Obtén más información sobre esta técnica en Cómo dividir componentes en varios dominios en Prácticas recomendadas para acelerar tu sitio web.

Nuestro método para la inicialización de la descarga se llama downloadAll(). Lo desarrollaremos con el tiempo. Por ahora, esta es la primera lógica para iniciar las descargas.

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 puedes ver en el código anterior, downloadAll() simplemente itera a través de downloadQueue y crea un nuevo objeto Image. Se agrega un objeto de escucha de eventos para el evento de carga y se establece el src de la imagen, lo que activa la descarga real.

Con este método, puedes iniciar las descargas.

Realiza un seguimiento del éxito y el fracaso

Otro requisito es hacer un seguimiento de los éxitos y los fracasos, ya que, lamentablemente, no todo funciona siempre a la perfección. Hasta ahora, el código solo realiza un seguimiento de los recursos que se descargaron correctamente. Si agregas un objeto de escucha de eventos para el evento de error, podrás capturar situaciones de éxito y de error.

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;
  }
}

Nuestro administrador de recursos necesita saber cuántas veces tuvimos éxito y cuántas veces fallamos, de lo contrario, nunca sabrá cuándo puede comenzar el juego.

En primer lugar, agregaremos los contadores al objeto en el constructor, que ahora se ve de la siguiente manera:

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

A continuación, incrementa los contadores en los objetos de escucha de eventos, que ahora se ven de la siguiente manera:

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

El administrador de recursos ahora realiza un seguimiento de los recursos cargados correctamente y de los que fallaron.

Señalización cuando se completa

Después de que el juego haya puesto en cola sus recursos para descargarlos y le haya pedido al administrador de recursos que los descargue, se le debe indicar al juego cuándo se descargaron todos los recursos. En lugar de que el juego pregunte una y otra vez si los recursos se descargaron, el administrador de recursos puede indicarle al juego que lo hizo.

El administrador de activos primero debe saber cuándo se termina cada activo. Ahora agregaremos un método isDone:

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

Cuando se compara successCount + errorCount con el tamaño de downloadQueue, el administrador de recursos sabe si todos los recursos finalizaron correctamente o tuvieron algún tipo de error.

Por supuesto, saber si está hecho es solo la mitad de la batalla; el administrador de recursos también debe verificar este método. Agregaremos esta verificación dentro de nuestros dos controladores de eventos, como se muestra en el siguiente código:

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);

Después de que se incrementen los contadores, veremos si ese fue el último activo de nuestra fila. Si el administrador de recursos ya terminó de descargarse, ¿qué debemos hacer exactamente?

Si el administrador de recursos terminó de descargar todos los recursos, por supuesto, llamaremos a un método de devolución de llamada. Cambiemos downloadAll() y agreguemos un parámetro para la devolución de llamada:

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

Llamaremos al método downloadCallback dentro de nuestros objetos de escucha 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);

El administrador de recursos ya está listo para el último requisito.

Recuperación fácil de recursos

Una vez que se le indique al juego que puede comenzar, comenzará a renderizar imágenes. El administrador de recursos no solo es responsable de descargar los recursos y hacer un seguimiento de ellos, sino también de proporcionarlos al juego.

Nuestro requisito final implica algún tipo de método getAsset, por lo que lo agregaremos ahora:

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

Este objeto de caché se inicializa en el constructor, que ahora se ve de la siguiente manera:

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

La caché se propaga al final de downloadAll(), como se muestra a continuación:

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>
  }
}

Bonificación: corrección de errores

¿Viste el error? Como se escribió anteriormente, solo se llama al método isDone cuando se activan eventos de carga o error. Pero ¿qué sucede si el administrador de activos no tiene ningún activo en cola para descargar? El método isDone nunca se activa y el juego nunca comienza.

Para adaptarte a esta situación, agrega el siguiente código a downloadAll():

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

Si no hay recursos en cola, se realizará la devolución de llamada de inmediato. Se corrigió el error.

Ejemplo de uso

El uso de este administrador de recursos en tu juego HTML5 es bastante sencillo. Esta es la forma más básica de usar la 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);
});

El código anterior ilustra lo siguiente:

  1. Crea un nuevo administrador de recursos
  2. Cómo poner en cola los recursos que se descargarán
  3. Inicia las descargas con downloadAll()
  4. Invoca la función de devolución de llamada para indicar cuándo los recursos están listos
  5. Cómo recuperar recursos con getAsset()

Áreas de mejora

Sin duda, superarás este simple administrador de recursos a medida que crees tu juego, aunque espero que te haya servido como punto de partida básico. Entre las funciones futuras, se incluyen las siguientes:

  • indicar qué activo tuvo un error
  • devoluciones de llamada para indicar el progreso
  • recuperar recursos de la API de File System

Publica mejoras, bifurcaciones y vínculos al código en los comentarios que aparecen a continuación.

Fuente completa

La fuente de este administrador de recursos y el juego del que se abstrae es de código abierto bajo la Licencia Apache y se puede encontrar en la cuenta de GitHub de Bad Aliens. Puedes jugar el juego Bad Aliens en tu navegador compatible con HTML5. Este juego fue el tema de mi charla en Google IO titulada Super Browser 2 Turbo HD Remix: Introducción al desarrollo de juegos HTML5 (diapositivas, video).

Resumen

La mayoría de los juegos tienen algún tipo de administrador de recursos, pero los juegos HTML5 requieren un administrador de recursos que cargue recursos a través de una red y controle las fallas. En este artículo, se describió un administrador de recursos simple que debería ser fácil de usar y adaptar para tu próximo juego HTML5. Diviértete y danos tu opinión en los comentarios. ¡Gracias!