Utilisation d’images et animations dans un jeu HTML5

Il se trouve qu’il y a peu de ressources sur les jeux HTML5 en ce moment, et surtout très peu sur la bonne façon de les faire. Je vous propose donc de jeter un œil à ma façon de faire. Ce tutoriel explique la méthode à suivre pour afficher des graphismes, et plus précisément un personnage animé.

Les animations de notre magnifique personnage tout droit venu de google image (clic droit enregistrer sous pour le récupérer) :

Spritesheet Etape 4

C’est quoi cette image bizarre ?

Cette image est un spritesheet : Il sert à décrire un élément du jeu animé (ici, le personnage. Ce spritesheet est une grosse image contenant tous les états d’animation du personnage.

Il est donc découpé en lignes, colonnes et cases :

  • Sur la première ligne, le personnage se déplace vers le bas. Alors que sur la deuxième, il se déplace vers la gauche
  • Lorsque l’on avance d’une colonne, l’animation avance d’un cran : Regardez comme les pieds du personnage avancent si l’on suit les colonnes de gauche à droite
  • Chaque case est définie par sa ligne et sa colonne commençant par 0. La case [0, 0] est donc la première étape de l’animation du personnage qui va vers le bas. La [0, 1] est la deuxième étape de cette animation, et la [1, 0] est quant à elle la première étape de l’animation vers la gauche.

Par exemple, l’animation de mario vers le bas est l’enchainement des 4 images de la ligne du haut :

Mario animé

Notre spritesheet est donc référencé en cases qui sont définies par :

  • Leur clé d’animation : L’étape de l’animation (l’avancement des pieds de mario), soit la colonne
  • Le type d’animation : Ici, les différentes directions du personnage, soit les lignes

Grille de spritesheet

Voici quelques exemples de valeurs pour les cases de notre spritesheet

Ok mais pourquoi utiliser une grosse image ? On peut pas simplement avoir une image pour chaque animation ?

On pourrait. Surtout dans notre cas où il y a peu d’images. Le problème, c’est qu’un jeu contient vite énormément d’images, et devoir charger plein d’images séparées est beaucoup plus lourd qu’une grosse image. De plus, il faudrait s’y retrouver dans les fichiers, etc.

Le spritesheet permet d’avoir une organisation claire dans les animations qui permet lors du développement de les utiliser assez facilement comme nous allons le voir.

Je sais pas pour vous, mais moi je la trouve moche notre page : Un tout petit canvas blanc avec une page toute blanche et une bordure noire… Mouais. On va améliorer ça un peu.

Dans le fichier html, changez le width et height du canvas pour mettre 900 et 540(taille de l’arrière plan qu’on utilisera juste après).

Si vous essayez, le canvas va être mal placé sur la page : changez le margin-left et margin-top dans le CSS en haut.

Je vous encourage à jouer un peu avec le design de la page (arrière plan, position du canvas, afficher le nom de votre jeu en haut…) : ça permet de se familiariser un peu avec le html/css et c’est toujours plus agréable d’avoir une jolie page :)

Pour commencer par quelque chose de simple, on va afficher un arrière-plan. Enregistrez l’image dans votre dossier de jeu (background.jpg dans mon cas)

Background

L’affichage, c’est assez simple. On utilise une instruction du canvas :

ctx.drawImage(image, x, y);

Cette instruction prend trois paramètres : une image, sa position en x, et sa position en y. Tout comme le fillrect, x et y démarrent au coin en haut à gauche.

Et comment je fais pour lui dire d’utiliser mon arrière-plan alors ?

Justement, c’est là que ça devient un peu chiant : Cette instruction prend une variable de type image. En javascript, la plupart des éléments html (body, image, paragraphe…) ont leur équivalent sous forme de variable. La fonction drawImage demande donc une variable image.

On va donc créer cette variable au début du jeu:

var background = new Image(); // On crée une image javascript
background.src = "background.jpg"; // On donne l'adresse de l'image à charger

C’est quoi ce background.src et ce new Image() ?

