Intelligence artificielle – jeux vidéo – pathfinding – les obstacles
Suite de la série consacrée à l’intelligence artificielle appliquée au jeu vidéo : la gestion des obstacles.
Je vous conseille de lire les articles précédents dans l’ordre :
– Intelligence artificielle – jeux vidéo javascript – crash and turn
– Intelligence artificielle – jeux vidéo javascript – Gestion du déplacement
Préambule à la gestion des obstacles
Je vous invite à lire l’article Initialiser le développement d’un jeu vidéo pour en reprendre le code.
// début du code isolé
(function () {
let requestAnimId;
let initialisation = function() {
// le code de l'initialisation
requestAnimId = window.requestAnimationFrame(main); // premier appel de main au rafraichissement de la page
}
let main = function() {
// le code du jeu
requestAnimId = window.requestAnimationFrame(main); // rappel de main au prochain rafraichissement de la page
}
window.onload = initialisation; // appel de la fonction initialisation au chargement de la page
})();
// fin du code isolé
On intègre dans le code ci-dessus, le code développé jusqu’à présent.
// début du code isolé
(function () {
let requestAnimId;
let gameDiv;
let gameCanvasContext;
let enemy = {
x : 0,
y : 0,
currentDirection : null,
xStep : 0,
yStep : 0,
searchDirectionToTarget : function(enemy, player) {
let xDirection;
let yDirection;
let xDist;
let yDist;
if ( enemy.x > player.x ) {
xDist = enemy.x - player.x;
xDirection = "W";
} else {
xDist = player.x - enemy.x;
xDirection = "E";
}
if ( enemy.y > player.y ) {
yDist = enemy.y - player.y;
yDirection = "N";
} else {
yDist = player.y - enemy.y;
yDirection = "S";
}
if ( xDist > yDist ) {
this.currentDirection = xDirection;
} else {
this.currentDirection = yDirection;
}
},
setDSPStepFromCurrentDirection : function() {
switch ( this.currentDirection ) {
case "E":
this.xStep = 1;
this.yStep = 0;
break;
case "W":
this.xStep = -1;
this.yStep = 0;
break;
case "N":
this.xStep = 0;
this.yStep = -1;
break;
case "S":
this.xStep = 0;
this.yStep = 1;
break;
default:
this.xStep = 0;
this.yStep = 0;
}
},
moveDSPToTarget : function() {
this.x += this.xStep;
this.y += this.yStep;
}
};
let player = {
x : 0,
y : 0
};
let createCanvasContext = function(name, width, height, zindex, htmlElement, color) {
let canvas = window.document.createElement("canvas");
canvas.id = name;
canvas.style.position = "absolute";
if ( color != undefined )
canvas.style.background = color;
canvas.style.zIndex = zindex;
canvas.width = width;
canvas.height = height;
if ( htmlElement != undefined )
htmlElement.appendChild(canvas.getContext('2d').canvas);
return canvas.getContext('2d');
}
let showSquare = function(canvasContext,color,x,y) {
canvasContext.beginPath();
canvasContext.rect(x*10,y*10,10,10);
canvasContext.fillStyle = color;
canvasContext.fill();
canvasContext.stroke();
}
let showWall = function(canvasContext,x,y) {
showSquare(canvasContext,"grey",x,y);
}
let showPlayer = function(canvasContext,x,y) {
showSquare(canvasContext,"yellow",x,y);
}
let showEnemy = function(canvasContext,x,y) {
showSquare(canvasContext,"red",x,y);
}
let initGameGrid = function() {
for(let i in walls) {
let line = walls[i];
let element = line.split("");
for(let j in element) {
if ( element[ j ] == "#" ) {
showWall(gameCanvasContext,j,i);
} else if ( element[ j ] == "E" ) {
showEnemy(gameCanvasContext,j,i);
enemy.x = parseInt(j);
enemy.y = parseInt(i);
element[ j ] = " ";
} else if ( element[ j ] == "P" ) {
showPlayer(gameCanvasContext,j,i);
player.x = parseInt(j);
player.y = parseInt(i);
element[ j ] = " ";
}
}
}
}
let walls = {
0: " ",
1: " # ",
2: " # ",
3: " ",
4: " ",
5: " ",
6: " # ",
7: " # # ",
8: " # # ",
9: " E # P ",
10: " # ",
11: " # # ",
12: " # # ",
13: " # # ",
14: " ",
15: " ",
16: " ",
17: " # ",
18: " # ",
19: " "
}
let initialisation = function() {
// code init
gameDiv = document.getElementById("gameDiv");
gameCanvasContext = createCanvasContext("game",200,200,1,gameDiv,"#000000");
// read and display game grid
initGameGrid();
enemy.searchDirectionToTarget(enemy,player);
enemy.setDSPStepFromCurrentDirection();
enemy.moveDSPToTarget();
showEnemy(gameCanvasContext,enemy.x,enemy.y);
showPlayer(gameCanvasContext,player.x,player.y);
requestAnimId = window.requestAnimationFrame(main);
}
let main = function() {
// main code
requestAnimId = window.requestAnimationFrame(main);
}
window.onload = initialisation; // appel de la fonction initialisation au chargement de la page
})();
Mouvement animé
Modifiez le code existant de façon à donner l’impression de mouvement de l’enemy dans le labyrinthe.
Vous devez donc :
– effacer le canvas;
– afficher le labyrinthe;
– rechercher la meilleure direction à prendre pour l’enemy (N, S, E, W);
– déplacer l’enemy;
– afficher l’enemy;
– afficher le player.
Tout ça dans une boucle infinie.
Première chose, la fonction d’effacement du canvas html5.
let clearCanvas = function(name, canvasContext) {
let canvas = document.getElementById(name);
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
}
Ensuite une fonction displayWall qui affiche uniquement les murs, très inspirée de la fonction initGameGrid.
let displayWall = function() {
for(let i in walls) {
let line = walls[i];
let element = line.split("");
for(let j in element) {
if ( element[ j ] == "#" ) {
showWall(gameCanvasContext,j,i);
}
}
}
}
Puis par le biais de la fonction main, on appelle en boucle les 2 fonctions précédentes ajoutées aux fonctions de recherche de direction, de choix de direction, de mouvement et d’affichage.
let main = function() {
// main code
clearCanvas("game", gameCanvasContext);
displayWall();
enemy.searchDirectionToTarget(enemy,player);
enemy.setDSPStepFromCurrentDirection();
enemy.moveDSPToTarget();
showEnemy(gameCanvasContext,enemy.x,enemy.y);
showPlayer(gameCanvasContext,player.x,player.y);
requestAnimId = window.requestAnimationFrame(main);
}
Je vous invite à alléger la fonction initialisation pour éviter un double appel inutile à un même code, le double appel concernant.
enemy.searchDirectionToTarget(enemy,player);
enemy.setDSPStepFromCurrentDirection();
enemy.moveDSPToTarget();
showEnemy(gameCanvasContext,enemy.x,enemy.y);
showPlayer(gameCanvasContext,player.x,player.y);
La fonction initialisation devient.
let initialisation = function() {
// code init
gameDiv = document.getElementById("gameDiv");
gameCanvasContext = createCanvasContext("game",200,200,1,gameDiv,"#000000");
// read and display game grid
initGameGrid();
requestAnimId = window.requestAnimationFrame(main);
}
Si tout se passe bien, vous devez voir un carré rouge (enemy) traversant l’écran de la gauche vers la droite.
Remarquez, tel un fantôme, enemy traverse les murs.
Franchissement des obstacles
Voilà le gros du travail qui consiste à franchir un obstacle en s’approchant le plus possible du joueur. C’est plus simple à dire qu’à faire.
Jusqu’à présent, l’enemy se déplace verticalement et horizontalement dans la direction du joueur.
Pour éviter qu’il ne traverse les murs, l’enemy doit vérifier avant d’avancer s’il y a un mur devant lui. Puis si c’est le cas, selon la position du joueur, se tourner à droite ou à gauche et aller droit devant lui. Il faut donc créer une fonction qui permet à l’enemy de vérifier s’il y a un mur ou pas devant lui.
En considérant que les bords du labyrinthe sont infranchissables, ils peuvent être apparentés à des murs. La fonction doit donc aussi prendre en compte les bords du labyrinthe.
Cette méthode utilise la direction qu’a pris l’enemy . On verifie seulement la présence d’un mur devant lui.
Cette méthode renvoie la valeur :
– vrai (true) s’il y a un mur ou le bord du labyrinthe devant;
– faux (false) dans le cas contraire.
Le code de la nouvelle méthode rattachée à l’objet enemy .
wallDSPInFrontOf : function() {
let returnValue = false;
switch ( this.currentDirection ) {
case "E":
if (this.x+1 >=20 || walls[this.y][this.x+1] == "#")
returnValue = true;
break;
case "W":
if (this.x-1 <0 || walls[this.y][this.x-1] == "#")
returnValue = true;
break;
case "N":
if (this.y-1 <0 || walls[this.y-1][this.x] == "#")
returnValue = true;
break;
case "S":
if (this.y+1 >=20 || walls[this.y+1][this.x] == "#")
returnValue = true;
break;
}
return returnValue;
}
Maintenant que l’on sait s’il y a un mur devant, l’enemy doit se tourner vers la droite ou la gauche en fonction de la position du joueur.
Pour simplifier, l’enemy doit rechercher une nouvelle direction en excluant la direction courante. Ceci nécessite donc de modifier la méthode searchDirectionToTarget afin qu’elle ne renvoie pas la direction du mur.
Il suffit de conditionner le changement de direction en fonction de la direction courante :
– si la direction courante est différente W et E, le choix de la direction est N ou S;
– si la direction courante est différente N et S, le choix de la direction est W ou E.
Le choix de la direction garde la nécessité de se rapprocher de la cible.
searchDirectionToTarget : function(enemy, player) {
let xDirection;
let yDirection;
let xDist;
let yDist;
if ( this.currentDirection != "E" && this.currentDirection != "W" ) {
if ( enemy.x > player.x ) {
xDist = enemy.x - player.x;
xDirection = "W";
} else {
xDist = player.x - enemy.x;
xDirection = "E";
}
}
if ( this.currentDirection != "N" && this.currentDirection != "S" ) {
if ( enemy.y > player.y ) {
yDist = enemy.y - player.y;
yDirection = "N";
} else {
yDist = player.y - enemy.y;
yDirection = "S";
}
}
if ( xDist > yDist ) {
this.currentDirection = xDirection != undefined ? xDirection : yDirection;
} else {
this.currentDirection = yDirection != undefined ? yDirection : xDirection;
}
}
Les obstacles sont gérés. Ils sont maintenant pris en compte, mais l’enemy tourne un peu en rond. Il doit maintenant atteindre sa cible. Ce sera l’objet du 4ème article sur l’algorithme crash and turn.
Pour la suite, c’est ici Intelligence artificielle jeux vidéo – Se déplacer vers la cible