Pianificazione delle azioni orientate agli obiettivi per un'IA più intelligente

Goal Oriented Action Planning (GOAP) è un sistema di intelligenza artificiale che fornirà facilmente le scelte degli agenti e gli strumenti per prendere decisioni intelligenti senza dover mantenere una macchina a stati finiti ampia e complessa.

Guarda la demo

In questa demo, ci sono quattro classi di caratteri, ognuno dei quali usa strumenti che si rompono dopo essere stati usati per un po ':

  • Minatore: Minerale di minerali sulle rocce. Ha bisogno di uno strumento per funzionare.
  • Logger: taglia gli alberi per produrre i log. Ha bisogno di uno strumento per funzionare.
  • Wood Cutter: taglia gli alberi in legno utilizzabile. Ha bisogno di uno strumento per funzionare.
  • Fabbro: forgia gli strumenti alla fucina. Tutti usano questi strumenti.

Ogni classe si calcola automaticamente, utilizzando la pianificazione dell'azione orientata all'obiettivo, quali azioni devono eseguire per raggiungere i propri obiettivi. Se il loro strumento si rompe, andranno a un mucchio di rifornimenti che ne ha uno fabbricato dal fabbro.

Cos'è GOAP?

La pianificazione dell'azione orientata all'obiettivo è un sistema di intelligenza artificiale per agenti che consente loro di pianificare una sequenza di azioni per soddisfare un particolare obiettivo. La particolare sequenza di azioni dipende non solo dall'obiettivo, ma anche dallo stato attuale del mondo e dall'agente. Ciò significa che se viene fornito lo stesso obiettivo per diversi agenti o stati del mondo, è possibile ottenere una sequenza di azioni completamente diversa. Ciò rende l'intelligenza artificiale più dinamica e realistica. Vediamo un esempio, come mostrato nella demo sopra.

Abbiamo un agente, un trinciapaglia, che prende i tronchi e li taglia a legna. L'elicottero può essere fornito con l'obiettivo MakeFirewood, e ha le azioni ChopLog, GetAxe, e CollectBranches.

Il ChopLog l'azione trasformerà un tronco in legna da ardere, ma solo se il taglialegna ha un'ascia. Il GetAxe l'azione darà al taglia legna un'ascia. Finalmente, il CollectBranches l'azione produrrà anche legna da ardere, senza richiedere un'ascia, ma la legna non avrà la stessa qualità.

Quando diamo l'agente il MakeFirewood obiettivo, otteniamo queste due diverse sequenze d'azione:

  • Ha bisogno di legna da ardere -> GetAxe -> ChopLog = fa legna da ardere
  • Ha bisogno di legna da ardere -> CollectBranches = fa legna da ardere

Se l'agente può ottenere un'ascia, può tagliare un ceppo per fare legna da ardere. Ma forse non possono ottenere un'ascia; quindi, possono semplicemente andare a raccogliere i rami. Ognuna di queste sequenze soddisferà l'obiettivo di MakeFirewood

GOAP può scegliere la sequenza migliore in base a quali precondizioni sono disponibili. Se non c'è un'ascia a portata di mano, il taglialegna deve ricorrere a raccogliere rami. Raccogliere rami può richiedere molto tempo e produrre legna da ardere di scarsa qualità, quindi non vogliamo che funzioni sempre, solo quando deve.

Per chi è GOAP

Probabilmente hai già familiarità con le macchine a stati finiti (FSM), ma in caso contrario, dai un'occhiata a questo fantastico tutorial. 

Potresti esserti imbattuto in stati molto grandi e complessi per alcuni dei tuoi agenti FSM, dove alla fine arrivi a un punto in cui non vuoi aggiungere nuovi comportamenti perché causano troppi effetti collaterali e lacune nell'IA.

GOAP trasforma questo:

Stato macchina a stati finiti: connesso ovunque.

In questo:

GOAP: bello e gestibile.


