Intelligence artificielle – jeux vidéo – pathfinding – atteindre la cible

Suite de la série consacrée à l’intelligence artificielle appliquée au jeu vidéo : le déplacement vers la cible.

Je vous conseille de lire les articles précédents dans l’ordre :
Intelligence artificiellejeux vidéo javascript – crash and turn
Intelligence artificiellejeux vidéo javascript – Gestion du déplacement
Intelligence artificiellejeux vidéo javascript – Gestion des obstacles

 

Introduction à l’intelligence artificielle, la cible dans le jeu vidéo

Dans l’article précédent, notre enemy se déplaçe, évite les murs mais n’atteint pas la cible. Rendons le un peu moins bête en lui permettant d’atteindre sa cible qu’est le joueur. Tel est l’objectif de cet article.

 

Résumé

Un petit résumé de ce qui est fait jusqu’à présent.

Des fonctions relatives à l’affichage (pas d’intelligence artificielle dans cette partie) :
– une fonction de création de canvas html5 (createCanvasContext) rattaché à un html element et qui renvoie le contexte 2d associé;
– une fonction pour effacé le canvas html5 entre chaque changement de position (clearCanvas). On pourrait s’en passer en utilisant 2 canvas html5 superposés mais là n’est pas la question;
– une fonction qui affiche un carré de 10 pixels sur 10 dans un canvas context donné, avec une couleur, et une position données;
– 3 autres fonctions dérivées de la précédente (showWall, showPlayer, showEnemy);
– une fonction pour afficher les murs (displayWall).

Un gros objet enemy a été créé : normal puisque c’est lui qui se déplace.

Cet objet compote 5 propriétés :
– sa position matérialisée par les propriétés x et y;
– sa direction matérialisée par la propriété currentDirection qui ne peut prendre que 4 valeurs (E, W, N, S);
– son pas de déplacement sur les axes x et y par les propriétés xStep et yStep.

Auxquelles se rajoutent des méthodes relatives aux déplacements en lien avec les fonctions d’affichage :
– une fonction qui spécifie les pas de déplacement sur les axes X et Y à partir de la direction à prendre (setDSPStepFromCurrentDirection);
– une fonction qui change la position en fonction des pas spécifiés précédemment (moveDSPToTarget);
– une fonction qui vérifie s’il y a un mur devant (wallDSPInFrontOf) à partir de la direction (currentDirection).

D’autres fonctions comme :
– celle d’initialisation du labyrinthe (initGameGrid) à partir du tableau walls;
– celle lancée au lancement du jeu (initialisation) et qui lance la boucle synchronisée avec l’affichage;
– celle lancée en boucle synchronisée (main).

La plus importante rattachée à l’objet enemy : searchDirectionToTarget, c’est ici qu’est implémentée l’intelligence artificielle, certes minimaliste, du jeu.

Un dernier petit objet, le cible appelée player. Player parce que c’est le personnage que le joueur incarne et qui est poursuivi par l’enemy (l’intelligence artificielle)

 

Atteindre la cible

Tout se passe dans la méthode searchDirectionToTarget et c’est elle qu’il faut modifier pour se rapprocher du joueur et le killer (hé oui, c’est le but).

Ajoutez une fonction qui regarde si la cible est en visibilité directe (pas de mur entre les deux). En réalité, ce sont deux fonctions : une pour l’axe des abscisses et la seconde pour l’axe des ordonnées.

Ces fonctions rattachées à l’objet enemy (l’intelligence artificielle) renvoient une structure de données de 2 valeurs :
– la première est une valeur booléenne indicatrice de la visibilité;
– la seconde est la direction (E, W, N ou S) que doit prendre l’enemy pour atteindre sa cible (en l’occurrence le joueur).

Ces fonctions utilisent le tableau walls pour répondre à la question posée.

Le code des 2 fonctions




isPlayerVisibleOnX : function() {
  let returnValue = {
    value : true,
    direction : undefined
  };
  let startX;
  let endX;
   
  if ( this.x < player.x ) {
    startX = this.x;
    endX = player.x;
    returnValue.direction = "E";
  } else if ( this.x > player.x ) {
    startX = player.x;
    endX = this.x;
    returnValue.direction = "W";
  }
   
  for (let i=startX;i<endX;i++) {
    if (walls[this.y][i] == "#") {
        returnValue.value = false;
    }
  }
  return returnValue;
},
 



