Gestion simple des éléments pour les jeux HTML5

Introduction

HTML5 a fourni de nombreuses API utiles pour créer des applications Web modernes, réactives et puissantes dans le navigateur. C'est génial, mais vous voulez vraiment créer des jeux et y jouer ! Heureusement, le format HTML5 a également marqué le début d'une nouvelle ère de développement de jeux, qui utilise des API telles que Canvas et de puissants moteurs JavaScript pour vous permettre de jouer directement dans votre navigateur sans nécessiter de plug-ins.

Cet article vous explique comment créer un composant de gestion des assets simple pour votre jeu HTML5. Sans gestionnaire d'éléments, votre jeu aura des difficultés à compenser les temps de téléchargement inconnus et le chargement asynchrone des images. Découvrez un exemple de gestionnaire d'assets simple pour vos jeux HTML5.

Le problème

Les jeux HTML5 ne doivent pas partir du principe que leurs assets (images ou audio, par exemple) se trouveront sur l'ordinateur local du joueur. En effet, les jeux HTML5 impliquent de jouer dans un navigateur Web avec des éléments téléchargés via HTTP. Comme le réseau est impliqué, le navigateur ne sait pas quand les éléments du jeu seront téléchargés et disponibles.

La méthode de base pour charger par programmation une image dans un navigateur Web est le code suivant:

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

Imaginez maintenant une centaine d'images à charger et à afficher au démarrage du jeu. Comment savoir que les 100 images sont prêtes ? Sont-elles toutes chargées ? Quand le jeu doit-il vraiment commencer ?

La solution

Laissez un gestionnaire d'assets gérer la mise en file d'attente des assets et faire un rapport au jeu lorsque tout est prêt. Un gestionnaire d'éléments généralise la logique de chargement des éléments sur le réseau et permet de vérifier facilement leur état.

Pour utiliser le gestionnaire d'actifs simple, vous devez respecter les exigences suivantes:

  • mettre des téléchargements en file d'attente
  • lancer les téléchargements
  • suivre les réussites et les échecs ;
  • signal lorsque tout est terminé
  • la récupération facile des actifs

Mise en file d'attente

La première exigence consiste à mettre les téléchargements en file d'attente. Cette conception vous permet de déclarer les éléments dont vous avez besoin sans les télécharger réellement. Cela peut être utile si, par exemple, vous souhaitez déclarer tous les éléments d'un niveau de jeu dans un fichier de configuration.

Le code du constructeur et de la mise en file d'attente se présente comme suit:

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

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

Démarrer les téléchargements

Une fois que vous avez placé tous les éléments à télécharger dans la file d'attente, vous pouvez demander au gestionnaire d'éléments de tous les télécharger.

Le navigateur Web peut heureusement charger les téléchargements en parallèle : généralement, jusqu'à 4 connexions par hôte. Pour accélérer le téléchargement des éléments, vous pouvez utiliser différents noms de domaine pour les héberger. Par exemple, au lieu de tout diffuser à partir de assets.example.com, essayez d'utiliser assets1.example.com, assets2.example.com, assets3.example.com, etc. Même si chacun de ces noms de domaine est simplement un CNAME vers le même serveur Web, le navigateur Web les considère comme des serveurs distincts et augmente le nombre de connexions utilisées pour le téléchargement des actifs. Pour en savoir plus sur cette technique, consultez l'article Répartir des composants sur plusieurs domaines dans les bonnes pratiques pour accélérer votre site Web.

Notre méthode d'initialisation des téléchargements s'appelle downloadAll(). Nous le développerons au fil du temps. Pour l'instant, voici la première logique pour simplement lancer les téléchargements.

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

Comme vous pouvez le voir dans le code ci-dessus, downloadAll() effectue simplement une itération dans la file d'attente de téléchargement et crée un objet Image. Un écouteur d'événements pour l'événement de chargement est ajouté, et la source de l'image est définie, ce qui déclenche le téléchargement réel.

Cette méthode permet de lancer les téléchargements.

Suivi des réussites et des échecs

Une autre exigence est de suivre à la fois les succès et les échecs, car malheureusement tout ne fonctionne pas toujours parfaitement. Jusqu'à présent, le code ne suit que les éléments correctement téléchargés. En ajoutant un écouteur d'événements pour l'événement d'erreur, vous pourrez capturer les scénarios de réussite et d'échec.

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

Notre gestionnaire d'assets doit savoir combien de succès et d'échecs nous avons rencontrés, sinon il ne saura jamais quand le jeu va commencer.

Tout d'abord, nous allons ajouter les compteurs à l'objet dans le constructeur, qui se présente désormais comme suit:

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

Ensuite, incrémentez les compteurs dans les écouteurs d'événements, qui se présentent maintenant comme suit:

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