Dissociando le azioni l'una dall'altra, ora possiamo concentrarci su ciascuna azione individualmente. Ciò rende il codice modulare, facile da testare e da mantenere. Se vuoi aggiungere un'altra azione, puoi semplicemente inserirla e nessuna altra azione deve essere modificata. Prova a farlo con un FSM!

Inoltre, puoi aggiungere o rimuovere azioni al volo per modificare il comportamento di un agente per renderlo ancora più dinamico. Hai un orco che all'improvviso ha iniziato a infuriare? Dare loro una nuova azione "attacco rabbia" che viene rimosso quando si calmano. Semplicemente aggiungendo l'azione all'elenco delle azioni è tutto ciò che devi fare; il pianificatore GOAP si prenderà cura di tutto il resto.

Se trovi che hai un FSM molto complesso per i tuoi agenti, allora dovresti provare GOAP. Un segno che il tuo FSM sta diventando troppo complicato è quando ogni stato ha una miriade di istruzioni if-else che verificano quale stato dovrebbero passare al prossimo, e l'aggiunta di un nuovo stato ti fa gemere di tutte le implicazioni che potrebbe avere.

Se hai un agente molto semplice che esegue solo uno o due compiti, allora GOAP potrebbe essere un po 'pesante e basterà un FSM. Tuttavia, vale la pena guardare i concetti qui e vedere se sarebbero abbastanza facili da inserire nel tuo agente.

Azioni

