Jeux HTML5: animation de sprites dans l’élément Canvas grâce à EaselJS

Si vous souhaitez écrire un petit jeu dit “casual” en utilisant l’élément Canvas d’HTML5, vous devrez trouver un moyen de gérer vos sprites. Il existe plusieurs librairies susceptibles de vous aider pour écrire des jeux en “HTML5” comme ImpactJS ou CraftyJS par exemple. De mon côté, j’ai décidé de me pencher sur la librairie EaselJS qui a été utilisée pour écrire PiratesLoveDaisies : un jeu en HTML5 de “Tower Defense”. Cette super librairie fonctionne dans tous les navigateurs HTML5 modernes, les tablettes et téléphones iOS, Android et Windows Phone 8. Vous pourrez même écrire des jeux HTML5 pour le Windows Store de Windows 8!!! Bref, vous allez pouvoir viser un nombre conséquent de plateformes grace aux derniers progrès effectés en terme de performance par l’ensemble des navigateurs du marché.

Par exemple, si vous utilisez actuellement Windows 8, vous pouvez ainsi installer et jouer au jeu Pirates Love Daisies depuis le Windows Store ici: Pirates Loves Daisies pour Windows 8 . Ce jeu en HTML5 utilise le framework EaselJS que nous allons voir ensemble à travers différents articles.

Nous allons donc voir à travers 3 premiers petits tutoriels comment utiliser vos sprites existants et comment les animer pour au final fabriquer un jeu complet de plateforme.

Cet article est ainsi le 1er d’une série de 3 articles :

- Jeux HTML5: animation de sprites dans l’élément Canvas grâce à EaselJS
- Jeux HTML5: construction des objets principaux & gestion des collisions avec EaselJS
- HTML5 Platformer: portage complet du jeu XNA vers <canvas> grâce à EaselJS

Mais je suis ensuite allé plus loin avec 3 articles plus avancés utilisant le meme jeu et éléments graphiques :

- Tutorial: créez des applications avec HTML5 sur Windows Phone grâce à PhoneGap où je vous montre comment porter ce jeu sous PhoneGap
- Modernisez vos jeux HTML5 canvas partie 1: mise à l’échelle matérielle & CSS3 où j’utilise CSS3 3D Transform, Transition & Grid Layout pour améliorer l’expérience de jeu
- Modernisez vos jeux HTML5 canvas partie 2: Offline API, Drag’n’drop & File API où je rend le jeu fonctionnel même en mode déconnecté

Introduction

Sur le site officiel EaselJS, vous pouvez parcourir quelques exemples intéressants à décortiquer et une documentation assez sommaire. Nous allons prendre l’exemple nommé sprites comme base de travail. Nous allons également utiliser les ressources disponibles dans l’exemple XNA 4.0 Platformer. Pour ceux qui suivent régulièrement mon blog, vous savez peut-être que j’affectionne particulièrement ce petit jeu et que j’aime jouer avec son code pour apprendre. Voici quelques articles que j’ai déjà écrit à ce sujet :

- Portage du Platformer Starter Kit XNA 3.1 vers XNA 4.0 pour Windows Phone 7
- Silverlight 4 XNA Platformer Level Editor pour Windows Phone 7 

Entre temps, cet exemple de code a été mis à jour par nos équipes XNA et est désormais disponible pour les plateformes Xbox 360, PC & Windows Phone 7 ici: App Hub – platformer . Vous pouvez le télécharger pour jouer avec le jeu et surtout pour extraire les sprites afin de les utiliser avec EaselJS.

Dans cet article, nous allons ainsi utiliser ces 2 images PNG comme source de nos séquences de sprites:

Notre ennemi/monstre en train de courir:

qui contient 10 images/sprites différents.

Notre monstre au repos:

qui contient une séquence de 11 sprites différents.

Note : ces exemples ont été testés avec succès par votre serviteur dans IE9/IE9 Mobile/IE10/IE10 Mobile/Chrome 21/Firefox 15 & Opera 12 & iPad.

Tutoriel 1 : construction des objets SpriteSheet et BitmapAnimation

Nous allons commencer par faire bouger notre monstre en train de courir sur toute la largeur du canvas.

La 1ère étape consiste donc à charger la séquence complète contenue dans l’image PNG via ce code:

 var imgMonsterARun = new Image();

function init() {
    //récupère le canvas et charge les images, attention à bien attendre la fin de tous les téléchargements
    canvas = document.getElementById("testCanvas");

    imgMonsterARun.onload = handleImageLoad;
    imgMonsterARun.onerror = handleImageError;
    imgMonsterARun.src = "img/MonsterARun.png";
}

