Mis à jour le mardi 1 mars 2016
  • Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les personnages

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

Dans cette partie, nous allons commencer à intégrer un personnage (celui que le joueur dirige, mais une bonne partie du travail sera faite pour les PNJ (Personnages Non Joueurs)) au jeu. Nous étudierons comment faire en sorte de pouvoir le déplacer à l'écran.

À la fin de cette partie, le personnage ne se déplacera que très basiquement, c'est à dire qu'il se "téléportera" d'une case à l'autre. L'animation du déplacement se fera dans la prochaine partie.

Choix et rendu des sprites

Le fichier de sprite

Avant d'afficher nos personnages, il faut se mettre d'accord sur le format utilisé.

C'est quoi un sprite ?

Non non, il ne s'agit pas d'une boisson, mais d'une image. Dans la même idée que le tileset, le sprite permet de combiner toutes les images d'un personnage dans un seul fichier afin d'en réduire le nombre.

Comme ce n'est probablement pas clair, voici en image le format que j'ai utilisé pour ce tutoriel. Pour les connaisseurs, vous verrez qu'il s'agit du format utilisé par RPG maker (un format que je trouve assez facile à comprendre, mais surtout qui vous permettra de trouver beaucoup de ressources sur Internet) :

Sprite

(image tirée du site rpg-maker.fr)

Nous voyons donc que cette image en contient en réalité 16 (4 lignes et 4 colonnes). Étudions-la de plus près :

  • Sur la première ligne, il y a 4 images du personnage tourné vers le bas

  • Sur la seconde ligne, il y a 4 images du personnage tourné vers la gauche

  • Sur la troisième ligne, il y a 4 images du personnage tourné vers la droite

  • Sur la dernière ligne, il y a 4 images du personnage tourné vers le haut

Ensuite, pour chaque ligne, nous avons quatre images. Ces quatre images constitueront les quatre "frames" de l'animation du personnage (que nous étudierons un peu plus loin). Ce qui nous importe pour le moment, c'est la colonne de gauche. C'est là que l'on trouve l'image du personnage lorsqu'il est immobile.

Notez qu'il peut exister des images de sprites de différentes tailles, petites ou grandes.

Avant de passer à la suite, je vous invite à enregistrer notre personnage dans le dossier "sprites" que nous avions créé au début (je l'ai nommé "exemple.png", mais si vous avez une idée de nom plus original n'hésitez pas :p ).

La classe Personnage

Nos personnages auront quatre attributs :

  • Un pour l'image du sprite

  • Deux dans lesquels nous stockerons la taille de chaque partie de l'image (pour la calculer une seule fois dans le constructeur)

  • Deux pour la position du personnage en x et y

  • Un pour l'orientation du personnage

C'est ce dernier auquel nous allons nous intéresser. Il va falloir choisir quoi mettre dans notre variable. Plusieurs solutions sont alors possibles, comme stocker des chaînes de caractères ou des objets. Ce serait plus facile à gérer (quoique ...), mais ce serait tout sauf performant.
La solution pour laquelle j'ai opté, c'est d'utiliser des nombres entiers. C'est moins facile à lire pour nous, mais les performances seront meilleures. Seulement, il faut là aussi qu'on choisisse quel entier associer à quelle direction. Nous pourrions nous faciliter la vie en prenant des nombres logiques, allant par exemple dans le sens des aiguilles d'une montre (1 pour le haut, 2 pour la droite ...). Saut qu'à un moment donné, lorsque nous voudrons dessiner notre personnage, il faudra déterminer quelle portion de l'image prendre par rapport à cet attribut. Si nous prenions des entiers comme ceux-là, nous devrions avoir quatre conditions (if, else if, ou switch) pour trouver les coordonnées de cette portion.

Nous pouvons l'éviter en prenant des nombres ordonnés dans la même logique que le fichier de sprite. Nous aurons donc :

  • 0 pour le personnage orienté vers le bas

  • 1 pour le personnage orienté vers la gauche

  • 2 pour le personnage orienté vers la droite

  • 3 pour le personnage orienté vers le haut

Ainsi, nous pourrons déterminer la position de la ligne correspondant à la direction sur l'image en multipliant simplement la hauteur d'une ligne par l'entier représentant la direction du personnage.