Le gestionnaire d'éléments effectue désormais le suivi des éléments qui ont bien été chargés et ceux qui ont échoué.

Signalisation terminée

Une fois que le jeu a placé ses éléments dans la file d'attente de téléchargement et invité le gestionnaire d'éléments à les télécharger tous, le jeu doit recevoir un message lorsque tous les éléments ont été téléchargés. Au lieu de demander sans cesse au jeu si les éléments sont téléchargés, le gestionnaire d'éléments peut lui répondre.

Le gestionnaire d'assets doit d'abord savoir quand tous les composants sont terminés. Nous allons maintenant ajouter une méthode isDone:

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

En comparant successCount + errorCount à la taille de la file d'attente downloadQueue, le gestionnaire d'éléments sait si chaque élément a abouti ou a généré une erreur.

Bien sûr, savoir si c'est le cas ne représente que la moitié du travail. Le gestionnaire d'actifs doit également vérifier cette méthode. Nous allons ajouter cette vérification dans nos deux gestionnaires d'événements, comme le montre le code ci-dessous:

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

Une fois les compteurs incrémentés, nous verrons s'il s'agit du dernier élément de notre file d'attente. Si le téléchargement du gestionnaire d'éléments est terminé, que devons-nous faire exactement ?

Si le gestionnaire d'éléments a fini de télécharger tous les éléments, nous appelons bien sûr une méthode de rappel. Modifions downloadAll() et ajoutons un paramètre pour le rappel:

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

Nous allons appeler la méthode downloadCallback dans nos écouteurs d'événements:

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

Le gestionnaire d'assets est enfin prêt à répondre à la dernière exigence.

Récupération facile des actifs

Une fois que le jeu a été signalé qu'il peut démarrer, les images commencent à s'afficher. Le gestionnaire d'assets n'est pas seulement responsable du téléchargement et du suivi des assets, mais aussi de leur mise à disposition dans le jeu.

Notre dernière condition implique la méthode getAsset, et nous allons donc l'ajouter maintenant:

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

Cet objet de cache est initialisé dans le constructeur, qui se présente désormais comme suit:

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

Le cache est renseigné à la fin de downloadAll(), comme indiqué ci-dessous:

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

Bonus: correction de bugs

Avez-vous repéré le bug ? Comme indiqué ci-dessus, la méthode isDone n'est appelée que lorsque des événements de chargement ou d'erreur sont déclenchés. Mais que se passe-t-il si aucun élément n'est en attente de téléchargement dans le gestionnaire d'éléments ? La méthode isDone n'est jamais déclenchée et le jeu ne démarre jamais.

Vous pouvez gérer ce scénario en ajoutant le code suivant à downloadAll():

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

Si aucun élément n'est mis en file d'attente, le rappel est appelé immédiatement. Bug corrigé.

Exemple d'utilisation

L'utilisation de ce gestionnaire d'éléments dans votre jeu HTML5 est assez simple. Voici la méthode la plus simple pour utiliser la bibliothèque:

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

Le code ci-dessus illustre:

  1. Crée un gestionnaire d'éléments
  2. Ajouter des éléments à la file d'attente à télécharger
  3. Lancer les téléchargements avec downloadAll()
  4. Invoquer la fonction de rappel pour signaler que les éléments sont prêts
  5. Récupérer les éléments avec getAsset()

Domaines à améliorer

Ce simple gestionnaire d'éléments ne sera sans doute pas là au fur et à mesure que vous développerez votre jeu, même si j'espère qu'il vous a permis de partir sur de bonnes bases. Les fonctionnalités à venir pourraient inclure:

  • signalant l'élément concerné par une erreur
  • pour indiquer la progression
  • Récupérer des éléments à partir de l'API File System

Veuillez publier des améliorations, des duplications et des liens vers le code dans les commentaires ci-dessous.

Source complète

Le code source de ce gestionnaire d'éléments, ainsi que le jeu dont il est extrait, sont disponibles en Open Source sous la licence Apache et sont disponibles sur le compte GitHub Bad Aliens. Vous pouvez jouer au jeu Bad Aliens dans votre navigateur compatible avec HTML5. Ce jeu a fait l'objet de ma conférence Google IO intitulée Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development (diapositives, vidéo).

Résumé

La plupart des jeux disposent d'un gestionnaire d'éléments, mais les jeux HTML5 nécessitent un gestionnaire d'éléments qui charge les éléments sur un réseau et gère les échecs. Cet article présente un gestionnaire d'assets simple qui devrait être facile à utiliser et adapté à votre prochain jeu HTML5. Amusez-vous bien et n'hésitez pas à nous donner votre avis dans les commentaires ci-dessous. Merci !