Ce code sera donc appelé en premier pour initialiser le contenu de notre jeu. Une fois chargé, nous pouvons démarrer le jeu. EaselJS expose un objet de type SpriteSheetpour gérer nos sprites. Ainsi en utilisant ce code :

 var spriteSheet = new createjs.SpriteSheet({
    // image à utiliser et à découper
    images: [imgMonsterARun], 
    // largeur, hauteur & point central de chacun des sprites
    frames: {width: 64, height: 64, regX: 32, regY: 32}, 
    animations: {    
        walk: [0, 9, "walk"]
    }
});

Nous indiquons que nous souhaitons créer une nouvelle séquence appelée “walk”qui sera fabriquée à partir de l’image appelée imgMonsterARun. Cette image sera divisée en 10 sous-images ayant une taille de 64x64 pixels chacune. Il constitue notre objet de base pour charger nos sprites et en créer des séquences. A noter que vous pouvez créer plusieurs séquences à partir de même fichier PNG si vous le souhaitez. C’est le cas de l’exemple avec les rats sur le site officiel d’EaselJS.

Maintenant, il nous faut utiliser l’objet BitmapAnimation. Cet objet va nous aider à animer notre séquence et à la positionner à l’écran. Revoyons ensemble le code d’initialisation de cette BitmapAnimation :

 // crée une instance de BitmapSequence pour afficher et animer une instance de SpriteSheet:
bmpAnimation = new createjs.BitmapAnimation(spriteSheet);

// On lance la séquence d’animation
bmpAnimation.gotoAndPlay("walk");

// mise en place d’une ombre portée. Cela peut couter cher en FPS en fonction du navigateur
bmpAnimation.shadow = new createjs.Shadow("#454", 0, 5, 4);

bmpAnimation.name = "monster1";
bmpAnimation.direction = 90;
bmpAnimation.vX = 4;
bmpAnimation.x = 16;
bmpAnimation.y = 32;

// On peut choisir exactement à quelle frame démarrer l’animation
bmpAnimation.currentFrame = 0;
stage.addChild(bmpAnimation);

Le constructeur de l’objet BitmapAnimation a simplement besoin d’un élément de type SpriteSheet comme paramètre. Ensuite, on donne un nom à la séquence, positionne quelques paramètres comme la vitesse et la position initiale de notre 1ère étape d’animation. Pour finir, on ajoute cette séquence aux éléments affichables à l’écran avec l’objet Stageet sa méthode addChild() .

La dernière étape consiste maintenant à décider ce que l’on souhaite faire dans la boucle d’animation. Cette boucle est appelée toutes les xxx millisecondes et vous permet de mettre à jour la position de vos sprites. Pour cela, EaselJS expose un objet Ticker qui nous fournit une horloge centralisée diffusée à un intervalle précis à tous les éléments souhaitant l’écouter. Si vous jetez un œil au code de Ticker.js, vous verrez que l’objet Ticker d’EaselJS 0.4 supporte désormais requestAnimationFrame() :

 var f = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame 
        || window.oRequestAnimationFrame || window.msRequestAnimationFrame;

Grâce à ce code, IE10, Firefox, Chrome et les futures versions d’Opéra sont déjà supportées. Par contre, EaselJS n’utilise pas par défaut requestAnimationFrame. Peut-être pour des raisons de compatibilité j’imagine.

Vous pouvez alors demander à l’utiliser explicitement (afin d’avoir potentiellement des animations plus efficaces) en positionnant la propriété booléen useRAF à vrai. Et si votre navigateur ne le supporte pas, ne vous inquiétez pas, le code basculera alors automatiquement sur setTimeout() universellement reconnu.

Bon, ensuite, tout ce qui vous reste à faire, c’est de vous abonner à l’évènement tick et d’implémenter une méthode .tick() qui sera rappelée pour vous. Ce code s’abonne par exemple à cet évènement sur l’objet global window :

 createjs.Ticker.addListener(window);
createjs.Ticker.useRAF = true;
// On vise le taux d’images/seconde optimal (60 FPS)
createjs.Ticker.setFPS(60);