Afin que ce soit plus facile à utiliser, je vous propose de définir des constantes (je les place dans le même fichier que la classe Personnage, mais tout en haut, en dehors de la classe elle-même).
Nous pouvons donc commencer à créer le fichier Personnage.js, dans le dossier js/classes (en pensant bien à l'inclure dans la page html) :

var DIRECTION = {
	"BAS"    : 0,
	"GAUCHE" : 1,
	"DROITE" : 2,
	"HAUT"   : 3
}

// Ici se trouvera la classe Personnage

Étant donné que nous avons tout ce qu'il faut, nous pouvons commencer à mettre en place la classe Personnage elle-même (le constructeur n'ayant pas grand-chose de plus complexe que pour la classe Tileset, je ne vais pas expliquer ce code, notamment pour ce qui est du chargement de l'image) :

function Personnage(url, x, y, direction) {
	this.x = x; // (en cases)
	this.y = y; // (en cases)
	this.direction = direction;
	
	// Chargement de l'image dans l'attribut image
	this.image = new Image();
	this.image.referenceDuPerso = this;
	this.image.onload = function() {
		if(!this.complete) 
			throw "Erreur de chargement du sprite nommé \"" + url + "\".";
		
		// Taille du personnage
		this.referenceDuPerso.largeur = this.width / 4;
		this.referenceDuPerso.hauteur = this.height / 4;
	}
	this.image.src = "sprites/" + url;
}

Personnage.prototype.dessinerPersonnage = function(context) {
	// Ici se trouvera le code de dessin du personnage
}

Comme vous le voyez, la méthode dessinerPersonnage n'a besoin que d'un seul paramètre, qui est l'objet context sur lequel il faut dessiner le personnage, car nous avons toutes les données nécessaires pour dessiner notre personnage :

context.drawImage(
	this.image, 
	0, this.direction * this.hauteur, // Point d'origine du rectangle source à prendre dans notre image
	this.largeur, this.hauteur, // Taille du rectangle source (c'est la taille du personnage)
	(this.x * 32) - (this.largeur / 2) + 16, (this.y * 32) - this.hauteur + 24, // Point de destination (dépend de la taille du personnage)
	this.largeur, this.hauteur // Taille du rectangle destination (c'est la taille du personnage)
);

Il y a deux calculs importants dans ce code.

  • Le point x où nous allons dessiner notre personnage. Il faut y soustraire la moitié de la largeur du personnage. Autrement, si le personnage est plus large que la taille d'une case, il apparaîtra à droite de cette case. Il faut également y ajouter 16 (c'est-à-dire la moitié de la largeur d'une case), sinon il sera centré, mais par rapport au côté gauche de la case.

  • Le point y où nous allons dessiner notre personnage. Il faut y soustraire la hauteur du personnage et y ajouter 24 (les trois quarts de la hauteur d'une case, car si vous observez bien le sprite, il y a toujours une petite marge qu'il faut prendre en compte entre les pieds du personnage et le bas de l'image, mais sinon on aurait pris 32). Autrement, comme la plupart de nos personnages sont plus hauts qu'une case, c'est leur tête qui apparaîtrait au niveau de la case, alors que ce sont leurs pieds qui touchent le sol.

Intégration basique des personnages dans la carte

Il ne nous manque plus qu'un détail pour afficher nos personnages : décider où faire appel à cette méthode de dessin. J'ai choisi d'intégrer dans la classe Map une liste des personnages qu'il faut afficher. Comme plus tard nous aurons des décors, et que ces décors pourront aussi bien être au dessus qu'en dessous des personnages, il est plus simple de le faire comme ça. De plus, comme nous aurons des PNJ (personnages non joueurs : vendeurs, passants ...) qui seront décrits dans le code JSON de chaque map, il est plus simple que ce soit la map qui gère cela.

Pour le moment, nous allons donc utiliser un simple tableau en attribut pour cela (j'ai mis ce code juste au-dessus des déclarations de méthodes, mais vous pouvez le mettre où vous voulez dans le constructeur) :

// Liste des personnages présents sur le terrain.
this.personnages = new Array();

Il nous faut aussi une méthode pour remplir ce tableau :

// Pour ajouter un personnage
Map.prototype.addPersonnage = function(perso) {
	this.personnages.push(perso);
}

Et enfin, il faut que la méthode de dessin de la map dessine également nos personnages. Pour cela, j'ajoute (temporairement) le code suivant à la suite de la boucle, en bas de la méthode dessinerMap :

// Dessin des personnages
for(var i = 0, l = this.personnages.length ; i < l ; i++) {
	this.personnages[i].dessinerPersonnage(context);
}

Nous pouvons maintenant tester le dessin de nos personnages. Pour cela, je vais créer quatre personnages, chacun orienté dans une direction précise. Et pour être sûr qu'ils se positionnent aux bons endroits, je vais les placer sur les côtés de la carte, le regard tourné vers le centre. Pour cela, j'ajoute le code suivant au fichier rpg.js, tout en haut du fichier (mais en dessous de la ligne qui instancie la carte) :

map.addPersonnage(new Personnage("exemple.png", 7, 14, DIRECTION.HAUT));
map.addPersonnage(new Personnage("exemple.png", 7, 0, DIRECTION.BAS));
map.addPersonnage(new Personnage("exemple.png", 0, 7, DIRECTION.DROITE));
map.addPersonnage(new Personnage("exemple.png", 14, 7, DIRECTION.GAUCHE));

Si tout va bien vous devriez obtenir ceci :

Résultat de l'affichage des personnages

Clavier et boucle principale

Méthode de déplacement

Maintenant que nous pouvons afficher nos personnages, il fa falloir qu'ils puissent se déplacer. Pour le moment nous allons faire des déplacements très simples, c'est-à-dire que le personnage passera d'une case à l'autre sans animation.

Pour cela, nous allons avoir une seule méthode, à laquelle il faudra passer deux paramètres :

  • La direction vers laquelle on souhaite aller

  • La référence de l'objet map. Nous en aurons besoin pour faire plusieurs vérifications. Il faudra en premier lieu savoir si le joueur n'essaye pas d'aller en dehors de la carte, mais plus tard il faudra vérifier qu'il peut aller sur la case en question (est-ce un mur par exemple ?)

Nous allons avoir besoin de connaître la case où l'on souhaite aller, à partir des coordonnées courantes et de la direction souhaitée. Je vais pour cela créer une méthode, qui prend en paramètre une direction, et qui renvoie un objet contenant deux attributs (x et y), qui sont les coordonnées correspondant à la case située dans la direction donnée par rapport au point actuel du joueur. Cette méthode nous sera utile à plusieurs reprises (et ce sera plus propre car nous aurons obligatoirement besoin de faire plusieurs tests).

Voici donc nos deux méthodes :

Personnage.prototype.getCoordonneesAdjacentes = function(direction)  {
	var coord = {'x' : this.x, 'y' : this.y};
	switch(direction) {
		case DIRECTION.BAS : 
			coord.y++;
			break;
		case DIRECTION.GAUCHE : 
			coord.x--;
			break;
		case DIRECTION.DROITE : 
			coord.x++;
			break;
		case DIRECTION.HAUT : 
			coord.y--;
			break;
	}
	return coord;
}
	
Personnage.prototype.deplacer = function(direction, map) {
	// On change la direction du personnage
	this.direction = direction;
		
	// On vérifie que la case demandée est bien située dans la carte
	var prochaineCase = this.getCoordonneesAdjacentes(direction);
	if(prochaineCase.x < 0 || prochaineCase.y < 0 || prochaineCase.x >= map.getLargeur() || prochaineCase.y >= map.getHauteur()) {
		// On retourne un booléen indiquant que le déplacement ne s'est pas fait, 
		// Ça ne coute pas cher et ca peut toujours servir
		return false;
	}
		
	// On effectue le déplacement
	this.x = prochaineCase.x;
	this.y = prochaineCase.y;
		
	return true;
}

Le clavier

Nous allons maintenant modifier le fichier rpg.js. Nous allons supprimer nos quatre personnages de test précédents (les quatre appels à la méthode "addPersonnage), pour n'en mettre qu'un seul, qui sera le personnage du joueur :

var joueur = new Personnage("exemple.png", 7, 14, DIRECTION.BAS);
map.addPersonnage(joueur);

Nous allons maintenant nous intéresser à la gestion du clavier. Je ne vais pas faire un système "dynamique", c'est-à-dire que l'association d'une touche avec la fonction qu'elle déclenche sera écrite directement dans le code (mais rien ne vous empêche de faire quelque chose de plus personnalisable, via un menu d'options par exemple).
Nous allons commencer par détecter l'appui sur les touches. Attention ici, afin d'éviter des erreurs éventuelles, on va commencer à détecter l'appui sur une touche uniquement à partir du moment ou la page est totalement chargée.

Ajoutez donc le code suivant en bas, à l'intérieur de la fonction "windows.onload" :

// Gestion du clavier
window.onkeydown = function(event) {
	alert('test');
	return false;
}

Vous pouvez tester, vous allez voir que dès que vous appuierez sur une touche du clavier, le message "test" apparaîtra.
Ici, j'ai retourné faux, tout simplement pour éviter que le comportement normal de la touche ne soit utilisé. Imaginez que vous ayez des barres de défilement sur votre page : à chaque fois que vous appuierez sur une flèche pour vous déplacer, elles vont bouger avec. En retournant faux, ce comportement ne se produira pas.

Maintenant, il va falloir identifier les touches pour produire une action en fonction de celle qui a été appuyée.
Pour cela, il faut d'abord récupérer l'objet correspondant à l'événement. Ensuite, il faut récupérer le code de la touche appuyée. Le code est soit dans l'attribut which, soit dans l'attribut keyCode (la plupart du temps keyCode fonctionne, mais sous Firefox par exemple ce n'est pas le cas lorsqu'on appuie sur une lettre) :

var e = event || window.event;
var key = e.which || e.keyCode;
alert(key);

(pensez également à supprimer la ligne d'affichage du message de test, elle ne nous servira plus)

Maintenant, à chaque appui sur une touche, un nombre correspondant à cette touche s'affichera. Par exemple nous avons :

Touche

Code

Haut

38

Bas

40

Gauche

37

Droite

39

Entrée

13

Et on les trouve où ces codes ?

Je ne connais aucune documentation complète à ce sujet. Pour les trouver, j'utilise un alert, comme dans cet exemple.

Le reste du travail est assez simple, il suffit de tester le code et de déplacer notre personnage à gauche si le code de la touche est 37, à droite si le code est 39 ... Notez également que j'ai pensé aux joueurs qui préfèrent la combinaison ZQSD ou encore WASD à l'usage des flèches directionnelles ^^ :

switch(key) {
	case 38 : case 122 : case 119 : case 90 : case 87 : // Flèche haut, z, w, Z, W
		joueur.deplacer(DIRECTION.HAUT, map);
		break;
	case 40 : case 115 : case 83 : // Flèche bas, s, S
		joueur.deplacer(DIRECTION.BAS, map);
		break;
	case 37 : case 113 : case 97 : case 81 : case 65 : // Flèche gauche, q, a, Q, A
		joueur.deplacer(DIRECTION.GAUCHE, map);
		break;
	case 39 : case 100 : case 68 : // Flèche droite, d, D
		joueur.deplacer(DIRECTION.DROITE, map);
		break;
	default : 
		//alert(key);
		// Si la touche ne nous sert pas, nous n'avons aucune raison de bloquer son comportement normal.
		return true;
}

Vous pouvez maintenant tester le jeu, appuyez sur les flèches et TADAAAA ... Ça ne fonctionne pas !

La boucle principale

Reprenons dans l'ordre les grandes phases de l'exécution de notre jeu :

  • Chargement de la page. Pendant ce temps, on initialise nos objets et on charge nos images.

  • Fin du chargement de la page

  • On dessine notre carte

  • On associe l'événement window.onkeypress à notre fonction de détection

  • L'utilisateur appuie sur une touche, ce qui modifie ses coordonnées x et y

Vous ne trouvez pas qu'il manque quelque chose après cela ?

Notre carte n'est pas redessinée après l'appui sur une touche ! Ce qui veut dire que les coordonnées x et y du personnage sont bien modifiées en mémoire, mais qu'on ne peut pas le voir puisque nous n'avons dessiné qu'une seule image, et puis plus rien.

Nous allons donc utiliser un genre de "boucle principale" (vous avez peut-être déjà entendu parler de ce concept par ailleurs). La boucle principale, c'est une boucle qui tourne en tâche de fond et qui redessine l'écran en permanence pour le garder à jour. Une fois mise en place, il nous suffira donc de modifier les coordonnées du personnage en mémoire pour qu'il soit automatiquement redessiné à sa nouvelle place sur l'écran quelques fractions de secondes plus tard.

En réalité ici ce n'est pas une boucle que nous allons utiliser. Si nous faisions une boucle infinie en JavaScript, le navigateur resterait bloqué (et stopperait l'exécution du programme au bout d'un moment). Nous allons donc utiliser la fonction setInterval.
Cette fonction prend deux paramètres :

  • Un callback, c'est-à-dire une fonction (ou une chaîne contenant un code à exécuter, ce qui est moins propre et moins performant)

  • Un nombre en millisecondes

Une fois l'appel à cette fonction effectué, le callback sera automatiquement appelé par JavaScript toutes les n millisecondes, n étant le nombre passé en second paramètre.

Le premier paramètre sera simple : Il s'agira d'une fonction faisant un appel à la méthode de dessin de notre map.

Le cerveau humain est capable de discerner entre 20 et 30 images par seconde. Je vais donc régler le second paramètre à 40 millisecondes (1000 / 40 = 25 images par seconde, nous n'avons pas vraiment d'animation pour le moment, donc difficile d'évaluer avec précision ce que ça donnera, mais nous re-règlerons ce chiffre plus tard si l'animation n'est pas assez fluide).

Remplacez donc la ligne suivante de notre fichier :

map.dessinerMap(ctx);

par ceci :

setInterval(function() {
	map.dessinerMap(ctx);
}, 40);

Animation des déplacements

Nous allons maintenant nous attaquer à l'animation des personnages lors de leurs déplacements. Nous n'aurons besoin ici de ne modifier que la classe Personnage.

La mise en place de l'animation se déroulera en deux parties principales :

  • L'animation elle-même (c'est-à-dire qu'on fera en sorte que durant le déplacement, on voie les jambes du personnage bouger)

  • La mise en place d'un mouvement fluide (au lieu que le personnage se téléporte d'une case à l'autre, il y passera progressivement).

L'animation elle-même

Nous avons vu que notre fichier de sprite contient, pour chaque direction, quatre images (frames) différentes.
Si on y regarde de plus près, voici l'organisation des frames (de gauche à droite) :

  • La première image est celle où le personnage est immobile

  • Sur la seconde image, le personnage avance son pied droit

  • Sur la troisième, il a de nouveau les jambes parallèles (en fait, il s'est avancé sur son pied droit et s'apprête à avancer le gauche pour finir un pas)

  • Sur la quatrième, il avance le pied gauche

Ce schéma se répète à l'infini, une fois arrivé à la quatrième image, on repart à la première. Le personnage se retrouve donc droit, prêt à faire un autre pas.

Pour réaliser cette animation, il va nous falloir deux paramètres :

  • Un repère pour mesurer le temps

  • La durée (dans l'unité choisie) entre deux frames pour réaliser cette animation

Pour le second, c'est assez facile. Nous allons déclarer une constante, à laquelle nous donnerons une valeur initiale. Une fois que l'animation fonctionnera, il nous suffira de modifier cette valeur pour avoir une animation à la bonne vitesse.

Pour le repère temporel, nous avons deux choix :

  • Utiliser l'heure du système (c'est-à-dire le timestamp)

  • Compter le nombre de passages dans la boucle principale, c'est-à-dire le nombre de fois qu'on redessine le personnage sur l'écran (qui est régulier).

J'ai choisi la deuxième option pour une raison principale : si nous souhaitons mettre un système de pause dans le jeu (plus tard), il nous sera impossible de mettre aussi l'horloge en pause :p . Nous risquerions donc de rencontrer des décalages dans l'animation après les pauses (ce qui n'est pas un bug primordial, mais autant l'éviter autant que possible).

Pour la durée de chaque frame de notre animation, il nous faut simplement un entier. On peut donc déclarer cette constante (je la déclare juste après les constantes de direction) :

var DUREE_ANIMATION = 4;

Nous allons avoir besoin de certaines informations pour gérer cette animation au niveau du personnage :

  • L'état du personnage, c'est-à-dire si le personnage est immobile ou non

  • Si le personnage est en mouvement, le nombre de passages effectué par la boucle principale, afin d'afficher la bonne image.

Je vais combiner ça en un seul attribut, qui contiendra :

  • Un nombre négatif si le personnage est immobile (Nous utiliserons -1, mais il vaut mieux tester s'il est négatif plutôt que de vérifier s'il est exactement égal à -1. Ainsi, l'attribut peut être potentiellement réutilisable pour stocker des informations concernant l'animation).

  • Un nombre positif ou nul si le personnage est en mouvement. Dans ce cas, ce nombre est le nombre de passages effectués dans la boucle principale.

Nous pouvons donc initialiser notre attribut à -1, pour que le personnage soit immobile quand il apparaît dans le jeu :

this.etatAnimation = -1;

Au moment où le personnage commencera à se déplacer, il faudra démarrer l'animation, et donc mettre cet attribut à 1 (on pourrait aussi le mettre à 0, mais cela décale l'animation d'une frame et provoque un court arrêt de l'animation entre deux cases). Ajoutez donc ce code dans la méthode "deplacer", juste avant les deux lignes où les attributs x et y sont modifiés :

// On commence l'animation
this.etatAnimation = 1;

Ensuite, il faut calculer l'image à afficher en fonction de l'animation courante. Modifiez donc la méthode "dessinerPersonnage", et ajoutez ceci au début :

var frame = 0;
if(this.etatAnimation >= 0) {
	frame = Math.floor(this.etatAnimation / DUREE_ANIMATION);
	if(frame > 3) {
		frame %= 4;
	}
	this.etatAnimation++;
}

Par défaut, nous prenons donc l'image immobile, c'est-à-dire l'image 0 (il y a quatre images dans notre animation, que l'on compte de 0 à 3). Si l'animation est commencée, on calcule la bonne image à afficher (on compte le nombre total de "pas" effectués dans l'animation, c'est-dire le nombre de fois où l'on a changé d'image. Si le nombre est supérieur à trois (qui est le numéro correspondant à la dernière image de l'animation), l'image à prendre est ce nombre modulo quatre (le reste de la division de ce nombre par le nombre d'images dans l'animation).

Il ne nous reste plus qu'à utiliser ce nombre. Dans l'appel à la méthode "drawImage" de l'objet context, nous avions mis un paramètre à 0. Il nous suffit de le remplacer par le numéro de l'image que nous venons de calculer, multiplié par la largeur du personnage :

this.largeur * frame

Mise en place d'un mouvement fluide

Pour que le déplacement se fasse de manière fluide, et pas par "téléportation", il nous faut tout d'abord définir une vitesse de déplacement. Ce nombre nous indiquera combien de passages par la boucle principale seront nécessaires pour que le personnage passe d'une case à l'autre (les deux cases étant adjacentes). Je vais donc définir une constante, juste à côté de celle que nous avons déclarée pour l'animation :

var DUREE_DEPLACEMENT = 15;

Maintenant il faut définir un point important : lorsque le personnage se trouve entre deux cases, quelles sont ses coordonnées ?

  • Soit les coordonnées qu'il avait avant de se déplacer

  • Soit ses nouvelles coordonnées (on anticipe donc le déplacement)

J'ai choisi le deuxième cas pour que nous n'ayons pas de problèmes "d'accès concurrents" à l'avenir. Je m'explique : si nous avons un autre personnage à l'écran qui souhaite aller sur la même case que nous et que durant le déplacement nous sommes toujours (en mémoire) sur la case précédente, l'autre pourra venir sur cette case, et nous serons alors deux sur une même case. Or, dans un prochain chapitre (sur les obstacles), nous ferons en sorte qu'il ne puisse y avoir qu'un seul personnage par case !
De même, si on prend un peu le problème à l'envers, si un personnage souhaite aller sur la case dans laquelle nous nous trouvions avant le déplacement alors que nous sommes entre deux cases, il devra attendre que nous ayons terminé le déplacement dans le premier cas. Dans le deuxième cas, il pourra commencer son déplacement sans problème.

À partir du temps total nécessaire à un déplacement, et du temps passé depuis le début du déplacement (que nous avons déjà dans l'attribut "etatAnimation"), nous allons - à chaque dessin - pouvoir calculer quelle proportion (pourcentage) du déplacement a été effectuée (nous obtiendrons un nombre décimal entre 0 et 1) pour passer d'une case à l'autre. En multipliant ce chiffre par 32 (qui est la taille d'une case), nous obtiendrons le nombre de pixels de "décalage" à appliquer (c'est-à-dire à ajouter ou soustraire en fonction de la direction) à la position du personnage en pixels.

La première chose à laquelle il faut penser (car on l'oublie souvent, ce qui provoque des choses assez étranges, vous n'aurez qu'à commenter ce bloc quand nous aurons terminé, et vous verrez ;) ), c'est d'empêcher le personnage de se déplacer tant qu'il y a déjà un déplacement en cours. Pour cela, il nous suffit d'ajouter la ligne suivante au début de la fonction "deplacer" :

// On ne peut pas se déplacer si un mouvement est déjà en cours !
if(this.etatAnimation >= 0) {
	return false;
}

Pour l'autre partie du code (que j'ai déjà décrit dans les grandes lignes), je vais vous donner directement le bloc de code (commenté) à remplacer (les modifications seraient un peu complexes à détailler ligne par ligne). Dans la fonction dessinerPersonnage, remplacez le code suivant (que nous avons défini juste avant) :

var frame = 0;
if(this.etatAnimation >= 0) {
	frame = Math.floor(this.etatAnimation / DUREE_ANIMATION);
	if(frame > 3) {
		frame %= 4;
	}
	this.etatAnimation++;
}

par ceci :

var frame = 0; // Numéro de l'image à prendre pour l'animation
var decalageX = 0, decalageY = 0; // Décalage à appliquer à la position du personnage
if(this.etatAnimation >= DUREE_DEPLACEMENT) {
	// Si le déplacement a atteint ou dépassé le temps nécessaire pour s'effectuer, on le termine
	this.etatAnimation = -1;
} else if(this.etatAnimation >= 0) {
	// On calcule l'image (frame) de l'animation à afficher
	frame = Math.floor(this.etatAnimation / DUREE_ANIMATION);
	if(frame > 3) {
		frame %= 4;
	}
	
	// Nombre de pixels restant à parcourir entre les deux cases
	var pixelsAParcourir = 32 - (32 * (this.etatAnimation / DUREE_DEPLACEMENT));
	
	// À partir de ce nombre, on définit le décalage en x et y.
	// NOTE : Si vous connaissez une manière plus élégante que ces quatre conditions, je suis preneur
	if(this.direction == DIRECTION.HAUT) {
		decalageY = pixelsAParcourir;
	} else if(this.direction == DIRECTION.BAS) {
		decalageY = -pixelsAParcourir;
	} else if(this.direction == DIRECTION.GAUCHE) {
		decalageX = pixelsAParcourir;
	} else if(this.direction == DIRECTION.DROITE) {
		decalageX = -pixelsAParcourir;
	}
	
	this.etatAnimation++;
}
/*
 * Si aucune des deux conditions n'est vraie, c'est qu'on est immobile, 
 * donc il nous suffit de garder les valeurs 0 pour les variables 
 * frame, decalageX et decalageY
 */

Il ne nous reste plus qu'à additionner nos variables decalageX et decalageY aux paramètres passés à la méthode drawImage. Ce qui nous donne ceci pour les 6ème et 7ème paramètres :

// Point de destination (dépend de la taille du personnage)
(this.x * 32) - (this.largeur / 2) + 16 + decalageX, (this.y * 32) - this.hauteur + 24 + decalageY,

Et voilà, si vous avez bien suivi les étapes, vous devriez pouvoir vous déplacer librement dans le jeu, ce qui devrait donner quelque chose comme ceci (ce gif est beaucoup moins beau et fluide que dans la réalité) :

Fin de la mise en place des personnages !

Nous sommes maintenant capables d'afficher une carte de n'importe quelle taille sur l'écran, ainsi que des personnages. Bien sûr notre jeu est loin d'être complet, mais ce premier résultat n'est-il pas satisfaisant ?

Exemple de certificat de réussite
Exemple de certificat de réussite