Intelligence artificielle – jeux vidéo – pathfinding – déplacement

Suite de l’article Intelligence artificielle – jeux vidéo, passons aux choses sérieuses : la poursuite intelligente. Pour rappel, vous gérez l’affichage du labyrinthe avec ses murs et les 2 protagonistes matérialisés par le carré rouge pour le poursuivant et le carré jaune pour le poursuivi.

 

Petit préambule

Le labyrinthe est représenté sur un plan en 2 dimensions (2D) orienté comme ci-dessous.

 

Chaque élément (mur, ennemi, ou joueur) à afficher dans ce plan nécessite pour indiquer son emplacement, de spécifier ses coordonnées (abscisse et ordonnée) dans ce plan 2D.

 

Les protagonistes : des objets ?

Notre exemple met en scène un robot tueur qui poursuit un joueur. Du Berzerk tout craché.

Nous parlons donc d’au moins 2 protagonistes. Ils ont de fait une existence (certes virtuelle) propre. Pourraient ils être représentés sous forme d’objets ? Oui, oui, oui, et encore oui.

Cependant, un objet a du sens que s’il a des caractéristiques (les propriétés) et aussi des moyens pour agir (les méthodes). Et Ce qui vient à l’esprit en premier lieu est bien entendu leur positionnement sur l’écran : abscisse x et ordonnée y.

Pour créer ces objets, rien de plus simple.

let enemy = {
 x : 0,
 y : 0
}
 
let player = {
 x : 0,
 y : 0
}

Il serait aussi bon que le positionnement de chacun soit initialisé à partir du tableau du jeu. Je vous propose donc de modifier la fonction initGameGrid en ce sens.

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 ] = " ";
   }
  }
 }
}

Nous avons donc une bonne base pour faire du développement propre.

 

Choix de la direction à prendre

Je vous propose dans un premier temps que le poursuivant (enemy) cherche quelle direction prendre pour atteindre sa cible (player). Cela prend la forme d’une fonction qui renvoie une direction sur 4 possibles. N pour le nord, S pour le sud, E pour l’est, et W pour l’ouest.

On considère dans cet exemple que le nord est le haut de l’écran, le sud le bas, l’est la droite et l’ouest la gauche.

Le principe :
– calculer les distances verticale et horizontale qui séparent les 2 protagonistes;
– déterminer la direction à prendre sur chaque axe (vertical et horizontal);
– comparer les distances verticale et horizontale pour déterminer l’axe sur lequel l’enemy va se déplacer. On choisit l’axe sur lequel la distance est la plus grande.

La méthode ci-dessous prend en paramètres 2 objets enemy et player. Ils ont les 2 mêmes propriétés x et y indicatrices de leur positionnement sur l’écran.

Cette méthode se rattache à l’objet enemy puisqu’elle n’a de sens que pour lui. Il est le seul poursuivant.

Le code

let enemy = {
 x : 0,
 y : 0,
   
 currentDirection : null,
 
 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;
  }
    
 }
}

Se déplacer en fonction de la direction choisie

Ici, le déplacement est simple : la ligne droite jusqu’à la rencontre d’un obstacle.

N’oubliez pas l’orientation de notre labyrinthe : son origine est en haut à gauche (position x=0 et y=0):
– pour faire avancer vers la droite un mobile dans le labyrinthe, il faut changer sa position X en l’incrémentant;
– pour faire avancer vers la gauche un mobile dans le labyrinthe, il faut changer sa position X en la décrémentant;
– pour faire avancer vers le bas un mobile dans le labyrinthe, il faut changer sa position Y en l’incrémentant;
– pour faire avancer vers le haut un mobile dans le labyrinthe, il faut changer sa position Y en la décrémentant.

Incrémentez ou décrémentez la position du poursuivant sur l’axe choisi:
– si la direction à prendre est N, la position X du poursuivant va être décrémentée;
– si la direction à prendre est S, la position X du poursuivant va être incrémentée;
– si la direction à prendre est W, la position Y du poursuivant va être décrémentée;
– si la direction à prendre est E, la position Y du poursuivant va être incrémentée.

Je vous propose de procéder en trois temps :
enemy détermine son pas de déplacement en fonction de la position du joueur;
enemy se déplacer dans le labyrinthe;
– on matérialise son déplacement en l’affichant à l’écran.

Le pas de déplacement se fait en fonction de la direction à prendre stockée dans la propriété currentDirection.

Le pas de déplacement se fait verticalement ou horizontalement. Il y a donc 2 valeurs à renseigner.

Matérialisez ces 2 valeurs par les propriétés xStep et yStep qui sont respectivement les pas de déplacement horizontal et vertical.

La pas de déplacement prend 3 valeurs :
– horizontal xStep : 0 ne bouge pas, 1 avance vers la droite, -1 recule vers la gauche;
– vertical yStep : 0 ne bouge pas, 1 avance vers le bas, -1 recule vers le haut.

J’utilise les termes reculer et avancer qui résultent simplement de l’orientation du repère utilisé. Origine en haut à gauche, orienté vers la droite et le bas.

L’enemy se déplace horizontalement ou verticalement, pas les 2 à fois, il en résulte :
– lorsque xStep est différent de 0 yStep prend la valeur 0;
– lorsque yStep est différent de 0 xStep prend la valeur 0.

Le code

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;   
 }  
},

Maintenant il faut déplacer l’enemy dans le labyrinthe pour que cela se voit. Commencez par modifier les coordonnées d’enemy.

moveDSPToTarget : function() {
 this.x += this.xStep;
 this.y += this.yStep;
}

Enfin affichons le via la fonction showEnemy .

showEnemy(gameCanvasContext,enemy.x,enemy.y); 

Le résultat : un deuxième carré rouge de l’ennemi (l’écran ne se rafraichit pas, du coup l’ancienne position est toujours affichée).

Dans le prochain article Intelligence artificielle jeux vidéo – Eviter les obstacles, vous apprendrez à gérer les obstacles (murs) rencontrés par enemy.

Ne rate pas cette chance

 

Reçois ton livre GRATUIT pour créer des jeux vidéo.

Bravo, jette un œil à ta boite mail pour télécharger ton guide.