Planification d'action axée sur les objectifs pour une IA plus intelligente

La planification d'action axée sur les objectifs (GOAP) est un système d'intelligence artificielle qui donnera facilement à vos agents des choix et les outils nécessaires pour prendre des décisions éclairées sans avoir à maintenir une machine à états finis grande et complexe..

Voir la démo

Dans cette démo, il existe quatre classes de caractères, chacune utilisant des outils interrompant leur utilisation pendant un certain temps:

  • Mineur: Mines de minerai sur des rochers. Besoin d'un outil pour travailler.
  • Logger: coupe les arbres pour produire des journaux. Besoin d'un outil pour travailler.
  • Bûcheron: Coupe les arbres en bois utilisable. Besoin d'un outil pour travailler.
  • Forgeron: Forge des outils à la forge. Tout le monde utilise ces outils.

Chaque classe déterminera automatiquement, en utilisant la planification d’actions ciblée, les actions qu’elles doivent exécuter pour atteindre leurs objectifs. Si leur outil se casse, ils iront dans une pile de ravitaillement qui en aura une faite par le forgeron..

Qu'est-ce que GOAP??

La planification d’action orientée objectif est un système d’intelligence artificielle destiné aux agents, qui leur permet de planifier une séquence d’actions pour atteindre un objectif particulier. La séquence d'actions dépend non seulement de l'objectif, mais également de l'état actuel du monde et de l'agent. Cela signifie que si le même objectif est fourni pour différents agents ou états du monde, vous pouvez obtenir une séquence d'actions complètement différente., Ce qui rend l'IA plus dynamique et réaliste. Regardons un exemple, comme dans la démo ci-dessus.

Nous avons un agent, un broyeur de bois, qui prend des bûches et les coupe en bois de chauffage. Le hachoir peut être fourni avec l'objectif MakeFirewood, et a les actions ChopLog, GetAxe, et CollectBranches.

le ChopLog action transformera une bûche en bois de chauffage, mais seulement si le bûcheron a une hache. le GetAxe l'action donnera au bûcheron une hache. Finalement, le CollectBranches l'action produira également du bois de chauffage, sans nécessiter de hache, mais la qualité du bois de chauffage ne sera pas aussi élevée.

Quand on donne à l'agent la MakeFirewood objectif, nous obtenons ces deux séquences d’actions différentes:

  • Besoin de bois de chauffage -> GetAxe -> ChopLog = fait du bois de chauffage
  • Besoin de bois de chauffage -> CollectBranches = fait du bois de chauffage

Si l'agent peut obtenir une hache, il peut alors couper une bille pour fabriquer du bois de chauffage. Mais peut-être qu'ils ne peuvent pas obtenir une hache; alors, ils peuvent simplement aller chercher des branches. Chacune de ces séquences remplira l’objectif de MakeFirewood

GOAP peut choisir la meilleure séquence en fonction des conditions préalables disponibles. S'il n'y a pas de hache à portée de main, le bûcheron doit recourir à la cueillette de branches. Ramasser les branches peut prendre beaucoup de temps et produire un bois de chauffage de qualité médiocre. Nous ne voulons donc pas qu’il fonctionne tout le temps, mais seulement quand il le faut..

A qui s'adresse GOAP

Vous connaissez probablement déjà les machines à états finis (FSM), mais si ce n’est pas le cas, jetez un coup d’œil à ce formidable tutoriel.. 

Vous avez peut-être rencontré des états très volumineux et complexes pour certains de vos agents FSM, où vous finissez par ne plus vouloir ajouter de nouveaux comportements, car ils entraînent trop d'effets secondaires et de lacunes dans l'IA..

GOAP tourne ceci:

Etats de la machine à états finis: connectés partout.

Dans ceci:

GOAP: gentil et maniable.


En dissociant les actions les unes des autres, nous pouvons maintenant nous concentrer sur chaque action individuellement. Cela rend le code modulaire et facile à tester et à maintenir. Si vous souhaitez ajouter une autre action, vous pouvez simplement l'insérer et aucune autre action ne doit être modifiée. Essayez de faire cela avec un FSM!

