Dans ce tutoriel, nous continuons à coder l'intelligence artificielle pour une partie de hockey à l'aide de comportements de direction et de machines à états finis. Dans cette partie de la série, vous découvrirez l'IA requise par les entités du jeu pour coordonner une attaque, ce qui implique d'intercepter et de porter la rondelle jusqu'au but de l'adversaire..
Coordonner et exécuter une attaque dans un jeu de sport coopératif est une tâche très complexe. Dans le monde réel, quand les humains jouent à un jeu de hockey, ils font nombreuses décisions basées sur de nombreuses variables.
Ces décisions impliquent des calculs et une compréhension de ce qui se passe. Un humain peut dire pourquoi un adversaire bouge en se basant sur les actions d'un autre adversaire, par exemple, "il bouge pour être dans une meilleure position stratégique". Ce n'est pas trivial de porter cette compréhension sur un ordinateur.
En conséquence, si nous essayons de coder l'IA pour suivre toutes les nuances et les perceptions humaines, le résultat sera une pile de code énorme et effrayante. De plus, le résultat peut ne pas être précis ni facilement modifiable.
C’est la raison pour laquelle notre IA d’attaque essaiera d’imiter la résultat d'un groupe d'humains jouant, pas la perception humaine elle-même. Cette approche conduira à des approximations, mais le code sera plus facile à comprendre et à modifier. Le résultat est assez bon pour plusieurs cas d'utilisation.
Nous allons décomposer le processus d'attaque en plusieurs parties, chacune effectuant une action très spécifique. Ces pièces sont les états d'une machine à états finis basée sur des piles. Comme expliqué précédemment, chaque état produira une force de direction qui obligera l'athlète à se comporter en conséquence.
L'orchestration de ces états et les conditions pour basculer entre eux définiront l'attaque. L'image ci-dessous présente l'ensemble du FSM utilisé dans le processus:
Une machine à états finis basée sur une pile représentant le processus d'attaque.Comme illustré par l'image, les conditions pour basculer entre les États seront uniquement basées sur la distance et la propriété du puck. Par exemple, l'équipe a la rondelle
ou la rondelle est trop loin
.
Le processus d'attaque sera composé de quatre états: tourner au ralenti
, attaque
, StealPuck
, et poursuivrePuck
. le tourner au ralenti
state était déjà implémenté dans le didacticiel précédent et constitue le point de départ du processus. De là, un athlète passera à attaque
si l'équipe a la rondelle, à StealPuck
si l'équipe adverse a la rondelle, ou à poursuivrePuck
si la rondelle n'a pas de propriétaire et qu'elle est suffisamment proche pour être récupérée.
le attaque
l'état représente un mouvement offensif. Dans cet état, l’athlète portant la rondelle (nommée chef
) tentera d'atteindre le but de l'adversaire. Les coéquipiers vont avancer, en essayant de soutenir l'action.
le StealPuck
l'état représente quelque chose entre un mouvement défensif et un mouvement offensif. Pendant qu'il est dans cet état, un athlète se concentrera sur la poursuite de l'adversaire portant la rondelle. L'objectif est de récupérer la rondelle pour que l'équipe puisse recommencer à attaquer.
Finalement, le poursuivrePuck
l'état n'est pas lié à l'attaque ou à la défense; il ne fera que guider les athlètes lorsque le palet n’a pas de propriétaire. Pendant qu’il est dans cet état, un athlète tentera d’obtenir la rondelle qui se déplace librement sur la patinoire (par exemple, après avoir été frappée par le bâton de quelqu'un)..
le tourner au ralenti
l'état qui a été précédemment mis en œuvre n'a pas de transitions. Puisque cet état est le point de départ de l'intégralité de l'IA, actualisons-le et permettez-lui de basculer vers d'autres états..
le tourner au ralenti
l'état a trois transitions:
Si l'équipe de l'athlète a la rondelle, tourner au ralenti
devrait être sauté du cerveau et attaque
devrait être poussé. De même, si l'équipe adverse a la rondelle, tourner au ralenti
devrait être remplacé par StealPuck
. La transition restante se produit lorsque personne ne possède la rondelle et qu’elle est proche de l’athlète; dans ce cas, poursuivrePuck
devrait être poussé dans le cerveau.
La version mise à jour de tourner au ralenti
est comme suit (tous les autres états seront implémentés plus tard):
class Athlete // (…) fonction privée idle (): void var aPuck: Puck = getPuck (); stopAndlookAt (aPuck); // Ceci est un hack pour aider à tester l'IA. si (mStandStill) retourne; // La rondelle a-t-elle un propriétaire? if (getPuckOwner ()! = null) // Oui, c'est le cas. mBrain.popState (); if (doesMyTeamHaveThePuck ()) // Mon équipe vient de recevoir la rondelle, c'est le moment de l'attaque! mBrain.pushState (attaque); else // L'équipe adverse a eu la rondelle, essayons de la voler. mBrain.pushState (stealPuck); else if (distance (this, aPuck) < 150) // The puck has no owner and it is nearby. Let's pursue it. mBrain.popState(); mBrain.pushState(pursuePuck); private function attack() :void private function stealPuck() :void private function pursuePuck() :void
Continuons avec la mise en œuvre des autres états.
Maintenant que l'athlète a acquis une certaine perception de l'environnement et est en mesure de passer de tourner au ralenti
à n'importe quel état, concentrons-nous sur la poursuite de la rondelle quand elle n'a pas de propriétaire.
Un athlète passera à poursuivrePuck
immédiatement après le début du match, car la rondelle sera placée au centre de la patinoire sans propriétaire. le poursuivrePuck
l'état a trois transitions:
La première transition est la rondelle est trop loin
, et il essaie de simuler ce qui se passe dans un vrai jeu en ce qui concerne la poursuite de la rondelle. Pour des raisons stratégiques, l'athlète le plus proche de la rondelle est généralement celui qui tente de l'attraper, tandis que les autres attendent ou essaient d'aider..
Sans passer à tourner au ralenti
lorsque la rondelle est éloignée, chaque athlète contrôlé par l'IA la poursuivra en même temps, même s'il en est éloigné. En vérifiant la distance entre l'athlète et la rondelle, poursuivrePuck
se dégage du cerveau et pousse tourner au ralenti
lorsque la rondelle est trop éloignée, ce qui signifie que l'athlète vient "d'abandonner" sa poursuite de la rondelle:
classe Athlete // (…) fonction privée poursuitePuck (): void var aPuck: Puck = getPuck (); if (distance (this, aPuck)> 150) // Puck est trop loin de notre position actuelle, alors abandonnons // en poursuivant la rondelle et espérons que quelqu'un sera plus proche pour obtenir la rondelle // pour nous. mBrain.popState (); mBrain.pushState (inactif); else // La rondelle est proche, essayons de l'attraper. // (…)
Lorsque la rondelle est proche, l’athlète doit la poursuivre, ce qui peut être facilement réalisé avec le comportement de recherche. En utilisant la position de la rondelle comme destination de recherche, l’athlète poursuivra gracieusement la rondelle et ajustera sa trajectoire au fur et à mesure que la rondelle se déplace:
classe Athlete // (…) fonction privée poursuitePuck (): void var aPuck: Puck = getPuck (); mBoid.steering = mBoid.steering + mBoid.separation (); if (distance (this, aPuck)> 150) // Puck est trop loin de notre position actuelle, alors abandonnons // en poursuivant la rondelle et espérons que quelqu'un sera plus proche pour obtenir la rondelle // pour nous. mBrain.popState (); mBrain.pushState (inactif); else // La rondelle est proche, essayons de l'attraper. if (aPuck.owner == null) // Personne n'a la rondelle, c'est notre chance de chercher et de l'obtenir! mBoid.steering = mBoid.steering + mBoid.seek (aPuck.position); else // Quelqu'un vient de recevoir la rondelle. Si le nouveau propriétaire de la rondelle appartient à mon équipe, // nous devrions passer à «attaque», sinon je devrais passer à «stealPuck» // et essayer de récupérer la rondelle. mBrain.popState (); mBrain.pushState (doesMyTeamHaveThePuck ()? attack: stealPuck);
Les deux transitions restantes dans le poursuivrePuck
Etat, l'équipe a la rondelle
et l'adversaire a la rondelle
, sont liés à la rondelle prise au cours du processus de poursuite. Si quelqu'un attrape la rondelle, l'athlète doit faire sauter le poursuivrePuck
Etat et pousser un nouveau dans le cerveau.
L'état à pousser dépend de la propriété de la rondelle. Si l'appel à doesMyTeamHaveThePuck ()
résultats vrai
, cela signifie qu'un coéquipier a obtenu la rondelle, alors l'athlète doit pousser attaque
, ce qui signifie qu'il est temps d'arrêter de poursuivre la rondelle et de commencer à avancer vers le but adverse. Si un adversaire a eu la rondelle, l'athlète doit pousser StealPuck
, ce qui fera que l'équipe tentera de récupérer la rondelle.
En petite amélioration, les athlètes ne doivent pas rester trop proches les uns des autres pendant poursuivrePuck
Etat, car un mouvement "encombré" qui poursuit n'est pas naturel. Ajout de la séparation à la force de direction de l’Etat (ligne 6
dans le code ci-dessus) garantit aux athlètes de garder une distance minimale entre eux.
Le résultat est une équipe capable de poursuivre la rondelle. À des fins de test, dans cette démo, la rondelle est placée au centre de la patinoire toutes les quelques secondes, pour que les athlètes continuent de bouger:
Après avoir obtenu la rondelle, un athlète et son équipe doivent se rapprocher du but de l’adversaire pour marquer. C'est le but de la attaque
Etat:
le attaque
l'état n'a que deux transitions: l'adversaire a la rondelle
et la rondelle n'a pas de propriétaire
. Étant donné que l'État est uniquement conçu pour amener les athlètes à se rapprocher du but de leur adversaire, il est inutile de continuer à attaquer si la rondelle n'est plus sous la possession de l'équipe..
Concernant le mouvement vers l'objectif de l'adversaire: l'athlète portant la rondelle (leader) et les coéquipiers qui l'aident devraient se comporter différemment. Le leader doit atteindre le but de l'adversaire et les coéquipiers doivent l'aider tout au long du parcours..
Ceci peut être implémenté en vérifiant si l'athlète qui exécute le code a le puck:
class Athlete // (…) fonction privée attack (): void var aPuckOwner: Athlete = getPuckOwner (); // La rondelle a-t-elle un propriétaire? if (aPuckOwner! = null) // Oui, c'est le cas. Voyons si le propriétaire appartient à l'équipe adverse. if (doesMyTeamHaveThePuck ()) if (amIThePuckOwner ()) // Mon équipe a la rondelle et c'est moi qui la possède! Avançons // vers le but de l'adversaire. mBoid.steering = mBoid.steering + mBoid.seek (getOpponentGoalPosition ()); else // Mon équipe a le puck, mais un coéquipier l'a. Suivons-le simplement // pour vous soutenir pendant l'attaque. mBoid.steering = mBoid.steering + mBoid.followLeader (aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separation (); else // L'adversaire a le puck! Arrêtez l'attaque // et essayez de la voler. mBrain.popState (); mBrain.pushState (stealPuck); else // Puck n'a pas de propriétaire, il est donc inutile de continuer à // attaquer. Il est temps de se réorganiser et de poursuivre la rondelle. mBrain.popState (); mBrain.pushState (poursuitePuck);
Si amIThePuckOwner ()
résultats vrai
(ligne 10), l'athlète qui exécute le code a la rondelle. Dans ce cas, il cherchera simplement la position de but de l'adversaire. C'est à peu près la même logique utilisée pour poursuivre la rondelle dans le poursuivrePuck
Etat.
Si amIThePuckOwner ()
résultats faux
, L'athlète n'a pas la rondelle, il doit donc aider le meneur. Aider le chef est une tâche compliquée, nous allons donc la simplifier. Un athlète assistera le leader simplement en cherchant une position devant lui:
Au fur et à mesure que le leader avance, il sera entouré par ses coéquipiers qui suivront les instructions. devant
point. Cela donne au chef quelques options pour passer la rondelle s'il y a des problèmes. Comme dans un vrai match, les coéquipiers environnants doivent également rester à l'écart du leader.
Ce modèle d’assistance peut être obtenu en ajoutant une version légèrement modifiée du comportement suivant le guide (ligne 18). La seule différence est que les athlètes suivront un point devant du leader, au lieu d'un derrière lui, comme cela avait été implémenté à l'origine dans ce comportement.
Les athlètes assistant le leader doivent également garder une distance minimale entre eux. Ceci est implémenté en ajoutant une force de séparation (ligne 19).
Le résultat est une équipe capable de se diriger vers le but adverse, sans encombrement et en simulant un mouvement d'attaque assisté:
La mise en œuvre actuelle du attaque
l'état est assez bon pour certaines situations, mais il a un défaut. Quand quelqu'un attrape la rondelle, il devient le leader et est immédiatement suivi par ses coéquipiers.
Que se passe-t-il si le chef avance vers son propre but quand il attrape la rondelle? Observez de plus près la démo ci-dessus et remarquez le schéma peu naturel lorsque des coéquipiers commencent à suivre le leader..
Lorsque le leader attrape la rondelle, le comportement de recherche prend un certain temps pour corriger la trajectoire du leader et le faire avancer efficacement vers le but de l'adversaire. Même lorsque le leader "manoeuvre", ses coéquipiers vont essayer de chercher son devant
point, ce qui signifie qu'ils vont se déplacer vers les leurs but (ou la place que le leader regarde).
Lorsque le leader est enfin en position et prêt à avancer vers le but adverse, ses coéquipiers "manoeuvrent" pour suivre le leader. Le leader bougera alors sans le soutien de son coéquipier tant que les autres ajusteront leurs trajectoires..
Cette faille peut être corrigée en vérifiant si le coéquipier est en avance sur le meneur lorsque l'équipe récupère la rondelle. Ici, la condition "en avant" signifie "plus près du but de l'adversaire":
class Athlete // (…) fonction privée isAheadOfMe (theBoid: Boid): Boolean var aTargetDistance: Number = distance (getOpponentGoalPosition (), theBoid); var aMyDistance: Number = distance (getOpponentGoalPosition (), mBoid.position); retourner aTargetDistance <= aMyDistance; private function attack() :void var aPuckOwner :Athlete = getPuckOwner(); // Does the puck have an owner? if (aPuckOwner != null) // Yeah, it has. Let's find out if the owner belongs to the opponents team. if (doesMyTeamHaveThePuck()) if (amIThePuckOwner()) // My team has the puck and I am the one who has it! Let's move // towards the opponent's goal. mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition()); else // My team has the puck, but a teammate has it. Is he ahead of me? if (isAheadOfMe(aPuckOwner.boid)) // Yeah, he is ahead of me. Let's just follow him to give some support // during the attack. mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separation(); else // Nope, the teammate with the puck is behind me. In that case // let's hold our current position with some separation from the // other, so we prevent crowding. mBoid.steering = mBoid.steering + mBoid.separation(); else // The opponent has the puck! Stop the attack // and try to steal it. mBrain.popState(); mBrain.pushState(stealPuck); else // Puck has no owner, so there is no point to keep // attacking. It's time to re-organize and start pursuing the puck. mBrain.popState(); mBrain.pushState(pursuePuck);
Si le leader (qui est le propriétaire de la rondelle) est en avance sur l'athlète qui exécute le code, alors l'athlète doit suivre le leader comme il le faisait auparavant (lignes 27 et 28). Si le leader est derrière lui, l'athlète doit conserver sa position actuelle en gardant une distance minimale entre les autres (ligne 33)..
Le résultat est un peu plus convaincant que le premier attaque
la mise en oeuvre:
Pointe: En peaufinant les calculs de distance et les comparaisons dans le isAheadOfMe ()
méthode, il est possible de modifier la façon dont les athlètes tiennent leurs positions actuelles.
L’état final du processus d’attaque est StealPuck
, qui devient actif lorsque l’équipe adverse a la rondelle. Le but principal de la StealPuck
L’État doit voler la rondelle à l’adversaire qui la porte, pour que l’équipe puisse recommencer à attaquer:
Puisque l'idée derrière cet état est de voler le puck à l'adversaire, si le puck est récupéré par l'équipe ou devient libre (c'est-à-dire qu'il n'a pas de propriétaire), StealPuck
va sortir du cerveau et pousser le bon état à faire face à la nouvelle situation:
Athlète de classe // (…) fonction privée stealPuck (): void // La rondelle a-t-elle un propriétaire? if (getPuckOwner ()! = null) // Oui, oui, mais qui l'a? if (doesMyTeamHaveThePuck ()) // Mon équipe a le puck, il est donc temps d'arrêter d'essayer de voler // le puck et de commencer à attaquer. mBrain.popState (); mBrain.pushState (attaque); else // Un adversaire a le puck. var aOpponentLeader: Athlete = getPuckOwner (); // Le poursuivons tout en maintenant une certaine séparation des // autres pour éviter que tout le monde ne se retrouve dans la même position // dans la poursuite. mBoid.steering = mBoid.steering + mBoid.pursuit (aOpponentLeader.boid); mBoid.steering = mBoid.steering + mBoid.separation (); else // La rondelle n'a pas de propriétaire, elle tourne probablement librement dans la patinoire. // Cela ne sert à rien d'essayer de le voler, finissons donc avec l'état 'stealPuck' // et passons à 'poursuitePuck'. mBrain.popState (); mBrain.pushState (poursuitePuck);
Si la rondelle a un propriétaire et qu'il appartient à l'équipe adverse, l'athlète doit poursuivre le leader adverse et essayer de voler la rondelle. Afin de poursuivre le leader de son adversaire, un athlète doit prédire où il sera dans un proche avenir, afin qu'il puisse être intercepté dans sa trajectoire. C'est différent de simplement chercher le chef adverse.
Heureusement, cela peut être facilement réalisé avec le comportement de poursuite (ligne 19). En utilisant une force de poursuite dans le StealPuck
Etat, les athlètes vont essayer de intercepter le chef de l'adversaire, au lieu de le suivre:
La mise en œuvre actuelle de StealPuck
fonctionne, mais dans un jeu réel, un ou deux athlètes seulement s’approchent du leader adverse pour voler la rondelle. Le reste de l’équipe reste dans les zones environnantes pour essayer d’aider, ce qui évite les vols fréquents.
Vous pouvez le corriger en ajoutant un contrôle de distance (ligne 17) avant la poursuite du leader par l'adversaire:
Athlète de classe // (…) fonction privée stealPuck (): void // La rondelle a-t-elle un propriétaire? if (getPuckOwner ()! = null) // Oui, oui, mais qui l'a? if (doesMyTeamHaveThePuck ()) // Mon équipe a le puck, il est donc temps d'arrêter d'essayer de voler // le puck et de commencer à attaquer. mBrain.popState (); mBrain.pushState (attaque); else // Un adversaire a le puck. var aOpponentLeader: Athlete = getPuckOwner (); // L'adversaire avec la rondelle est-il proche de moi? if (distance (aOpponentLeader, this) < 150) // Yeah, he is close! Let's pursue him while mantaining a certain // separation from the others to avoid that everybody will ocuppy the same // position in the pursuit. mBoid.steering = mBoid.steering.add(mBoid.pursuit(aOpponentLeader.boid)); mBoid.steering = mBoid.steering.add(mBoid.separation(50)); else // No, he is too far away. In the future, we will switch // to 'defend' and hope someone closer to the puck can // steal it for us. // TODO: mBrain.popState(); // TODO: mBrain.pushState(defend); else // The puck has no owner, it is probably running freely in the rink. // There is no point to keep trying to steal it, so let's finish the 'stealPuck' state // and switch to 'pursuePuck'. mBrain.popState(); mBrain.pushState(pursuePuck);
Au lieu de poursuivre aveuglément le leader de l'adversaire, un athlète vérifiera si la distance qui le sépare du leader de l'adversaire est inférieure à, disons, 150
. Si c'est vrai
, la poursuite se déroule normalement, mais si la distance est supérieure à 150
, cela signifie que l'athlète est trop éloigné du leader adverse.
Si cela se produit, il est inutile de continuer à voler la rondelle, car elle est trop loin et il y a probablement des coéquipiers déjà en place qui tentent de faire de même. La meilleure option est de pop StealPuck
du cerveau et pousser le la défense
état (qui sera expliqué dans le prochain tutoriel). Pour le moment, un athlète maintiendra sa position actuelle si le leader adverse est trop éloigné..
Le résultat est un modèle de vol plus convaincant et naturel (pas d'encombrement):
Il y a un dernier tour que les athlètes doivent apprendre pour attaquer efficacement. Pour le moment, ils se dirigent vers le but adverse sans tenir compte des adversaires. Un adversaire doit être considéré comme une menace et doit être évité.
En utilisant le comportement de prévention des collisions, les athlètes peuvent esquiver leurs adversaires lorsqu'ils se déplacent:
Comportement d'évitement de collision utilisé pour éviter les adversaires.Les opposants seront considérés comme des obstacles circulaires. En raison de la nature dynamique des comportements de direction, qui sont mis à jour dans chaque boucle de jeu, le modèle d'évitement fonctionnera gracieusement et en douceur pour les obstacles en mouvement (ce qui est le cas ici)..
Pour que les athlètes évitent les adversaires (obstacles), une seule ligne doit être ajoutée à l'état d'attaque (ligne 14):
class Athlete // (…) fonction privée attack (): void var aPuckOwner: Athlete = getPuckOwner (); // La rondelle a-t-elle un propriétaire? if (aPuckOwner! = null) // Oui, c'est le cas. Voyons si le propriétaire appartient à l'équipe adverse. if (doesMyTeamHaveThePuck ()) if (amIThePuckOwner ()) // Mon équipe a la rondelle et c'est moi qui la possède! Avançons // vers le but de l'adversaire, en évitant tous les adversaires en cours de route. mBoid.steering = mBoid.steering + mBoid.seek (getOpponentGoalPosition ()); mBoid.steering = mBoid.steering + mBoid.collisionAvoidance (membres getOpponentTeam ().); else // Mon équipe a le puck, mais un coéquipier l'a. Est-il en avance sur moi? if (isAheadOfMe (aPuckOwner.boid)) // Ouais, il est en avance sur moi. Suivons-le simplement pour apporter un soutien // pendant l'attaque. mBoid.steering = mBoid.steering + mBoid.followLeader (aPuckOwner.boid); mBoid.steering = mBoid.steering + mBoid.separation (); else // Non, le coéquipier avec la rondelle est derrière moi. Dans ce cas, // maintenons notre position actuelle avec une certaine séparation par rapport aux // autres, afin d'éviter tout encombrement. mBoid.steering = mBoid.steering + mBoid.separation (); else // L'adversaire a le puck! Arrêtez l'attaque // et essayez de la voler. mBrain.popState (); mBrain.pushState (stealPuck); else // Puck n'a pas de propriétaire, il est donc inutile de continuer à // attaquer. Il est temps de se réorganiser et de poursuivre la rondelle. mBrain.popState (); mBrain.pushState (poursuitePuck);
Cette ligne ajoutera une force anti-collision à l'athlète, qui sera combinée avec les forces existantes. En conséquence, l'athlète évitera les obstacles tout en cherchant le but de son adversaire.
Ci-dessous, une démonstration d’un athlète dirigeant le attaque
Etat. Les opposants sont inébranlables pour mettre en évidence le comportement d'évitement de collision:
Ce tutoriel explique la mise en œuvre du schéma d’attaque utilisé par les athlètes pour voler et porter la rondelle vers le but de l’adversaire. En combinant plusieurs comportements de direction, les athlètes sont désormais en mesure d’effectuer des mouvements complexes, tels que suivre un leader ou poursuivre son adversaire avec la rondelle..
Comme indiqué précédemment, la mise en œuvre de l'attaque vise à simuler ce que les humains faire, le résultat est donc une approximation d'un jeu réel. En ajustant individuellement les états qui composent l'attaque, vous pouvez produire une meilleure simulation, ou une simulation qui correspond à vos besoins..
Dans le prochain tutoriel, vous apprendrez à faire défendre les athlètes. L’intelligence artificielle deviendra complète, capable d’attaquer et de défendre, ce qui permettra à des équipes contrôlées à 100% de s'affronter.