Les machines à états finis et les comportements de direction se marient parfaitement: leur nature dynamique permet de combiner des états simples et des forces pour créer des comportements complexes. Dans ce tutoriel, vous apprendrez à coder une équipe modèle utilisant une machine à états finis basée sur une pile combinée avec des comportements de direction.
Toutes les icônes FSM faites par Lorc et disponibles sur http://game-icons.net. Les atouts de la démo: Top / Down Shoot 'Em Up Spritesheet de takomogames et Alien Breed (esque) Top-Down Tilesheet de SpicyPixel.
Remarque: Bien que ce tutoriel ait été écrit avec AS3 et Flash, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..
Après avoir terminé ce didacticiel, vous serez en mesure de mettre en place un modèle d’escouade dans lequel un groupe de soldats suivra le chef, chassant les ennemis et pillant des objets:
Le précédent tutoriel sur les machines à états finis décrivait leur utilité pour la mise en œuvre d'une logique d'intelligence artificielle: au lieu d'écrire une pile très complexe de code d'intelligence artificielle, elle peut être répartie sur un ensemble d'états simples, chacun effectuant des tâches très spécifiques fuir un ennemi.
La combinaison d'états résulte en une intelligence artificielle sophistiquée, mais facile à comprendre, à modifier et à maintenir. Cette structure est également l’un des piliers des comportements de direction: la combinaison de forces simples pour créer des motifs complexes.
C'est pourquoi les FSM et les comportements de direction forment une excellente combinaison. Les états peuvent être utilisés pour contrôler les forces qui agiront sur un personnage, améliorant ainsi l'ensemble déjà puissant de modèles pouvant être créés à l'aide de comportements de direction..
Afin d'organiser tous les comportements, ils seront répartis sur les états. Chaque état générera une force de comportement spécifique, ou un ensemble d'entre eux, comme la recherche, la fuite et l'évitement de collision..
Lorsqu'un état particulier est actif, seule la force résultante sera appliquée au personnage, le faisant se comporter en conséquence. Par exemple, si l’état actuellement actif est fuyez
et ses forces sont une combinaison de fuir
et évitement de collision
, le personnage va fuir une place tout en évitant tout obstacle.
Les forces de direction sont calculées à chaque mise à jour du jeu, puis ajoutées au vecteur de vélocité du personnage. En conséquence, lorsque l'état actif change (et avec lui le modèle de mouvement), le personnage passera en douceur au nouveau modèle à mesure que les nouvelles forces sont ajoutées après chaque mise à jour..
La nature dynamique des comportements de direction assure cette transition fluide; les états coordonnent simplement les forces de direction actives à un moment donné.
La structure permettant d'implémenter un motif de groupe encapsulera les FSM et les comportements de direction dans les propriétés d'une classe. Toute classe représentant une entité qui se déplace ou est influencée par les forces de direction aura une propriété appelée boid
, qui est une instance de la Boid
classe:
classe publique Boid public var position: Vector3D; vitesse de propagation publique: Vector3D; direction publique var: Vector3D; public var masse: Number; fonction publique recherche (cible: Vector3D, slowingRadius: Number = 0): Vector3D (…) fonction publique fuyant (position: Vector3D): Vector3D (…) fonction publique update (): void (…) (… )
le Boid
la classe a été utilisée dans la série de comportement de direction et il fournit des propriétés comme rapidité
et position
(les deux vecteurs mathématiques), ainsi que des méthodes pour ajouter des forces de direction, telles que chercher()
, fuir()
, etc.
Une entité qui utilise un FSM basé sur une pile aura la même structure que le Fourmi
classe du précédent tutoriel FSM: le FSM basé sur la pile est géré par le cerveau
propriété et chaque état est mis en œuvre en tant que méthode.
Ci-dessous est la Soldat
classe, qui a un comportement de direction et des capacités FSM:
classe publique Soldier private var brain: StackFSM; // Contrôle le contenu FSM private var boid: Boid; // Contrôle les comportements de direction fonction publique Soldier (posX: nombre, posY: nombre, masse totale: nombre) (…) brain = new StackFSM (); // Poussez l'état "suivez" pour que le soldat suive le leader brain.pushState (follow); public function update (): void // Met à jour le cerveau. Il exécutera la fonction d'état en cours. brain.update (); // Mise à jour des comportements de direction boid.update ();
Le motif de groupe sera mis en œuvre à l'aide d'une machine à états finis basée sur une pile. Les soldats, qui sont les membres de l'équipe, suivront le chef (contrôlé par le joueur), traquant tous les ennemis proches..
Quand un ennemi meurt, il peut déposer un objet qui peut être bon ou mauvais (un medkit ou un badkit, respectivement). Un soldat va briser la formation de l'escouade et ramasser de bons objets à proximité, ou fuira les lieux pour éviter de mauvais objets..
Vous trouverez ci-dessous une représentation graphique du FSM basé sur l'empilement contrôlant le "cerveau" du soldat:
Les sections suivantes présentent la mise en œuvre de chaque état. Tous les extraits de code de ce didacticiel décrivent l’idée principale de chaque étape, en omettant tous les détails concernant le moteur de jeu utilisé (Flixel, dans ce cas)..
Le premier état à implémenter est celui qui restera actif presque tout le temps: Suivez le guide. La partie pillage sera mise en œuvre plus tard, donc pour le moment la suivre
l'état fera seulement que le soldat suive le chef, changeant l'état actuel en chasse
s'il y a un ennemi à proximité:
fonction publique follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtient un pointeur sur le leader addSteeringForce (boid.followLeader (aLeader)); // suis le leader // Y a-t-il un monstre à proximité? if (getNearestEnemy ()! = null) // Oui, il y en a un! Traquez-le! // Pousse l'état "hunt". Cela fera que le soldat cesse de suivre le chef et // commence à chasser le monstre. brain.pushState (chasse); fonction privée getNearestEnemy (): Monster // voici l'implémentation pour obtenir l'ennemi le plus proche
Malgré la présence d'ennemis, tant que l'État est actif, il générera toujours une force pour suivre le chef, en utilisant le comportement suivant du chef..
Si getNearestEnemy ()
renvoie quelque chose, cela signifie qu'il y a un ennemi autour. Dans ce cas, le chasse
l'état est poussé dans la pile pendant l'appel brain.pushState (chasse)
, faire en sorte que le soldat cesse de suivre le chef et commence à chasser ses ennemis.
Pour le moment, la mise en œuvre de la chasse()
Etat peut simplement sortir de la pile, de cette façon, les soldats ne seront pas bloqués à l'état de chasse:
fonction publique hunt (): void // Pour l'instant, extrayons simplement l'état hunt () du cerveau. brain.popState ();
Notez qu'aucune information n'est transmise au chasse
état, comme qui est l'ennemi le plus proche. Ces informations doivent être collectées par le chasse
lui-même, car il détermine si le chasse
doit rester actif ou sortir lui-même de la pile (en renvoyant le contrôle à la suivre
Etat).
Le résultat jusqu’à présent est une escouade de soldats à la suite du chef (notez que les soldats ne chasseront pas car le chasse()
méthode apparaît elle-même):
Pointe: chaque État devrait être responsable de mettre fin à son existence en se retirant de la pile.
Le prochain état à mettre en œuvre est chasse
, qui fera des soldats traquer tout ennemi proche. Le code pour chasse()
est:
fonction publique hunt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Avons-nous un monstre à proximité? if (aNearestEnemy! = null) // Oui, nous le faisons. Calculons à quelle distance il est. var aDistance: Number = CalculateDistance (aNearestEnemy, this); // Le monstre est-il assez proche pour tirer? si (aDistance <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState();
L’État commence par assigner aNearestEnemy
avec l'ennemi le plus proche. Si aNearestEnemy
est nul
cela signifie qu'il n'y a pas d'ennemi autour, alors l'État doit se terminer. L'appel brain.popState ()
apparaît le chasse
l'état, le passage du soldat à l'état suivant dans la pile.
Si aNearestEnemy
n'est pas nul
, cela signifie qu'il y a un ennemi à chasser et que l'état doit rester actif. L'algorithme de chasse est basé sur la distance entre le soldat et l'ennemi: si la distance est supérieure à 80, le soldat cherchera la position de l'ennemi; si la distance est inférieure à 80, le soldat fera face à l'ennemi et tirera immobile.
Puisque chasse()
sera invoqué à chaque mise à jour de jeu. Si un ennemi est présent, le soldat le recherchera ou le tirera. La décision de se déplacer ou de tirer est contrôlée dynamiquement par la distance entre le soldat et l'ennemi..
Le résultat est une équipe de soldats capables de suivre le chef et de traquer les ennemis proches:
Chaque fois qu'un ennemi est tué, il peut déposer un objet. Le soldat doit récupérer l'objet s'il est bon ou le fuir s'il est mauvais. Ce comportement est représenté par deux états dans le FSM précédemment décrit:
collectItem
et fuyez
États. le collectItem
l'état fera un soldat arriver à l'élément déposé, tandis que le fuyez
l'état fera le soldat fuir l'emplacement du mauvais article. Les deux états sont presque identiques, la seule différence est la force d’arrivée ou de fuite:
fonction publique runAway (): void var aItem: Item = getNearestItem (); if (aItem! = null && aItem.alive && aItem.type == Item.BADKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.flee (aItemPos)); else brain.popState (); fonction publique collectItem (): void var aItem: Item = getNearestItem (); if (aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); else brain.popState (); private function getNearestItem (): Item // voici le code pour obtenir l'item le plus proche
Ici, une optimisation des transitions est utile. Le code pour passer de la suivre
état à la collectItem
ou la fuyez
States est identique: vérifie s'il y a un objet à proximité, puis pousse le nouvel état.
L'état à pousser dépend du type d'élément. En conséquence, la transition vers collectItem
ou fuyez
peut être implémenté comme une méthode unique, nommée checkItemsNearby ()
:
fonction privée checkItemsNearby (): void var aItem: Item = getNearestItem (); if (aItem! = null) brain.pushState (aItem.type == Item.BADKIT? runAway: collectItem);
Cette méthode vérifie l'élément le plus proche. Si c'est un bon, le collectItem
l'état est poussé dans le cerveau; si c'est mauvais, le fuyez
l'état est poussé. S'il n'y a pas d'article à collectionner, la méthode ne fait rien.
Cette optimisation permet l'utilisation de checkItemsNearby ()
pour contrôler la transition de tout état à collectItem
ou fuyez
. Selon le soldat FSM, cette transition existe dans deux états: suivre
et chasse
.
Leur mise en œuvre peut être légèrement modifiée pour s'adapter à cette nouvelle transition:
fonction publique follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtient un pointeur sur le leader addSteeringForce (boid.followLeader (aLeader)); // suit le leader // Vérifie s'il y a un élément à collecter (ou à fuir) checkItemsNearby (); // Y a-t-il un monstre à proximité? if (getNearestEnemy ()! = null) // Oui, il y en a un! Traquez-le! // Pousse l'état "hunt". Cela fera que le soldat cesse de suivre le chef et // commence à chasser le monstre. brain.pushState (chasse); fonction publique hunt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Vérifie s'il existe un élément à collecter (ou à fuir) checkItemsNearby (); // Avons-nous un monstre à proximité? if (aNearestEnemy! = null) // Oui, nous le faisons. Calculons à quelle distance il est. var aDistance: Number = CalculateDistance (aNearestEnemy, this); // Le monstre est-il assez proche pour tirer? si (aDistance <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState();
Tout en suivant le chef, un soldat va rechercher des objets à proximité. Lors de la chasse à un ennemi, un soldat vérifiera également les objets à proximité..
Le résultat est la démo ci-dessous. Notez qu'un soldat tentera de collecter ou d'éviter un objet chaque fois qu'il y en a un à proximité, même s'il y a des ennemis à chasser et le chef à suivre..
Un aspect important concernant les états et les transitions est la priorité parmi eux. En fonction de la ligne où une transition est placée dans la mise en œuvre d'un état, la priorité de cette transition change.
En utilisant le suivre
état et la transition faite par checkItemsNearby ()
à titre d'exemple, regardez l'implémentation suivante:
fonction publique follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtient un pointeur sur le leader addSteeringForce (boid.followLeader (aLeader)); // suit le leader // Vérifie s'il y a un élément à collecter (ou à fuir) checkItemsNearby (); // Y a-t-il un monstre à proximité? if (getNearestEnemy ()! = null) // Oui, il y en a un! Traquez-le! // Pousse l'état "hunt". Cela fera que le soldat cesse de suivre le chef et // commence à chasser le monstre. brain.pushState (chasse);
Cette version de suivre()
fera un soldat passer à collectItem
ou fuyez
avant vérifier s'il y a un ennemi autour. En conséquence, le soldat récupérera (ou fuira) un objet même s'il y a des ennemis à proximité qui devraient être traqués par le chasse
Etat.
Voici une autre implémentation:
fonction publique follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtient un pointeur sur le leader addSteeringForce (boid.followLeader (aLeader)); // suis le leader // Y a-t-il un monstre à proximité? if (getNearestEnemy ()! = null) // Oui, il y en a un! Traquez-le! // Pousse l'état "hunt". Cela fera que le soldat cesse de suivre le chef et // commence à chasser le monstre. brain.pushState (chasse); else // Vérifie s'il existe un élément à collecter (ou à fuir) checkItemsNearby ();
Cette version de suivre()
fera un soldat passer à collectItem
ou fuyez
seulement après il découvre qu'il n'y a pas d'ennemis à tuer.
La mise en œuvre actuelle de suivre()
, chasse()
et collectItem ()
souffrir de problèmes prioritaires. Le soldat essaiera de ramasser un objet même s'il y a des choses plus importantes à faire. Afin de résoudre ce problème, quelques ajustements sont nécessaires.
En ce qui concerne la suivre
état, le code peut être mis à jour pour:
(suivre () avec les priorités)
fonction publique follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtient un pointeur sur le leader addSteeringForce (boid.followLeader (aLeader)); // suis le leader // Y a-t-il un monstre à proximité? if (getNearestEnemy ()! = null) // Oui, il y en a un! Traquez-le! // Pousse l'état "hunt". Cela fera que le soldat cesse de suivre le chef et // commence à chasser le monstre. brain.pushState (chasse); else // Vérifie s'il existe un élément à collecter (ou à fuir) checkItemsNearby ();
le chasse
l'état doit être changé en:
fonction publique hunt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Avons-nous un monstre à proximité? if (aNearestEnemy! = null) // Oui, nous le faisons. Calculons à quelle distance il est. var aDistance: Number = CalculateDistance (aNearestEnemy, this); // Le monstre est-il assez proche pour tirer? si (aDistance <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState(); // Check if there is an item to collect (or run away from) checkItemsNearby();
Finalement, le collectItem
l'état doit être changé pour arrêter tout pillage s'il y a un ennemi autour:
fonction publique collectItem (): void var aItem: Item = getNearestItem (); var aMonsterNearby: Boolean = getNearestEnemy ()! = null; if (! aMonsterNearby && aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); else brain.popState ();
Le résultat de toutes ces modifications est la démo du début du tutoriel:
Dans ce didacticiel, vous avez appris à coder un motif d’escouade dans lequel un groupe de soldats suivrait un chef, traquant et pillant les ennemis proches. L’intelligence artificielle est mise en œuvre à l’aide d’un FSM reposant sur une pile, associé à plusieurs comportements.
Comme il a été démontré, les machines à états finis et les comportements de direction constituent une combinaison puissante et un grand match. En répartissant la logique sur les états du FSM, il est possible de sélectionner dynamiquement les forces de direction qui agiront sur un personnage, permettant ainsi la création de modèles d'IA complexes..
Combinez les comportements de conduite que vous connaissez déjà avec les FSM et créez de nouveaux modèles exceptionnels!