Ces deux choses sont l’occasion de faire une mini intro sur les objets et classes. Voici donc l’explication de la chose en version courte (si vous connaissez déjà le principe sautez ce paragraphe) :

Il existe certaines variables spéciales qui sont des objets. Ces variables ont des propriétés spécifiques, par exemple l’objet player pourrait avoir comme propriété sa vie et sa position. On accèderait à la vie en faisant player.life par exemple

Ici, on change la propriété “src” de notre image en faisant background.src = “background.png”, qui correspond au chemin de l’image.

Et notre image a été créée en faisant new Image(); en fait, Image est une classe. C’est un moule prédéfini d’objet avec ses propriétés précises.

En gros, quand on fait “new Image();” on demande à javascript “Crée-moi une nouvelle image vide steuplay”. Puis en changeant sa propriété src on lui dit où aller chercher son image.

On va pouvoir afficher notre arrière-plan. Modifions donc notre fonction render pour rajouter ça :

function render(){
    ctx.clearRect(0, 0, 500, 300);
    ctx.drawImage(background, 0, 0); // Ici on affiche le background en 0, 0
    ctx.fillStyle = "#222";
    ctx.fillRect(playerX, playerY, 10, 10);
}

Voilà, si vous lancez le jeu vous devriez avoir votre arrière-plan

Passons aux choses sérieuses : On sait afficher des images, on va donc tenter (et réussir, promis) de les animer.

Pour ça, on va se créer quelques variable pour décrire l’état de notre animation :

var	animationLength = 4, // Le nombre de clés d'animation (de colonnes)
	animationX = 0, // La position actuelle de l'anim en X
	animationY = 0, // La position actuelle de l'anim en Y
	playerImageWidth = 50, // La taille du personnage en largeur
	playerImageHeight = 69; // La taille du personnage en hauteur

Avec ça, on va pouvoir créer notre image du personnage et l’afficher.

Je vous laisse créer l’image : c’est le même principe que pour l’arrière-plan.

Pour l’affichage, on va avoir quelques subtilités. En effet : on ne veut pas afficher toute l’image, mais une partie de cette image (notre case d’animation actuelle).

Heureusement, la méthode drawImage a déjà prévu le coup, et existe en fait en trois versions qui permettent de préciser plus ou moins de détails. On va utiliser la version la plus complète :

ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);

Woh, c’est quoi tous ces paramètres ?

C’est beaucoup plus simple que ça en a l’air. En gros, il s’agit de deux rectangles. Vous vous souvenez du fonctionnement de ctx.fillRect ? Il demandait un rectangle défini par sa position (x, y) et sa taille (w, h).

Ici, on nous demande :

  • Le rectangle source (s) : la fenètre qu’on va prendre dans l’image d’origine (notre case d’animation, donc) défini par sa position (sx, sy) et sa taille (sw, sh)
  • Et le rectangle de destination (d) :  L’endroit où sera dessiné notre personnage (dx, dy), et sa taille (dw, dh)

Petite image explicative piquée à Mozilla :

Canvas_drawimage

Mettons-ça en application dans notre cas pour dessiner notre personnage.

ctx.drawImage(player, 	animationX * playerImageWidth,
// Le numéro de la case multipliée par la largeur d'une case donne la position de la case
	animationY * playerImageHeight,
 // Oui, vous pouvez sauter des lignes entre paramètres d'une fonction. C'est plus lisible
	playerImageWidth,
 // On ne veut qu'une case en largeur/hauteur
	playerImageHeight,
	playerX, 
// On dessine à la position du joueur, comme avant avec le fillrect...
	playerY,
	playerImageWidth,
 // Si vous vouliez, vous pourriez augmenter ces deux là pour afficher le joueur en zoomé
	playerImageHeight);

Dans la fonction render, on peut donc remplacer notre fillrect par ce drawImage :

function render(){
	ctx.clearRect(0, 0, 500, 300);
	ctx.drawImage(background, 0, 0);
	ctx.fillStyle = "#222";
	ctx.drawImage(playerImage,
		animationX * playerImageWidth,
		animationY * playerImageHeight,
		playerImageWidth,
		playerImageHeight,
		playerX,
		playerY,
		playerImageWidth,
		playerImageHeight);
}