Vous pouvez également ajouter ou supprimer des actions à la volée pour modifier le comportement d'un agent et les rendre encore plus dynamiques. Avez-vous un ogre qui a soudainement commencé à faire rage? Donnez-leur une nouvelle action "attaque de rage" qui est supprimée quand ils se calment. Ajouter simplement l’action à la liste des actions est tout ce que vous avez à faire; le planificateur GOAP se chargera du reste.

Si vous avez un FSM très complexe pour vos agents, vous devriez essayer GOAP. Un signe que votre FSM devient trop complexe est que chaque État a une myriade de déclarations if-else testant quel état il devrait passer au suivant, et que l'ajout d'un nouvel état vous fait gémir à toutes les implications que cela pourrait avoir..

Si vous avez un agent très simple qui n'effectue qu'une ou deux tâches, alors GOAP sera peut-être un peu lourd et un FSM suffira. Cependant, il est intéressant de regarder les concepts ici et de voir s’ils seraient assez faciles à brancher sur votre agent.

actes

Un action est quelque chose que l'agent fait. Habituellement, il ne s'agit que de jouer une animation et un son, et de changer un peu d'état (par exemple, ajouter du bois de chauffage). Ouvrir une porte est une action (et une animation) différente de celle qui consiste à prendre un crayon. Une action est encapsulée et ne devrait pas avoir à s’inquiéter des autres actions..

Pour aider GOAP à déterminer les actions que nous souhaitons utiliser, chaque action reçoit un Coût. Une action à coût élevé ne sera pas choisie par rapport à une action à moindre coût. Lorsque nous ordonnons les actions ensemble, nous additionnons les coûts, puis nous choisissons la séquence avec le coût le plus bas..

Permet d'attribuer des coûts aux actions:

  • GetAxe Coût: 2
  • ChopLog Coût: 4
  • CollectBranches Coût: 8

Si nous regardons à nouveau la séquence d'actions et additionnons les coûts totaux, nous verrons quelle est la séquence la moins chère:

  • Besoin de bois de chauffage -> GetAxe (2) -> ChopLog(4) = fait du bois de chauffage(total: 6)
  • Besoin de bois de chauffage -> CollectBranches(8) = fait du bois de chauffage(total: 8)

Obtenir une hache et couper une bûche produit du bois de chauffage à un coût inférieur à 6 $, tandis que le ramassage des branches produit un bois à un coût plus élevé de 8 €. Notre agent choisit donc de prendre une hache et de couper du bois.

Mais cette même séquence ne fonctionne-t-elle pas tout le temps? Pas si on introduit conditions préalables

Conditions et effets

Les actions ont conditions préalables et effets. Une condition préalable est l'état requis pour l'exécution de l'action et les effets sont la modification de l'état une fois l'action exécutée..

Par exemple, le ChopLog l'action nécessite que l'agent ait une hache à portée de main. Si l'agent n'a pas de hache, il doit trouver une autre action pouvant remplir cette condition préalable afin de laisser le ChopLog action courir. Heureusement la GetAxe l'action fait que c'est l'effet de l'action.

Le planificateur GOAP

Le planificateur GOAP est un morceau de code qui examine les conditions préalables et les effets des actions et crée des files d'attente d'actions qui permettront d'atteindre un objectif. Cet objectif est fourni par l'agent, accompagné d'un état mondial et d'une liste d'actions que l'agent peut effectuer. Avec ces informations, le planificateur GOAP peut ordonner les actions, voir celles qui peuvent être exécutées et celles qui ne le peuvent pas, puis décider quelles actions sont les meilleures à effectuer. Heureusement pour vous, j'ai écrit ce code, vous n'avez donc pas à.

Pour mettre cela en place, ajoutons des conditions préalables et des effets aux actions de notre couperet à bois:

  • GetAxe Coût: 2. Conditions préalables: "une hache est disponible", "n'a pas de hache". Effet: "a une hache".
  • ChopLog Coût: 4. Conditions préalables:"a une hache". Effet: "faire du bois de chauffage"
  • CollectBranches Coût: 8. Conditions préalables: (aucune). Effet: "faire du bois de chauffage".