Un azione è qualcosa che fa l'agente. Di solito si tratta solo di riprodurre un'animazione e un suono e di cambiare un po 'di stato (ad esempio aggiungendo legna da ardere). Aprire una porta è un'azione diversa (e un'animazione) piuttosto che prendere una matita. Un'azione è incapsulata e non dovrebbe preoccuparsi di quali siano le altre azioni.

Per aiutare GOAP a determinare quali azioni vogliamo utilizzare, ogni azione è data a costo. Un'azione ad alto costo non verrà scelta per un'azione a costi inferiori. Quando eseguiamo in sequenza le azioni, sommiamo i costi e quindi scegliamo la sequenza con il costo più basso.

Consente di assegnare alcuni costi alle azioni:

  • GetAxe Costo: 2
  • ChopLog Costo: 4
  • CollectBranches Costo: 8

Se guardiamo di nuovo la sequenza di azioni e sommiamo i costi totali, vedremo quale è la sequenza più economica:

  • Ha bisogno di legna da ardere -> GetAxe (2) -> ChopLog(4) = produce legna da ardere(totale: 6)
  • Ha bisogno di legna da ardere -> CollectBranches(8) = produce legna da ardere(totale: 8)

Ottenere un'ascia e tagliare un ceppo produce legna da ardere al costo inferiore di 6, mentre la raccolta dei rami produce legno al costo più alto di 8. Quindi, il nostro agente sceglie di ottenere un'ascia e tagliare il legno.

Ma questa stessa sequenza non funzionerà sempre? Non se lo presentiamo precondizioni...

Precondizioni ed effetti

Le azioni hanno precondizioni e effetti. Una precondizione è lo stato richiesto per l'azione da eseguire e gli effetti sono la modifica allo stato dopo l'esecuzione dell'azione.

Ad esempio, il ChopLog l'azione richiede all'agente di avere un'ascia a portata di mano. Se l'agente non ha un'ascia, deve trovare un'altra azione che possa adempiere a tale precondizione per consentire a ChopLog corsa d'azione. Fortunatamente, il GetAxe l'azione lo fa: questo è l'effetto dell'azione.

Il pianificatore GOAP

Il pianificatore GOAP è un codice che analizza le precondizioni e gli effetti delle azioni e crea code di azioni che realizzeranno un obiettivo. Tale obiettivo è fornito dall'agente, insieme a uno stato mondiale e un elenco di azioni che l'agente può eseguire. Con queste informazioni, il pianificatore GOAP può ordinare le azioni, vedere quali possono essere eseguite e quali no, e quindi decidere quali azioni sono le migliori da eseguire. Fortunatamente per te, ho scritto questo codice, quindi non devi.

Per configurarlo, aggiungiamo precondizioni ed effetti alle azioni del nostro chopper di legno:

  • GetAxe Costo: 2. Presupposti: "un ascia è disponibile", "non ha un'ascia". Effetto: "ha un'ascia".
  • ChopLog Costo: 4. Prerequisiti:"ha un'ascia". Effetto: "fare legna da ardere"
  • CollectBranches Costo: 8. Prerequisiti: (nessuno). Effetto: "fare legna da ardere".

Il planner GOAP ora ha le informazioni necessarie per ordinare la sequenza di azioni per fare legna da ardere (il nostro obiettivo). 

Iniziamo fornendo a GOAP Planner lo stato attuale del mondo e lo stato dell'agente. Questo stato mondiale combinato è:

  • "non ha un'ascia"
  • "un'ascia è disponibile"
  • "il Sole splende"

Guardando le nostre attuali azioni disponibili, l'unica parte degli stati che sono rilevanti per loro è il "non ha un'ascia" e gli stati "un ascia è disponibile"; l'altro potrebbe essere utilizzato per altri agenti con altre azioni.

Ok, abbiamo il nostro attuale stato mondiale, le nostre azioni (con le loro precondizioni ed effetti) e l'obiettivo. Pianifichiamo!

OBIETTIVO: "fare legna da ardere" Stato attuale: "non ha un'ascia", "un ascia è disponibile" Può agire ChopLog? NO - richiede la precondizione "ha un'ascia" Non posso usarlo ora, prova un'altra azione. L'azione GetAxe può funzionare? SÌ, le precondizioni "un ascia è disponibile" e "non ha un'ascia" sono vere. Azione PUSH sulla coda, stato di aggiornamento con l'effetto dell'azione Nuovo stato "ha un'ascia" Rimuovi stato "è disponibile un ascia" perché ne abbiamo appena preso uno. Può funzionare ChopLog azione? SÌ, la precondizione "ha un'ascia" è vera azione PUSH sulla coda, lo stato di aggiornamento con l'effetto dell'azione Nuovo stato "ha un'ascia", "fa legna da ardere" Abbiamo raggiunto il nostro obiettivo "fa legna da ardere" Sequenza d'azione: GetAxe -> ChopLog

Il pianificatore eseguirà anche le altre azioni e non si fermerà solo quando troverà una soluzione all'obiettivo. Cosa succede se un'altra sequenza ha un costo inferiore? Correrà attraverso tutte le possibilità per trovare la soluzione migliore.

Quando pianifica, si costruisce a albero. Ogni volta che viene applicata un'azione, viene fuori dalla lista delle azioni disponibili, quindi non abbiamo una stringa di 50 GetAxe azioni back-to-back. Lo stato è cambiato con l'effetto di quella azione.

L'albero che il planner crea è simile a questo:

Possiamo vedere che in realtà troverà tre percorsi per raggiungere l'obiettivo con i loro costi totali:

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

Sebbene GetAxe -> CollectBranches funziona, il percorso più economico è GetAxe -> ChopLog, quindi questo è restituito.

Che aspetto hanno le precondizioni e gli effetti nel codice? Bene, dipende da voi, ma ho trovato più semplice archiviarli come coppia chiave-valore, dove la chiave è sempre una stringa e il valore è un oggetto o tipo primitivo (float, int, booleano o simile). In C #, potrebbe assomigliare a questo:

HashSet< KeyValuePair > precondizioni; HashSet< KeyValuePair > effetti;

Quando l'azione è in corso, che aspetto hanno questi effetti e che cosa fanno? Beh, non devono fare niente, sono solo usati per la pianificazione e non influenzano lo stato dell'agente reale finché non corrono per davvero. 

Vale la pena sottolinearlo: le azioni di pianificazione non sono le stesse che eseguirle. Quando un agente esegue il GetAxe azione, sarà probabilmente vicino a una pila di strumenti, eseguirà un'animazione di piegatura verso il basso e quindi memorizzerà un oggetto ascia nello zaino. Questo cambia lo stato dell'agente. Ma, durante GOAP pianificazione, il cambiamento di stato è solo temporaneo, in modo che il pianificatore possa capire la soluzione ottimale.

Prerequisiti procedurali

A volte, le azioni devono fare un po 'di più per determinare se possono essere eseguite. Ad esempio, il GetAxe l'azione ha la precondizione di "un'ascia disponibile" che dovrà cercare nel mondo, o nelle immediate vicinanze, per vedere se c'è un'ascia che l'agente può prendere. Potrebbe determinare che l'ascia più vicina è troppo lontana o dietro le linee nemiche e dirà che non può correre. Questa precondizione è procedurale e richiede l'esecuzione di un codice; non è un semplice operatore booleano che possiamo semplicemente attivare.

Ovviamente, alcune di queste precondizioni procedurali possono richiedere un po 'di tempo per essere eseguite, e dovrebbero essere eseguite su qualcosa di diverso dal thread di rendering, idealmente come thread in background o come Coroutine (in Unity).

Potresti avere anche effetti procedurali, se lo desideri. E se vuoi introdurre risultati ancora più dinamici, puoi cambiare il costo di azioni al volo!

GOAP e stato

Il nostro sistema GOAP dovrà vivere in una piccola macchina a stati finiti (FSM), per la sola ragione che, in molti giochi, le azioni dovranno essere vicine a un obiettivo per poter essere eseguite. Finiamo con tre stati:

  • Inattivo
  • MoveTo
  • PerformAction

Quando è inattivo, l'agente individuerà quale obiettivo vogliono raggiungere. Questa parte è gestita al di fuori di GOAP; GOAP ti dirà solo quali azioni puoi eseguire per raggiungere quell'obiettivo. Quando viene scelto un obiettivo, viene passato a GOAP Planner, insieme allo stato di partenza del mondo e dell'agent, e il planner restituirà un elenco di azioni (se può raggiungere tale obiettivo).

Quando il planner è terminato e l'agente ha il suo elenco di azioni, proverà a eseguire la prima azione. Tutte le azioni dovranno sapere se devono essere nel raggio di azione di un obiettivo. Se lo fanno, allora l'FSM passerà allo stato successivo: MoveTo.

Il MoveTo lo stato dirà all'agente che deve spostarsi su un obiettivo specifico. L'agente farà lo spostamento (e riprodurrà l'animazione di camminata), quindi farà sapere al FSM quando si trova nel raggio di azione del bersaglio. Questo stato viene quindi estratto e l'azione può essere eseguita.

Il PerformAction lo stato eseguirà l'azione successiva nella coda di azioni restituite da GOAP Planner. L'azione può essere istantanea o durare per molti fotogrammi, ma quando viene eseguita viene eliminata e quindi viene eseguita l'azione successiva (di nuovo, dopo aver controllato se l'azione successiva deve essere eseguita entro il raggio di un oggetto).

Tutto ciò si ripete fino a quando non ci sono azioni da eseguire, a quel punto torniamo al Inattivo stato, ottieni un nuovo obiettivo e pianifica di nuovo.

Un esempio di codice reale

È tempo di dare un'occhiata ad un esempio reale! Non preoccuparti; non è così complicato, e ho fornito una copia di lavoro in Unity e C # per provarlo. Ne parlerò brevemente qui in modo da avere un'idea dell'architettura. Il codice utilizza alcuni degli stessi esempi di WoodChopper come sopra.

Se vuoi scavare direttamente, vai qui per il codice: http://github.com/sploreg/goap

Abbiamo quattro lavoratori:

  • Fabbro: trasforma il minerale di ferro in utensili.
  • Logger: utilizza uno strumento per abbattere alberi per produrre registri.
  • Minatore: miniera di rocce con uno strumento per la produzione di minerale di ferro.
  • Taglia legna: usa uno strumento per tagliare tronchi per produrre legna da ardere.

Gli strumenti si consumano nel tempo e dovranno essere sostituiti. Fortunatamente, il fabbro fa strumenti. Ma il minerale di ferro è necessario per creare utensili; è qui che entra in gioco il Minatore (che ha anche bisogno di strumenti). The Wood Cutter ha bisogno di registri, e quelli provengono dal Logger; entrambi hanno bisogno anche di strumenti.

Strumenti e risorse sono memorizzati su pile di scorta. Gli agenti raccoglieranno i materiali o gli strumenti di cui hanno bisogno dai mucchietti, e consegneranno loro il loro prodotto.

Il codice ha sei classi GOAP principali:

  • GoapAgent: capisce lo stato e usa il FSM e GoapPlanner operare.
  • GoapAction: azioni che gli agenti possono eseguire.
  • GoapPlanner: pianifica le azioni per il GoapAgent.
  • FSM: la macchina a stati finiti.
  • FSMState: uno stato nell'FSM.
  • IGoap: l'interfaccia che usano i nostri veri attori laburisti. Legami con gli eventi per GOAP e l'FSM.

Diamo un'occhiata al GoapAction classe, dato che è quello che sottoclassi:

public abstract class GoapAction: MonoBehaviour private HashSet> precondizioni; HashSet privato> effetti; private bool inRange = false; / * Il costo dell'esecuzione dell'azione. * Calcola un peso che si adatta all'azione. * La modifica influirà sulle azioni che vengono scelte durante la pianificazione. * / Costo float pubblico = 1f; / ** * Un'azione deve spesso essere eseguita su un oggetto. Questo è quell'oggetto. Può essere nullo * / target pubblico di GameObject; public GoapAction () preconditions = new HashSet> (); effects = new HashSet> ();  public void doReset () inRange = false; target = null; reset ();  / ** * Ripristina le variabili che devono essere ripristinate prima che la pianificazione si ripeta. * / public abstract void reset (); / ** * L'azione è stata fatta? * / public abstract bool isDone (); / ** * Controlla in modo procedurale se questa azione può essere eseguita. Non tutte le azioni * avranno bisogno di questo, ma alcune potrebbero. * / public abstract bool checkProceduralPrecondition (agente GameObject); / ** * Esegui l'azione. * Restituisce True se l'azione viene eseguita correttamente o falsamente * se qualcosa è successo e non può più essere eseguito. In questo caso * la coda azione dovrebbe essere eliminata e l'obiettivo non può essere raggiunto. * / public abstract bool perform (agente di GameObject); / ** * Questa azione deve essere nel raggio d'azione di un oggetto di gioco di destinazione? * In caso contrario, lo stato moveTo non dovrà essere eseguito per questa azione. * / public abstract bool richiedeInRange (); / ** * Siamo nel raggio d'azione dell'obiettivo? * Lo stato MoveTo lo imposterà e verrà ripristinato ogni volta che viene eseguita questa azione. * / public bool isInRange () return inRange;  public void setInRange (bool inRange) this.inRange = inRange;  public void addPrecondition (chiave stringa, valore oggetto) preconditions.Add (new KeyValuePair(chiave, valore));  public void removePrecondition (string key) KeyValuePair remove = default (KeyValuePair); foreach (KeyValuePair kvp in precondizioni) if (kvp.Key.Equals (chiave)) remove = kvp;  if (! default (KeyValuePairPrecondizioni. Rimuovi (rimuovi));  public void addEffect (chiave stringa, valore oggetto) effects.Add (new KeyValuePair(chiave, valore));  public void removeEffect (stringa chiave) KeyValuePair remove = default (KeyValuePair); foreach (KeyValuePair kvp in effetti) if (kvp.Key.Equals (chiave)) remove = kvp;  if (! default (KeyValuePair) .Equaliti (rimuovi)) effects.Remove (rimuovi);  HashSet pubblico> Precondizioni ottenere precondizioni di ritorno;  HashSet pubblico> Effetti ottieni effetti di ritorno; 

Niente di troppo fantasia qui: memorizza precondizioni ed effetti. Sa anche se deve essere nel raggio d'azione di un obiettivo e, in tal caso, allora l'FSM sa spingere il MoveTo stato quando necessario. Sa anche quando è finito; che è determinato dalla classe di azioni di implementazione.

Ecco una delle azioni:

public class MineOreAction: GoapAction private bool mined = false; private IronRockComponent targetRock; // dove otteniamo il minerale da float privato startTime = 0; public float miningDuration = 2; // seconds public MineOreAction () addPrecondition ("hasTool", true); // abbiamo bisogno di uno strumento per fare questo addPrecondition ("hasOre", false); // se abbiamo minerale non vogliamo più addEffect ("hasOre", true);  public override void reset () mined = false; targetRock = null; startTime = 0;  public override bool isDone () return mined;  public override bool richiedeInRange () return true; // sì abbiamo bisogno di stare vicino a un rock public override bool checkProceduralPrecondition (agente GameObject) // trova il rock più vicino che possiamo estrarre IronRockComponent [] rocks = FindObjectsOfType (typeof (IronRockComponent)) come IronRockComponent []; IronRockComponent closest = null; float closestDist = 0; foreach (IronRockComponent rock in rocks) if (closest == null) // primo, quindi sceglierlo per now closest = rock; closestDist = (rock.gameObject.transform.position - agent.transform.position) .magnitude;  else // questo è più vicino dell'ultimo? float dist = (rock.gameObject.transform.position - agent.transform.position) .magnitude; se (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) // finished mining BackpackComponent backpack = (BackpackComponent) agent.GetComponent (typeof (BackpackComponent)); backpack.numOre + = 2; mined = true; ToolComponent tool = backpack.tool.GetComponent (typeof (ToolComponent)) come ToolComponent; tool.use (0.5f); if (tool.destroyed ()) Destroy (backpack.tool); backpack.tool = null;  return true; 

La più grande parte dell'azione è il checkProceduralPreconditions metodo. Cerca l'oggetto di gioco più vicino con un IronRockComponent, e salva questo obiettivo rock. Poi, quando si esibisce, ottiene quella roccia bersaglio salvata ed eseguirà l'azione su di essa. Quando l'azione viene riutilizzata nuovamente nella pianificazione, tutti i relativi campi vengono reimpostati in modo che possano essere nuovamente calcolati.

Questi sono tutti componenti che vengono aggiunti al Minatore oggetto entità in Unity:


Affinché il tuo agente funzioni, devi aggiungere i seguenti componenti:

  • GoapAgent.
  • Una classe che implementa IGoap (nell'esempio sopra, questo è Miner.cs).
  • Alcune azioni.
  • Uno zaino (solo perché le azioni lo usano, non è correlato a GOAP).
Puoi aggiungere qualsiasi azione tu voglia e questo cambierebbe il comportamento dell'agente. Potresti anche dargli tutte le azioni in modo da poter estrarre minerali, forgiare utensili e tritare il legno.

Ecco la demo in azione di nuovo:

Ogni lavoratore va verso l'obiettivo di cui ha bisogno per compiere la propria azione (albero, roccia, ceppo, o qualsiasi altra cosa), esegue l'azione e spesso torna al mucchio di rifornimenti per abbandonare i propri beni. Il fabbro aspetterà ancora un po 'finché non ci sarà del minerale di ferro in una delle pile di riserva (aggiunta dal minatore). Il fabbro quindi si spegne e crea gli attrezzi, e lascerà gli strumenti sul mucchio di scorta più vicino a lui. Quando lo strumento di un operaio si rompe, si dirigono verso il mucchio di rifornimenti vicino al fabbro dove si trovano i nuovi strumenti.

Puoi prendere il codice e l'app completa qui: http://github.com/sploreg/goap.

Conclusione

Con GOAP, puoi creare una vasta serie di azioni senza il malcontento degli stati interconnessi che spesso viene fornito con una macchina a stati finiti. Le azioni possono essere aggiunte e rimosse da un agente per produrre risultati dinamici, oltre a mantenerti sano quando si mantiene il codice. Finirai con un'IA flessibile, intelligente e dinamica.