isPlayerVisibleOnY : function() {
  let returnValue = {
    value : true,
    direction : undefined
  };
  let startY;
  let endY;
   
  if ( this.y < player.y ) {
    startY = this.y;
    endY = player.y;
    returnValue.direction = "S";
  } else if ( this.y > player.y ) {
    startY = player.y;
    endY = this.y;
    returnValue.direction = "N";
  }
   
  for (let i=startY;i<endY;i++) {
       if (walls[i][this.x] == "#") {
         returnValue.value = false;
   }
  }
  return returnValue;
},

Maintenant, il reste à savoir quand les appeler. Jusqu’à présent, l’enemy change de direction dès qu’il rencontre un obstacle. En dehors de cela, il va droit devant lui.

if ( enemy.wallDSPInFrontOf() ) {
 enemy.searchDirectionToTarget(enemy,player);
}

Le fait que l’enemy change de direction dès qu’il rencontre un obstacle ne change pas. Par contre lorsqu’il n’en rencontre pas (il se déplace en ligne droite), il regarde si sa cible est visible. Si c’est le cas, il se dirige vers sa cible dont la direction est donnée par les fonctions de visibilité isPlayerVisibleOnX et isPlayerVisibleOnY.

Le code précédent devient.

if ( enemy.wallDSPInFrontOf() ) {
 enemy.searchDirectionToTarget(enemy,player);
} else {
  if ( enemy.isPlayerVisibleOnX().value && enemy.y == player.y ) {
      enemy.currentDirection = enemy.isPlayerVisibleOnX().direction;
  } else if ( enemy.isPlayerVisibleOnY().value && enemy.x == player.x ) {
      enemy.currentDirection = enemy.isPlayerVisibleOnY().direction;
  }
}

Pour le test en live, cliquez TEST LIVE

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.

Parfait ?

Vous voyez qu’il n’y a rien de compliqué. Mais (hé oui, il y a toujours un mais) la solution n’est pas parfaite. Changez le labyrinthe en accolant pas exemple un mur horizontal à un mur vertical. Par exemple, ceci.

let walls = {
  0:  "                    ",
  1:  "              #     ",
  2:  "              #     ",
  3:  "                    ",
  4:  "                    ",
  5:  "                    ",
  6:  "              #     ",
  7:  "         #    #     ",
  8:  "         #    #     ",
  9:  "  E           #  P  ",
  10: "              #     ",
  11: "         #   ##     ",
  12: "         #    #     ",
  13: "         #    #     ",
  14: "                    ",
  15: "                    ",
  16: "                    ",
  17: "              #     ",
  18: "              #     ",
  19: "                    "
 }

Pour le test en live, cliquez TEST LIVE

Aïe, ça fait mal : l’enemy passe à travers les murs. Le code ne fonctionne que sur labyrinthes avec des murs horizontaux et/ou verticaux mais qui ne sont pas accolés. Comme ceci, par exemple.

let walls = {
 0:  "                    ",
 1:  "              #     ",
 2:  "              #     ",
 3:  "                    ",
 4:  "                    ",
 5:  "                    ",
 6:  "              #     ",
 7:  "         #    #     ",
 8:  "         #    #     ",
 9:  "  E           #  P  ",
 10: "              #     ",
 11: "         # ## #     ",
 12: "         #    #     ",
 13: "         #    #     ",
 14: "                    ",
 15: "            ####    ",
 16: "                    ",
 17: "              #     ",
 18: "              #     ",
 19: "                    "
}

Pour le test en live, cliquez TEST LIVE

Nous lui avons donné la faculté à trouver sa cible, on lui a ainsi donné le pouvoir de passer à travers les murs. On contourne les obstacles au prochain article.

Explore l'univers de la création de jeux vidéo

Saisis ton prénom et ton e-mail pour recevoir ton livre GRATUIT et commence ton voyage ludique dès maintenant.

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