Le planificateur GOAP dispose désormais des informations nécessaires pour ordonner la séquence d'actions permettant de fabriquer du bois de chauffage (notre objectif).. 

Nous commençons par fournir au planificateur GOAP l’état actuel du monde et l’état de l’agent. Cet état mondial combiné est:

  • "n'a pas de hache"
  • "un hache est disponible"
  • "le soleil brille"

En regardant nos actions disponibles actuelles, la seule partie des états qui les concernent est l'état "n'a pas de hache" et les états "un hache est disponible"; l'autre pourrait être utilisé pour d'autres agents avec d'autres actions.

D'accord, nous avons notre état mondial actuel, nos actions (avec leurs conditions préalables et leurs effets) et l'objectif. Planifions!

OBJECTIF: "fabriquer du bois de chauffage" Etat actuel: "n'a pas de hache", "un hache est disponible" L'action ChopLog peut-elle être exécutée? NON - nécessite une condition préalable "a un axe". Ne peut pas l'utiliser maintenant, essayez une autre action. L'action GetAxe peut-elle être exécutée? OUI, les conditions préalables "un axe est disponible" et "n'a pas un axe" sont vraies. Action PUSH sur la file d'attente, mise à jour de l'état avec l'effet de l'action New State "a un axe" Supprimer l'état "un axe est disponible" car nous venons d'en prendre un. L'action ChopLog peut-elle s'exécuter? OUI, la condition préalable "a un axe" est vraie Action PUSH sur la file d'attente, mise à jour de l'état avec l'effet de l'action Nouvel État "a un axe", "fait du bois de chauffage" Nous avons atteint notre OBJECTIF de "fait du bois de chauffage" Séquence d'action: GetAxe -> ChopLog

Le planificateur exécutera également les autres actions, et il ne s'arrêtera pas lorsqu'il trouvera une solution à l'objectif. Et si une autre séquence a un coût inférieur? Il passera par toutes les possibilités pour trouver la meilleure solution.

Quand il planifie, il construit un arbre. Chaque fois qu'une action est appliquée, elle est supprimée de la liste des actions disponibles, nous n'avons donc pas une chaîne de 50 GetAxe des actions dos à dos. L'état est modifié avec l'effet de cette action.

L'arbre que le planificateur construit ressemble à ceci:

Nous pouvons voir qu'il trouvera en réalité trois chemins vers l'objectif avec leurs coûts totaux:

  • GetAxe -> ChopLog (total: 6)
  • GetAxe -> CollectBranches(total: 10)
  • CollectBranches (total: 8)

Bien que GetAxe -> CollectBranches fonctionne, le chemin le moins cher est GetAxe -> ChopLog, alors celui-ci est retourné.

À quoi ressemblent les conditions préalables et les effets dans le code? Cela dépend de vous, mais j’ai trouvé plus facile de les stocker en tant que paire clé-valeur, où la clé est toujours une chaîne et la valeur est un type d’objet ou primitif (float, int, booléen ou similaire). En C #, cela pourrait ressembler à ceci:

HashSet< KeyValuePair > les conditions préalables; HashSet< KeyValuePair > effets;

Lorsque l'action est exécutée, à quoi ressemblent ces effets et que font-ils? Eh bien, ils ne doivent rien faire, ils sont vraiment utilisés uniquement pour la planification et n'affectent pas l'état de l'agent réel tant qu'ils ne se sont pas lancés dans la course.. 

Cela vaut la peine d'être souligné: planifier des actions n'est pas la même chose que les exécuter. Lorsqu'un agent effectue la GetAxe action, il sera probablement à proximité d’une pile d’outils, jouera une animation courbée et ramassée, puis stockera un objet de la hache dans son sac à dos. Cela change l'état de l'agent. Mais pendant GOAP Planification, le changement d'état n'est que temporaire, afin que le planificateur puisse trouver la solution optimale.