Et voici le code qui sera rappelé toutes les 17 ms (lorsque cela sera possible) pour mettre à jour la position de notre monstre:

 function tick() {
    // On teste si le personnage n’arrive pas en bout d’écran à droite avant qu’il ne disparaisse à jamais!
    if (bmpAnimation.x >= screen_width - 16) {
        // Nous avons atteint le côté droit de notre écran
        // Nous devons maintenant marcher vers la gauche pour retourner à la positon initiale
        bmpAnimation.direction = -90;
    }

    if (bmpAnimation.x < 16) {
        // Nous avons atteint le côté gauche de notre écran
        // Nous devons maintenant marcher vers la droite
        bmpAnimation.direction = 90;
    }

    // On bouge le sprite en fonction de la direction et de la vitesse demandée
    if (bmpAnimation.direction == 90) {
        bmpAnimation.x += bmpAnimation.vX;
    }
    else {
        bmpAnimation.x -= bmpAnimation.vX;
    }

    // mise à jour de la scène de jeu
    stage.update();
}

Vous pouvez tester le résultat final ici :

Vous pouvez aussi naviguer vers cette URL : easelJSSpritesTutorial01 si vous souhaitez voir l’exemple de code complet.

Eh, mais elle est bizarre cette animation non ? Effectivement, il y a 2 problèmes :

1 – les étapes d’animation sont un peu étrange car le personnage semble boucler trop vite au sein de sa séquence de sprites.

2 – le personnage ne peut marcher normalement que de la droite vers la gauche. On a l’impression sinon qu’il se la raconte un peu en effectuant un Moonwalk dans l’autre sens. Clignement d'œil

Regardons comment le calmer un petit peu et fixer tout cela dans le 2ème tutoriel. .

Tutorial 2: contrôle de la vitesse d’animation et inversion des sprites

Pour corriger la vitesse de l’animation, la méthode la plus simple consiste à changer le paramètre frequency de l’objet animations contenu dans votre objet SpriteSheet. Cela est plus ou moins précisé dans la documentation: SpriteSheet . Voici le code pour construire ainsi le nouveau SpriteSheet :

 var spriteSheet = new createjs.SpriteSheet({
    images: [imgMonsterARun], 
    frames: { width: 64, height: 64, regX: 32, regY: 32 }, 
    // Pour ralentir les animations des sprites, on fixe la fréquence de rappel à 4 pour ralentir 4 fois
    animations: {
        walk: [0, 9, "walk", 4]
    }
});

Pour être cohérent, vous avez besoin également de changer la vélocité en changeant la propriété vX de 4 à 1 (logiquement également divisée par 4).

Pour ajouter de nouvelles frames afin de permettre au personnage de marcher normalement de la gauche vers la droite, nous devons inverser horizontalement chacune des frames du sprite. Cela tombe bien, EaselJS expose un objet SpriteSheetUtils qui contient une méthode addFlippedFrames() comme vous pouvez le voir dans le code source. Voici le code permettant de l’utiliser :

 SpriteSheetUtils.addFlippedFrames(spriteSheet, true, false, false);

Nous créons ainsi une nouvelle séquence qui sera nommée automatiquement “walk_h” basée sur la séquence “walk” que nous avons inversée horizontalement. Pour finir, voici le code qui s’occupe de savoir quelle séquence jouer en fonction de la position actuelle du personnage :

 function tick() {
    // On teste si le personnage n’arrive pas en bout d’écran à droite avant qu’il ne disparaisse
    if (bmpAnimation.x >= screen_width - 16) {
        // Nous avons atteint le côté droit de notre écran
        // Nous devons maintenant marcher vers la gauche pour retourner à la positon initiale
        bmpAnimation.direction = -90;
        bmpAnimation.gotoAndPlay("walk")
    }

    if (bmpAnimation.x < 16) {
        // Nous avons atteint le côté gauche de notre écran
        // Nous devons maintenant marcher vers la droite 
        bmpAnimation.direction = 90;
        bmpAnimation.gotoAndPlay("walk_h");
    }

    // On bouge le sprite en fonction de la direction et de la vitesse demandée
    if (bmpAnimation.direction == 90) {
        bmpAnimation.x += bmpAnimation.vX;
    }
    else {
        bmpAnimation.x -= bmpAnimation.vX;
    }

    // mise à jour de la scène de jeu
    stage.update();
}

Vous pouvez tester le résultat final ici :

Vous pouvez aussi naviguer vers cette URL : easelJSSpritesTutorial02 si vous souhaitez voir l’exemple de code complet.

Tutoriel 3 : chargement de plusieurs sprites et utilisation de plusieurs animations

Il est temps désormais de charger l’état “au repos” de notre monstre préféré. L’idée est donc de jouer l’animation où il court sur un aller/retour complet. Ensuite, fatigué, le monstre passera à l’état “au repos” à jamais.