A ce niveau, si tout se passe bien vous devriez avoir un petit Mario à la place de notre bon vieux carré noir. Pendant que vous y êtes, changez sa position de départ qui est un peu nulle. Pour ma part, j’ai mis 450;390 :)

Le plus gros du travail est fait : On sait récupérer notre case dans le spritesheet et l’afficher à l’écran. Il ne reste plus qu’une chose : Savoir quelle case est la bonne. Heureusement, c’est plutôt simple. On va commencer par ajouter une fonction animate  à notre gameloop :

function animate () {
	animationX++;
	animationX %= animationLength;
}
function run(){

	inputs();
	animate();
	update();
	render();

	window.requestAnimationFrame(run);
}

Que fait cette fonction animate ? Pour l’instant pas grand chose : Elle augmente animationX tout le temps, et le remet à 0 quand il dépasse la longueur de l’animation.

La deuxième ligne est une petite astuce : On utilise l’opérateur modulo (qui retourne le reste d’une division) pour empêcher la variable de dépasser son maximum (ici 4).

Quand animationX vaut un nombre inférieur à 4, le modulo retourne le reste (0, 1, 2, puis 3). Dès qu’animationX va valoir 4, le modulo va retourner 0 (4/4 = 0). En faisant animationX %= animationLength, on assigne donc 0 à cette variable à chaque fois qu’elle dépasse 4.

Bref. On a augmenté animationX. Or, animationX décrit notre étape d’animation. Cela veut dire qu’en fait, on fait avancer les jambes de notre perso. Si vous lancez le code, vous allez alors voir Mario bouger.

“Mais c’est nul il bouge beaucoup trop vite ! ”

En effet. Notre gameplay loop se faisant 60 fois par seconde je le rapelle, on change de case d’animation 60 fois par seconde. Du coup c’est un peu trop rapide.

Pour ralentir ça, on ne va animer le personnage qu’une fois toutes les X gameplay loops. Créons deux variables pour ça :

var animPace = 6,
lastAnim = 0;

animPace, c’est le nombre de gameplay loops entre deux animations. lastAnim, quant à elle, servira à stocker le nombre de loops passées.

On va utiliser ces variables pour compter le nombre de loops et n’animer que lorsque l’on dépasse les 6, un peu à la manière dont on remet l’animation à 0 quand elle dépasse la quatrième case. Notre fonction animate devient donc ça :

function animate () {
	if (lastAnim > animPace) { // On anime une fois toutes les six loops
		lastAnim = 0;
 // On reset le compteur de loop pour qu'il puisse recompter jusqu'à 6 après
		animationX++;
		animationX %= 4;
	} else { // On incrémente le compteur de loops en attendant d'animer
		lastAnim++;
	}
}

On a maintenant un personnage animé :D

Dernier détail, il ne change pas de direction si on tourne à gauche ou à droite ! Remédions-y tout de suite.

La direction de l’animation est décrite par animationY. Sur la première ligne mario va en bas, la deuxième à gauche, etc. Du coup, on doit regarder quelles touches de clavier sont pressées pour mettre à jour animationY dans notre fonction animate :

if (left) {
	animationY = 1;
} else if (right) {
	animationY = 2;
}

Ca ressemble à ça. Je vous laisse deviner quoi mettre pour les directions haut et bas.

Le personnage court dans le vide si l’on appuie sur aucune touche. Essayez de trouver un moyen pour qu’il ne s’anime pas quand aucune touche n’est appuyée, et remettre son anim à 0 (comme ça quand on s’arrête, le personnage est dirigé vers le bas en position naturelle, et pas au milieu d’un mouvement).

Vous pouvez jeter un oeil à mon résultat, et récupérer le code (clic droit, afficher la source) si vous êtes perdus. Pour un premier jet ça commence à ressembler à quelque chose, et c’est pas si moche 🙂

source: www.anthonypigeot.com

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s