Conditions de procédure

Parfois, les actions doivent faire un peu plus pour déterminer si elles peuvent être exécutées. Par exemple, le GetAxe l'action a la condition préalable "un hache est disponible" qui devra explorer le monde ou les environs immédiats pour voir s'il existe un hache que l'agent peut prendre. Cela pourrait déterminer que la hache la plus proche est trop loin ou derrière les lignes ennemies, et dira qu'elle ne peut pas courir. Cette condition préalable est procédurale et doit exécuter du code; ce n'est pas un simple opérateur booléen que nous pouvons simplement basculer.

Évidemment, certaines de ces conditions préalables de procédure peuvent prendre un certain temps et doivent être effectuées sur autre chose que le fil de rendu, idéalement en tant que fil de fond ou Coroutines (dans Unity)..

Vous pourriez aussi avoir des effets sur la procédure, si vous le souhaitez. Et si vous voulez introduire des résultats encore plus dynamiques, vous pouvez changer le Coût d'actions à la volée!

GOAP et Etat

Notre système GOAP devra vivre dans une petite machine à états finis (FSM), pour la seule raison que, dans de nombreux jeux, les actions doivent être proches de la cible pour être performantes. On se retrouve avec trois états:

  • Tourner au ralenti
  • Déménager à
  • Faire une action

Lorsqu'il est inactif, l'agent déterminera quel objectif il souhaite atteindre. Cette partie est gérée en dehors de GOAP; GOAP vous indiquera simplement les actions que vous pouvez exécuter pour atteindre cet objectif. Lorsqu'un objectif est choisi, il est transmis au planificateur GOAP, ainsi qu'au monde et à l'état de départ de l'agent. Le planificateur renverra une liste d'actions (s'il peut atteindre cet objectif)..

Lorsque le planning est terminé et que l'agent a sa liste d'actions, il essaiera d'exécuter la première action. Toutes les actions devront savoir si elles doivent être à portée d'une cible. Si tel est le cas, le FSM affichera l'état suivant: Déménager à.

le Déménager à state indiquera à l'agent qu'il doit se déplacer vers une cible spécifique. L'agent fera le déplacement (et jouera l'animation de marche), puis fera savoir au FSM quand il se trouvera à portée de la cible. Cet état est ensuite supprimé et l'action peut être exécutée..

le Faire une action state exécutera la prochaine action dans la liste des actions renvoyées par le planificateur GOAP. L'action peut être instantanée ou se prolonger sur plusieurs images, mais lorsqu'elle est terminée, elle est supprimée et l'action suivante est exécutée (à nouveau, après avoir vérifié si l'action suivante doit être effectuée à portée d'un objet)..

Tout cela se répète jusqu’à ce qu’il n’y ait plus aucune action à effectuer, puis on revient au Tourner au ralenti Etat, obtenir un nouvel objectif et planifier à nouveau.

Un exemple de code réel

Il est temps de regarder un exemple réel! Ne t'inquiète pas ce n'est pas si compliqué, et j'ai fourni une copie de travail en Unity et C # que vous pouvez essayer. J'en parlerai brièvement ici pour que vous ayez une idée de l'architecture. Le code utilise certains des mêmes exemples WoodChopper que ci-dessus.

Si vous voulez bien vous plonger, rendez-vous ici pour le code: http://github.com/sploreg/goap

Nous avons quatre ouvriers:

  • Forgeron: transforme le minerai de fer en outil.
  • Enregistreur: utilise un outil pour abattre des arbres afin de produire des journaux.
  • Mineur: extrait des roches avec un outil pour produire du minerai de fer.
  • Bûcheron: utilise un outil pour couper des bûches afin de produire du bois de chauffage.

Les outils s'usent avec le temps et devront être remplacés. Heureusement, le forgeron fabrique des outils. Mais il faut du minerai de fer pour fabriquer des outils; c'est là qu'intervient le mineur (qui a également besoin d'outils). Le bûcheron a besoin de bûches, et celles-ci proviennent du bûcheron; les deux ont besoin d'outils aussi bien.

