Administración sencilla de recursos para juegos HTML5

Introducción

HTML5 ha proporcionado muchas API útiles para crear aplicaciones web modernas, receptivas y potentes en el navegador. Esto es genial, pero realmente quieres crear y disfrutar juegos. Afortunadamente, HTML5 también marcó el comienzo de una nueva era de desarrollo de juegos que utiliza APIs como Canvas y motores de JavaScript potentes para ofrecer videojuegos directamente a tu navegador sin la necesidad de usar complementos.

En este artículo, se explica cómo crear un componente simple de administración de recursos para tu juego HTML5. Sin un administrador de recursos, tu juego tendrá dificultades para compensar los tiempos de descarga desconocidos y la carga asíncrona de imágenes. Sigue el proceso 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 jugarán en un navegador web con recursos descargados a través de HTTP. Debido a que la red está involucrada, el navegador no está seguro de cuándo se descargarán y estarán disponibles los elementos del juego.

El siguiente código es la manera básica de cargar de manera programática una imagen en un navegador web:

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 inicie el juego. ¿Cómo sabes cuándo están listas las 100 imágenes? ¿Se cargaron todos correctamente? ¿Cuándo debería comenzar realmente el juego?

La solución

Deja que un administrador de recursos se encargue de colocar los recursos en una fila y los 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 manera fácil de verificar el estado.

Nuestro administrador de activos simple tiene los siguientes requisitos:

  • poner en cola las descargas
  • iniciar descargas
  • hacer un seguimiento de los éxitos y los errores
  • señal cuando todo esté listo
  • recuperación sencilla de elementos.

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, por ejemplo, si quieres declarar todos los recursos de un nivel de un juego en un archivo de configuración.

El código para el constructor y la puesta en 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 elementos que se descargarán, puedes pedirle al administrador de recursos que comience a descargar todo.

Por suerte, el navegador web puede paralelizar las descargas, generalmente hasta 4 conexiones por host. Una forma de acelerar la descarga de recursos es usar una variedad de nombres de dominio para el hosting de recursos. Por ejemplo, en lugar de publicar todo desde recursos.example.com, intenta usar recursos1.example.com, recursos2.example.com, recursos3.example.com, etc. Incluso si cada uno de esos nombres de dominio es simplemente un CNAME en el mismo servidor web, el navegador web los ve como servidores separados y aumenta la cantidad de conexiones utilizadas para la descarga de recursos. Consulta las prácticas recomendadas para acelerar tu sitio web si quieres obtener más información sobre esta técnica en Cómo dividir los componentes en los dominios.

Nuestro método para la inicialización de la descarga se llama downloadAll(). Lo iremos avanzando 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() solo itera a través de la 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.

Seguimiento de procesos exitosos y errores

Otro requisito es hacer un seguimiento tanto de los éxitos como los fracasos porque, lamentablemente, no todo siempre funciona a la perfección. Hasta el momento, el código solo hace un seguimiento de los recursos descargados correctamente. Si agregas un objeto de escucha de eventos para el evento de error, podrás capturar situaciones de éxito y fracaso.

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ántos éxitos y fracasos encontramos; de lo contrario, nunca sabrá cuándo puede comenzar el juego.

Primero, 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 hace un seguimiento de los recursos cargados correctamente y los que fallaron.

Señales cuando haya terminado

Después de que el juego haya puesto los elementos en una cola para descargar y le haya pedido al administrador de activos que los descargue, se le debe avisar al juego cuando se hayan descargado todos los elementos. En lugar de que el juego pregunte una y otra vez si se descargaron los elementos, el administrador de recursos puede indicarle al juego.

El administrador de recursos primero necesita saber cuándo están terminados todos los recursos. Ahora, agregaremos un método isDone:

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

Mediante la comparación desuccessCount + errorCount con el tamaño de DownloadQueue, el administrador de elementos sabe si cada elemento finalizó correctamente o tuvo algún tipo de error.

Por supuesto que saber si ya se ha hecho es solo la mitad de la batalla; el administrador de activos 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 aumentar los contadores, veremos si ese fue el último elemento de la cola. Si el administrador de activos ha terminado la descarga, ¿qué debemos hacer exactamente?

Si el administrador de recursos terminó de descargar todos los recursos, llamaremos a un método de devolución de llamada, por supuesto. 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 activos finalmente está listo para el último requisito.

Recuperación sencilla de recursos

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

El requisito final implica algún tipo de método getAsset, así que lo agregaremos ahora:

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

Este objeto de caché se inicializa en el constructor, que ahora tiene el siguiente aspecto:

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

Contenido adicional: Corrección de errores

¿Detectaste 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 activos en cola para su descarga? El método isDone nunca se activa y el juego nunca se inicia.

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

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

Si no hay elementos en cola, se llama de inmediato a la devolución de llamada. Se corrigió el error.

Ejemplo de uso

Utilizar este administrador de elementos 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);
});

En el código anterior se ilustra lo siguiente:

  1. Crea un nuevo administrador de activos.
  2. Agregar elementos a la fila que se descargarán
  3. Comienza las descargas con downloadAll()
  4. Indica cuándo los recursos están listos invocando la función de devolución de llamada.
  5. Recupera elementos con getAsset().

Áreas de mejora

No habrá dudas sobre este sencillo administrador de recursos a medida que desarrolles tu juego, aunque espero que te haya servido para comenzar. Las funciones futuras podrían incluir:

  • que indican qué recurso tiene 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.

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 puedes encontrarlos en la cuenta de GitHub de Bad Aliens. Puedes jugar al juego de Bad Aliens en un navegador compatible con HTML5. Este juego fue el tema de mi charla de Google IO titulada Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development (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 los recursos en una red y controle las fallas. En este artículo, se describe un administrador de elementos simple que debería serte fácil de usar y adaptar para tu próximo juego HTML5. Diviértete y danos tu opinión en los comentarios. ¡Gracias!