Nous allons donc devoir charger plusieurs images PNG depuis le serveur web. Il est d’ailleurs très important d’attendre que toutes les ressources soient totalement téléchargées afin de commencer les opérations de dessin. Voici une manière très simple de s’en assurer :

 var numberOfImagesLoaded = 0;

var imgMonsterARun = new Image();
var imgMonsterAIdle = new Image();

function init() {
    //récupère le canvas et charge les images, attention à bien attendre la fin de tous les téléchargements 
    canvas = document.getElementById("testCanvas");

    imgMonsterARun.onload = handleImageLoad;
    imgMonsterARun.onerror = handleImageError;
    imgMonsterARun.src = "img/MonsterARun.png";

    imgMonsterAIdle.onload = handleImageLoad;
    imgMonsterAIdle.onerror = handleImageError;
    imgMonsterAIdle.src = "img/MonsterAIdle.png";
}

function handleImageLoad(e) {
    numberOfImagesLoaded++;

    // Nous ne démarrons pas le jeu tant que toutes les images ne sont pas téléchargées
    // Sinon vous risquez de commencer à dessiner sans la bonne ressource et de lever 
    // cet exception du DOM: INVALID_STATE_ERR (11) sur l’appel de la méthode drawImage
    if (numberOfImagesLoaded == 2) {
        numberOfImagesLoaded = 0;
        startGame();
    }
}

Ce code est très basique. Par exemple, il ne gère pas les erreurs correctement en tentant de télécharger à nouveau l’image problématique en cas de 1er échec. Si vous fabriquez un jeu, vous aurez besoin d’écrire votre propre gestionnaire de téléchargements de contenus si la librairie JavaScript que vous utilisez de l’implémente pas encore.

Pour ajouter la séquence “au repos” et positionner ses différents paramètres, il nous suffit d’utiliser le même type de code vu précédemment :

 var spriteSheetIdle = new createjs.SpriteSheet({
    images: [imgMonsterAIdle],
    frames: { width: 64, height: 64, regX: 32, regY: 32 }, 
    animations: {
        idle: [0, 10, "idle", 4]
    }
});

bmpAnimationIdle = new createjs.BitmapAnimation(spriteSheetIdle);

bmpAnimationIdle.name = "monsteridle1";
bmpAnimationIdle.x = 16;
bmpAnimationIdle.y = 32;

Ensuite, au sein de la méthode tick() , nous devons stopper l’animation où le monstre court une fois que ce dernier atteint à nouveau le côté gauche de l’écran. Puis ensuite, il suffit de jouer l’animation “au repos” à la place. Voici le code qui fait exactement cela :

 if (bmpAnimation.x < 16) {
    // Nous avons atteint le côté gauche de notre écran, il est temps de se reposer 
    bmpAnimation.direction = 90;
    bmpAnimation.gotoAndStop("walk");
    stage.removeChild(bmpAnimation);
    bmpAnimationIdle.gotoAndPlay("idle");
    stage.addChild(bmpAnimationIdle);
}

Vous pouvez tester le résultat final ici :

Vous pouvez aussi naviguer vers cette URL : easelJSSpritesTutorial03 si vous souhaitez voir l’exemple de code complet.

Voilà, c’est tout pour aujourd’hui ! Vous pouvez reprendre une activité normale. Mais si l’envie vous en prend, n’hésitez pas à jeter un œil à la suite ici : Jeux HTML5: construction des objets principaux & gestion des collisions avec EaselJS . C’est une 2ème partie indispensable à comprendre avant de réussir à écrire un jeu de plateforme complet comme nous le verrons dans le 3ème et dernier article.

David

Note : ce tutoriel a été écrit à l’origine pour EaselJS 0.3.2 en Juillet 2010 et a été mis à jour pour EaselJS 0.5 le 26/11/2012. Pour ceux d’entre vous qui aviez lu la version 0.3.2, voici les changements principaux de la v0.4 à connaitre ayant un impact sur cet article :

  1. BitmapSequence n’est désormais plus disponible et a été remplacé par BitmapAnimation
  2. Vous pouvez désormais simplement ralentir la boucle d’animation des sprites via la propriété frequency du SpriteSheet
  3. EaselJS 0.4 peut utiliser désormais requestAnimationFrame pour des animations potentiellement plus efficaces sur les navigateurs le supportant (comme IE10+, Firefox 4.0+ & Chrome via les préfixes appropriés)
  4. Depuis EaselJS 0.4.2 et 0.5, vous devez utiliser le namespace createjs avant de faire appel aux objets du framework.