Les outils et les ressources sont stockés sur des piles de fournitures. Les agents vont collecter les matériaux ou les outils dont ils ont besoin des piles et y déposer leurs produits..

Le code comporte six classes GOAP principales:

  • GoapAgent: comprend l’état et utilise le FSM et GoapPlanner opérer.
  • GoapAction: actions que les agents peuvent effectuer.
  • GoapPlanner: planifie les actions pour la GoapAgent.
  • FSM: la machine à états finis.
  • FSMState: un état dans le FSM.
  • IGoap: l'interface que nos vrais acteurs ouvriers utilisent. Liens avec les événements pour GOAP et les FSM.

Regardons le GoapAction classe, puisque c’est celle que vous allez sous-classer:

classe abstraite publique GoapAction: MonoBehaviour private HashSet> les conditions préalables; HashSet privé> effets; private bool inRange = false; / * Le coût d'exécution de l'action. * Déterminez un poids qui convient à l'action. * Le modifier aura une incidence sur les actions choisies lors de la planification. * / Coût flottant = 1f; / ** * Une action doit souvent être effectuée sur un objet. Ceci est cet objet. Peut être nul. * / cible GameObject publique; public GoapAction () preconditions = new HashSet> (); effets = new HashSet> ();  public void doReset () inRange = false; target = null; réinitialiser ();  / ** * Réinitialise toutes les variables devant être réinitialisées avant que la planification ne se reproduise. * / public abstract void reset (); / ** * L'action est-elle terminée? * / public abstract bool isDone (); / ** * Vérifie de manière procédurale si cette action peut s'exécuter. Toutes les actions * n’auront pas besoin de cela, mais certaines le pourraient. * / public abstract bool checkProceduralPrecondition (agent GameObject); / ** * Exécute l'action. * Retourne True si l'action est réussie ou false * si quelque chose s'est passé et qu'elle ne peut plus être effectuée. Dans ce cas *, la file d'attente des actions doit s'effacer et l'objectif ne peut pas être atteint. * / public abstract bool perform (agent GameObject); / ** * Cette action doit-elle être à portée d'un objet de jeu cible? * Dans le cas contraire, l'état moveTo n'aura pas besoin de s'exécuter pour cette action. * / public abstract bool requireInRange (); / ** * Sommes-nous à portée de la cible? * L'état MoveTo définira ceci et il sera réinitialisé chaque fois que cette action est effectuée. * / public bool isInRange () return inRange;  public void setInRange (bool inRange) this.inRange = inRange;  public void addPrecondition (clé de chaîne, valeur d'objet) preconditions.Add (new KeyValuePair(valeur clé) );  public void removePrecondition (string key) KeyValuePair remove = default (KeyValuePair) foreach (KeyValuePair kvp dans les conditions préalables) if (kvp.Key.Equals (clé)) remove = kvp;  if (! default (KeyValuePair) .Equals (remove)) conditions préalables.Remove (remove);  public void addEffect (clé de chaîne, valeur d'objet) effects.Add (new KeyValuePair(valeur clé) );  public void removeEffect (chaîne de clé) KeyValuePair remove = default (KeyValuePair) foreach (KeyValuePair kvp dans les effets) if (kvp.Key.Equals (clé)) remove = kvp;  if (! default (KeyValuePair) .Equals (remove)) effets.Remove (remove);  HashSet public> Preconditions get return préconditions;  HashSet public> Effets get effets de retour; 

Rien d’exceptionnel ici: il stocke les conditions préalables et les effets. Il sait également s’il doit se trouver à portée d’une cible et, le cas échéant, le FSM sait Déménager à Etat en cas de besoin. Il sait aussi quand cela est fait; qui est déterminé par la classe d'action d'implémentation.

Voici l'une des actions:

Classe publique MineOreAction: GoapAction private bool mined = false; IronRockComponent targetRock privé; // où nous obtenons le minerai de float privé startTime = 0; public float miningDuration = 2; // secondes public MineOreAction () addPrecondition ("hasTool", true); // nous avons besoin d'un outil pour ce faire addPrecondition ("hasOre", false); // si nous en avons, nous ne voulons plus de addEffect ("hasOre", true);  public override void reset () mined = false; targetRock = null; heure de début = 0;  public override bool isDone () return mined;  public public bool requireInRange () return true; // oui, nous devons être près d'un rocher public override bool checkProceduralPrecondition (agent GameObject) // trouver le rocher le plus proche pouvant être extrait IronRockComponent [] rocks = FindObjectsOfType (typeof (IronRockComponent)) comme IronRockComponent []; IronRockComponent la plus proche = null; float la plus procheDist = 0; foreach (IronRockComponent rock in rocks) if (le plus proche == null) // le premier, choisissez-le pour l'instant le plus proche = rock; mostDist = = (rock.gameObject.transform.position - agent.transform.position) .magnitude;  else // est-ce plus proche que le dernier? float dist = (rock.gameObject.transform.position - agent.transform.position) .magnitude; si (dist < closestDist)  // we found a closer one, use it closest = rock; closestDist = dist;    targetRock = closest; target = targetRock.gameObject; return closest != null;  public override bool perform (GameObject agent)  if (startTime == 0) startTime = Time.time; if (Time.time - startTime > miningDuration) // fin de l'extraction de BackpackComponent backpack = (BackpackComponent) agent.GetComponent (typeof (BackpackComponent)); sac à dos.numOre + = 2; miné = vrai; ToolComponent tool = backpack.tool.GetComponent (typeof (ToolComponent)) en tant que ToolComponent; utilisation.outil (0.5f); if (tool.destroyed ()) Destroy (backpack.tool); backpack.tool = null;  return true; 

La plus grande partie de l'action est la checkProceduralPreconditions méthode. Il cherche l'objet de jeu le plus proche avec un IronRockComponent, et enregistre cette cible rock. Ensuite, lorsqu’il exécute sa tâche, il récupère la pierre cible sauvegardée et exécute l’action dessus. Lorsque l'action est réutilisée dans la planification, tous ses champs sont réinitialisés pour pouvoir être à nouveau calculés..

Ce sont tous les composants qui sont ajoutés à la Mineur objet entité dans Unity:


Pour que votre agent fonctionne, vous devez y ajouter les composants suivants:

  • GoapAgent.
  • Une classe qui implémente IGoap (dans l'exemple ci-dessus, c'est Miner.cs).
  • Quelques actions.
  • Un sac à dos (uniquement parce que les actions l'utilisent; il n'est pas lié à GOAP).
Vous pouvez ajouter les actions de votre choix, ce qui changerait le comportement de l'agent. Vous pouvez même tout faire pour qu'il puisse extraire du minerai, forger des outils et couper du bois.

Voici à nouveau la démo en action:

Chaque ouvrier se rend à la cible dont il a besoin pour accomplir son action (arbre, rocher, billot, etc.), exécute l'action et retourne souvent à la pile de fournitures pour y déposer ses marchandises. Le forgeron attendra un peu jusqu'à ce qu'il y ait du minerai de fer dans l'une des piles d'approvisionnement (ajoutée par le mineur). Le forgeron part alors fabriquer des outils et les déposera à la pile de fournitures la plus proche de lui. Lorsque les outils d’un ouvrier se brisent, ils se dirigent vers la pile de fournitures située près du forgeron, où se trouvent les nouveaux outils..

Vous pouvez récupérer le code et l'application complète ici: http://github.com/sploreg/goap.

Conclusion

Avec GOAP, vous pouvez créer une grande série d’actions sans le casse-tête d’états interconnectés, souvent livré avec une machine à états finis. Des actions peuvent être ajoutées et supprimées d'un agent pour produire des résultats dynamiques, ainsi que pour vous garder sain d'esprit lors de la maintenance du code. Vous vous retrouverez avec une intelligence artificielle flexible, intelligente et dynamique.