Jeux HTML5: construction des objets principaux & gestion des collisions avec EaselJS
Nous avons vu dans l’article précédent comment animer des sprites avec EaselJS : Jeux HTML5: animation de sprites dans l’élément Canvas grâce à EaselJS
Nous allons maintenant voir comment créer certains des objets de base de notre jeu comme les ennemies ou le héro de notre jeu de plateforme. Nous verrons aussi comment implémenter un mécanisme basique de collision entre les 2. Cette fois-ci ce tutoriel sera principalement basé sur l’exemple suivant: EaselJS Game sample
Vous trouverez également un exemple fonctionnel à la fin de cet article. C’est la base d’un petit jeu.
Cette article est le 2ème 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
Construction de l’objet Monster
Cet objet dispose de 2 états :
1 – En train de courir sur la longueur de l’écran
2 – Etre en état de repos une fois l’un des bords de l’écran atteint avant de courir à nouveau.
Oui, je sais, la vie d’un monstre n’est pas palpitante. Mais c’est comme ça ! Attention, aussi stupide soit-il, si vous le touchez vous mourrez.
Cette fois-ci, j’ai fusionné en un unique fichier PNG les sprites venant de l’exemple XNA Platformer définissant les séquences où le personnage court et où il reste immobile. Voici par exemple le PNG associé au monstre MonsterC :
Notre objet monstre est défini au sein du fichier Monster.js et utilise l’objet BitmapAnimation comme prototype qui se prête en effet très bien à ce genre de scénarios. Il contient tout ce dont nous avons besoin: une méthode tick() , un mécanisme de “hit testing” pour vérifier les collisions et une manière de gérer nos sprites sous la forme de plusieurs animations séparées.
Il nous faut juste y ajouter de la logique spécifique à notre monstre comme la gestion d’un timer gérant l’état “au repos” et cela devrait faire l’affaire. Voici ainsi le code du fichier Monster.js définissant donc nos ennemies dans le jeu :
(function (window) {
function Monster(monsterName, imgMonster, x_end) {
this.initialize(monsterName, imgMonster, x_end);
}
Monster.prototype = new BitmapAnimation();
// propriétés publiques
Monster.prototype.IDLEWAITTIME = 40;
Monster.prototype.bounds = 0;
//taille du cercle visible
Monster.prototype.hit = 0;
// constructeur:
// unique pour éviter d'écraser celui de la classe de base
Monster.prototype.BitmapAnimation_initialize = Monster.prototype.initialize;
// variables membres pour gérer l’état au repos
// et le temps à attendre avant de courir à nouveau
this.isInIdleMode = false;
this.idleWaitTicker = 0;
var quaterFrameSize;
Monster.prototype.initialize = function (monsterName, imgMonster, x_end) {
var localSpriteSheet = new SpriteSheet({
images: [imgMonster], //image à utiliser
frames: {width: 64, height: 64, regX: 32, regY: 32},
animations: {
walk: [0, 9, "walk", 4],
idle: [10, 20, "idle", 4]
}
});
SpriteSheetUtils.addFlippedFrames(localSpriteSheet, true, false, false);
this.BitmapAnimation_initialize(localSpriteSheet);
this.x_end = x_end;
quaterFrameSize = this.spriteSheet.getFrame(0).rect.width / 4;
// on commence à jouer la 1ère séquence
this.gotoAndPlay("walk_h");
// mise en place d’une ombre portée. Attention, gros impact sur les performances
// en fonction des navigateurs/plateformes matérielles.
this.shadow = new Shadow("#000", 3, 2, 2);
this.name = monsterName;
// 1 = droite & -1 = gauche
this.direction = 1;
// vitesse
this.vX = 1;
this.vY = 0;
// on saute directement à la 1ère frame de la séquence walk_h
this.currentFrame = 21;
}
Monster.prototype.tick = function () {
if (!this.isInIdleMode) {
// On bouge le sprite en fonction de la direction et de la vitesse
this.x += this.vX * this.direction;
this.y += this.vY * this.direction;
// On teste les bords de l’écran sinon notre sprite partirait vivre ailleurs (dans le cloud?)
if (this.x >= this.x_end - (quaterFrameSize + 1) || this.x < (quaterFrameSize + 1)) {
this.gotoAndPlay("idle");
this.idleWaitTicker = this.IDLEWAITTIME;
this.isInIdleMode = true;
}
}
else {
this.idleWaitTicker--;
if (this.idleWaitTicker == 0) {
this.isInIdleMode = false;
if (this.x >= this.x_end - (quaterFrameSize + 1)) {
// Nous avons atteint le côté droit de l'écran
// Nous devons maintenant marcher vers la gauche
this.direction = -1;
this.gotoAndPlay("walk");
}
if (this.x < (quaterFrameSize + 1)) {
// Nous avons atteint le côté gauche de l'écran
// Nous devons maintenant marcher vers la droite
this.direction = 1;
this.gotoAndPlay("walk_h");
}
}
}
}
Monster.prototype.hitPoint = function (tX, tY) {
return this.hitRadius(tX, tY, 0);
}
Monster.prototype.hitRadius = function (tX, tY, tHit) {
if (tX - tHit > this.x + this.hit) { return; }
if (tX + tHit < this.x - this.hit) { return; }
if (tY - tHit > this.y + this.hit) { return; }
if (tY + tHit < this.y - this.hit) { return; }
//test basé sur une distance à base de cercle
return this.hit + tHit > Math.sqrt(Math.pow(Math.abs(this.x - tX), 2) + Math.pow(Math.abs(this.y - tY), 2));
}
window.Monster = Monster;
} (window));
La collision est gérée par les méthodes hitPoint() et hitRadius() . Le “hit testing” est fait via des cercles ce qui est moins précis qu’un modèle de boites.
Construction de l’objet Player
En tant que digne représentant du joueur humain qui l’anime, la logique associée au personnage principal du jeu est un peu différente des monstres et bien entendu plus évoluée (quoique…) !
Par exemple, les positions x & y sont normalement contrôlées par l’utilisateur souhaitant bouger son personnage avec son clavier. Par ailleurs, notre héro dispose de plus d’animations que nos monstres puisqu’il peut mourir, sauter, bouger, célébrer sa victoire et être immobile.
Voici ainsi le PNG qui lui est dédié :
Bon, dans ce tutoriel, on va rester simple. Nous allons uniquement gérer les séquence où il court, il meurt et où il reste immobile. Cependant, chargeons malgré tout toutes les animations pour un potentiel usage futur. Voici le code du fichier Player.js. La lecture du code et de ses commentaires devraient vous fournir suffisamment de détails pour en comprendre son fonctionnement :
(function (window) {
function Player(imgPlayer, x_start, x_end) {
this.initialize(imgPlayer, x_start, x_end);
}
Player.prototype = new BitmapAnimation();
// propriétés publiques
Player.prototype.bounds = 0;
Player.prototype.hit = 0;
Player.prototype.alive = true;
// constructeur:
// unique pour éviter d'écraser celui de la classe de base
Player.prototype.BitmapAnimation_initialize = Player.prototype.initialize;
//unique to avoid overiding base class
var quaterFrameSize;
Player.prototype.initialize = function (imgPlayer, x_end) {
var localSpriteSheet = new SpriteSheet({
images: [imgPlayer], // image à utiliser
frames: { width:64, height:64, regX:32, regY: 32 },
animations: {
walk: [0, 9, "walk", 4],
die: [10, 21, false, 4],
jump: [22, 32],
celebrate: [33, 43],
idle: [44, 44]
}
});
SpriteSheetUtils.addFlippedFrames(localSpriteSheet, true, false, false);
this.BitmapAnimation_initialize(localSpriteSheet);
this.x_end = x_end;
quaterFrameSize = this.spriteSheet.getFrame(0).rect.width / 4;
// La 1ère séquence jouée est celle "au repos"
this.gotoAndPlay("idle");
this.isInIdleMode = true;
// mise en place des ombres
this.shadow = new Shadow("#000", 3, 2, 2);
this.name = "Hero";
// 1 = droite & -1 = gauche
this.direction = 1;
// vitesse
this.vX = 4;
this.vY = 0;
// on commence directement à la 1ère frame de la séquence walk_h (droite)
this.currentFrame = 66;
// Taille des bords du cercle pour les tests de collisions
this.bounds = 28;
this.hit = this.bounds;
}
Player.prototype.tick = function () {
if (this.alive && !this.isInIdleMode) {
// Test sur les bords de l’écran
// Le joueur est bloqué sur chacun des bords mais on souhaite qu'il continue
// de courir quand même
if ((this.x + this.direction > quaterFrameSize) && (this.x + (this.direction * 2) < this.x_end - quaterFrameSize + 1)) {
// On bouge le sprite en fonction de sa direction et sa vitesse
this.x += this.vX * this.direction;
this.y += this.vY * this.direction;
}
}
}
window.Player = Player;
} (window));
Le héro sera contrôlé à distance depuis la page principale.
Construction de l’objet Content Manager
En général, la première étape d’un jeu HTML5 est de tout simplement télécharger les ressources dont il a besoin avant de démarrer le jeu. Dans mon cas, vous trouverez un gestionnaire de téléchargements et de ressources très basique au sein du fichier ContentManager.js :
En voici le code :
// Utilisé pour télécharger toutes les ressources utiles
// hébergées sur le serveur web
function ContentManager() {
// fonction qui sera rappelée une fois que tous les éléments
// auront été téléchargés (fonction de callback)
var ondownloadcompleted;
// Nombre d'éléments à télécharger
var NUM_ELEMENTS_TO_DOWNLOAD = 15;
// Pour l'affectation de la méthode de callback
this.SetDownloadCompleted = function (callbackMethod) {
ondownloadcompleted = callbackMethod;
};
// Nous avons 4 types d’ennemies, 1 héro & 1 type de bloc
this.imgMonsterA = new Image();
this.imgMonsterB = new Image();
this.imgMonsterC = new Image();
this.imgMonsterD = new Image();
this.imgTile = new Image();
this.imgPlayer = new Image();
// le fond du jeu peut être créé à partir de 3 claques différents
// ces 3 calques existent en 3 versions
this.imgBackgroundLayers = new Array();
var numImagesLoaded = 0;
// méthode publique pour lancer le téléchargement
this.StartDownload = function () {
SetDownloadParameters(this.imgPlayer, "img/Player.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgMonsterA, "img/MonsterA.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgMonsterB, "img/MonsterB.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgMonsterC, "img/MonsterC.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgMonsterD, "img/MonsterD.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgTile, "img/Tiles/BlockA0.png", handleImageLoad, handleImageError);
// téléchargement des 3 claques * 3 versions
for (var i = 0; i < 3; i++) {
this.imgBackgroundLayers[i] = new Array();
for (var j = 0; j < 3; j++) {
this.imgBackgroundLayers[i][j] = new Image();
SetDownloadParameters(this.imgBackgroundLayers[i][j], "img/Backgrounds/Layer" + i
+ "_" + j + ".png", handleImageLoad, handleImageError);
}
}
}
function SetDownloadParameters(imgElement, url, loadedHandler, errorHandler) {
imgElement.src = url;
imgElement.onload = loadedHandler;
imgElement.onerror = errorHandler;
}
// notre gestionnaire d'évènement en cas de succès
function handleImageLoad(e) {
numImagesLoaded++
// Si tous les éléments ont été téléchargés avec succès
if (numImagesLoaded == NUM_ELEMENTS_TO_DOWNLOAD) {
numImagesLoaded = 0;
// On rappelle la méthode de callback mise en place par SetDownloadCompleted
ondownloadcompleted();
}
}
// appelée si une erreur survient pendant le téléchargement (une 404 par exemple)
function handleImageError(e) {
console.log("Error Loading Image : " + e.target.src);
}
}
Je sais. Il manque pas mal de choses pour en faire un vrai bon gestionnaire de contenu : une barre de progression sur l’état des téléchargements, un meilleur gestionnaire d’erreurs, l’utilisation éventuelle du localStorage, un code peut-être plus générique, etc. Mais bon, j’ai essayé d’en faire un le plus simple et facile à comprendre.
Rassemblons toutes les pièces dans la page principale
Maintenant que nous avons les blocs de base de notre jeu, on peut commencer à s’amuser à les assembler pour construire un jeu de plateforme extrêmement simple. Pour cela, je vous propose de revoir chacune des parties de la page principale hébergeant notre petit jeu.
Dans la méthode init() , nous instancions l’objet Stage d’EaselJS puis nous utilisons l’objet ContentManager vu juste précédemment pour télécharger l’ensemble de nos fichier PNG:
function init() {
// on récupère l’instance du canvas puis on charge les images
canvas = document.getElementById("testCanvas");
// création de l’objet Stage que l’on fait pointer vers notre canvas
stage = new Stage(canvas);
// on récupère la largeur et la hauteur du canvas pour de futurs calculs savants
screen_width = canvas.width;
screen_height = canvas.height;
contentManager = new ContentManager();
contentManager.SetDownloadCompleted(startGame);
contentManager.StartDownload();
}
Une fois le téléchargement fini, la fonction startGame() est appelée. La 1ère chose qu’elle fait est d’appeler la fonction CreateAndAddRandomBackground() . Cette fonction créée simplement un arrière-plan aléatoire en utilisant les possibilités offertes par les 3 claques. Puis, notre héro est créé et sa position verticale Y est déterminée de manière aléatoire. Juste en dessous du héro, on construit une plateforme très simple sur laquelle notre héro pourra faire son petit jogging. Pour finir, on construit les 4 objets de type Monster() à l’intérieur du tableau Monsters et on ajoute l’ensemble au jeu/au stage :
function startGame() {
// Nombre aléatoire pour positionner verticalement notre
// Héro & ses méchants ennemies sur 8 paliers possibles
var randomY;
CreateAndAddRandomBackground();
// Notre héro peut être déplacé avec les flèches (gauche, droite)
document.onkeydown = handleKeyDown;
document.onkeyup = handleKeyUp;
// On créé le héro
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Hero = new Player(contentManager.imgPlayer, screen_width);
Hero.x = 400;
Hero.y = randomY;
// Bloc sur lequel notre héro et éventuellement quelques ennemies pourront marcher
bmpSeqTile = new Bitmap(contentManager.imgTile);
bmpSeqTile.regX = bmpSeqTile.frameWidth / 2 | 0;
bmpSeqTile.regY = bmpSeqTile.frameHeight / 2 | 0;
// On prends le même motif que l'on duplique sur toute la largeur de l'écran
for (var i = 0; i < 20; i++) {
// On clone le motif original
var bmpSeqTileCloned = bmpSeqTile.clone();
// On positionne ses propriétés d’affichage
bmpSeqTileCloned.x = 0 + (i * 40);
bmpSeqTileCloned.y = randomY + 32;
// puis on l'ajoute dans les objets à afficher
stage.addChild(bmpSeqTileCloned);
}
// Notre collection personnelle de monstres
Monsters = new Array();
// Création du 1er type de monstres
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Monsters[0] = new Monster("MonsterA", contentManager.imgMonsterA, screen_width);
Monsters[0].x = 20;
Monsters[0].y = randomY;
// Création du 2nd type de monstres
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Monsters[1] = new Monster("MonsterB", contentManager.imgMonsterB, screen_width);
Monsters[1].x = 750;
Monsters[1].y = randomY;
// Création du 3eme type de monstres
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Monsters[2] = new Monster("MonsterC", contentManager.imgMonsterC, screen_width);
Monsters[2].x = 100;
Monsters[2].y = randomY;
// Alors d'après vous, on fait quoi là ? :)
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Monsters[3] = new Monster("MonsterD", contentManager.imgMonsterD, screen_width);
Monsters[3].x = 650;
Monsters[3].y = randomY;
// On ajoute tous les monstres à l'écran
for (var i=0; i<Monsters.length;i++){
stage.addChild(Monsters[i]);
}
// Puis on ajoute la compagnie
stage.addChild(Hero);
Ticker.addListener(window);
// On vise le meilleur taux possible (60 FPS)
// Et on utiliser requestAnimationFrame si disponible
Ticker.useRAF = true;
Ticker.setFPS(60);
}
A la fin de la page, vous trouverez 2 gestionnaires de clavier évident qui s’occupe de jouer les animations walk ou walk_h de notre héro en fonction des touches sur lesquelles vous presserez. Pour finir, la logique principale de notre jeu n’est finalement contenue que dans les quelques lignes de code présentes au sein de la méthode tick() :
function tick() {
// on parcourt notre collection de monstres
for (monster in Monsters) {
var m = Monsters[monster];
// On appelle explicitement la méthode tick
// de chacun des monstres pour appeler leur logique de mise à jour
m.tick();
// Si notre héro est toujours vivant mais s'il est trop proche
// de l'un des monstres...
if (Hero.alive && m.hitRadius(Hero.x, Hero.y, Hero.hit)) {
//...il doit mourir malheureusement! (la morale des jeux est ignoble)
Hero.alive = false;
// On joue alors l'animation de mort en fonction de
// la direction dans laquelle le héro courait
if (Hero.direction == 1) {
Hero.gotoAndPlay("die_h");
}
else {
Hero.gotoAndPlay("die");
}
continue;
}
}
// Mise à jour du héro
Hero.tick();
// Et on met le tout à jour
stage.update();
}
Ainsi, on vérifie à chaque tick (donc potentiellement toutes les 17 ms) si l’un des monstres ne serait pas en train de toucher notre héro à partir des paramètres de collisions que nous avons vu plus tôt. Si l’un des monstres est trop proche, notre malheureux héro doit alors mourir.
Jouez avec l’exemple complet !
Si vous avez tout lu jusque ici, je pense que vous avez amplement mérité de pouvoir jouer avec l’exemple qui est juste en dessous. A chaque fois que vous presserez le bouton “Start”, un nouvel arrière-plan sera généré et chacun des personnages (ennemies comme héro) seront placés à des positions différentes de manière aléatoire. Vous pouvez également faire bouger le héro avec les flèches gauche et droite de votre clavier.
Au fait, ne vous inquiétez pas. Comme vous ne pouvez pas sauter, il y a actuellement aucun moyen de gagner dans ce jeu. C’est donc un jeu 100% looser (le premier du genre ? ). Ce jeu est donc vivement déconseillé aux mauvais perdants.
Note : comme je vous l’ai indiqué plus haut, il n’y a pas de barre de progression sur l’état du téléchargement. Vous devrez donc attendre un “certain temps” avant de pouvoir jouer après avoir appuyé sur le bouton “Start”. C’est ce que l’on appelle communément une EUP (expérience utilisateur pourrie). Mais cela sera ensuite immédiat une fois le tout présent dans le cache du navigateur.
Vous pouvez également y jouer via ce lien : easelJSCoreObjectsAndCollision
Pour finir le jeu, il nous reste à gérer la séquence de saut du héro en utilisant un moteur physique assez simple et des collisions un peu plus poussées, à charger la musique et les effets sonores et finalement à charger les niveaux. Mais la base est bien là si vous souhaitez écrire votre propre petit jeu. Vous avez désormais toutes les cartes en main !
Mais si vous souhaitez analyser le jeu entier avec l’ensemble du code source disponible, rendez-vous dans le prochain article : HTML5 Platformer: portage complet du jeu XNA vers <canvas> grâce à EaselJS
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.4. 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 :
- BitmapSequence n’est désormais plus disponible et a été remplacé par BitmapAnimation
- Vous pouvez désormais simplement ralentir la boucle d’animation des sprites via la propriété frequency du SpriteSheet
- 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)
- Vous devez appeler explicitement la méthode tick() de chacun des objets dans un gestionnaire d’évènements global plutôt que de laisser un Ticker global appeler automatiquement chacun des implémentations du tick.
Comments
Anonymous
March 27, 2012
Il y a un petit souci (est-ce dû à mon naviguateur ?) on arrive pas à voir tes personnages dans l'exemple... Le background nickel mais les personnages ne sont pas visibles.Anonymous
March 27, 2012
Bonjour Alex, J'ai testé l'exemple avec succès sous IE9/IE10, Firefox 11 et Chrome 17. Tu as quoi comme navigateur? Bye, DavidAnonymous
April 04, 2012
pareil rien pour moi !Anonymous
April 04, 2012
Les gars, si vous me dites pas dans quelles conditions vous testez, je ne vais pas pouvoir faire grand chose! :)Anonymous
April 16, 2012
Excellent sous Firefox 11.0Anonymous
September 19, 2012
Excellent, merci pour tes tutos très bien commentés ! Sauf erreur de ma part, quelque chose manque pour le moment : le son.Anonymous
March 15, 2013
si vous travaillée avec html5 vous serez besoin du navigateur OPERAAnonymous
May 13, 2013
Vous serez mieux de pas commenter... Sinon, bravo pour le tuto