Målorienterad handlingsplanering för en smartare AI

Målorienterad handlingsplanering (GOAP) är ett AI-system som enkelt kan ge dina agenter val och verktygen för att fatta smarta beslut utan att behöva behålla en stor och komplex finite state machine.

Visa demo

I den här demonstrationen finns det fyra teckenklasser, som varje använder verktyg som bryts efter att ha använts ett tag:

  • Miner: Myntmalm i stenar. Behöver ett verktyg att fungera.
  • Logger: Chops träd för att producera stockar. Behöver ett verktyg att fungera.
  • Träskärare: skär träd i användbart trä. Behöver ett verktyg att fungera.
  • Smed: Smidesverktyg på smeden. Alla använder dessa verktyg.

Varje klass kommer att räkna ut automatiskt, med hjälp av målorienterad handlingsplanering, vilka åtgärder de behöver göra för att nå sina mål. Om deras verktyg bryts, kommer de att gå till en leverans som har gjorts av smeden.

Vad är GOAP?

Målorienterad handlingsplanering är ett artificiellt intelligenssystem för agenter som gör det möjligt för dem att planera en sekvens av åtgärder för att tillfredsställa ett visst mål. Den specifika åtgärdssekvensen beror inte bara på målet utan också på världens nuvarande och agentens nuvarande tillstånd. Detta innebär att om samma mål tillhandahålls för olika agenter eller världsstater kan du få en helt annan sekvens av åtgärder. Vilket gör AI mer dynamisk och realistisk. Låt oss titta på ett exempel, vilket framgår av demo ovan.

Vi har en agent, en trähackare, som tar loggar och huggar dem upp i ved. Hackaren kan levereras med målet MakeFirewood, och har handlingarna ChopLog, GetAxe, och CollectBranches.

De ChopLog åtgärd kommer att göra en logg i ved, men bara om träklipparen har en yxa. De GetAxe åtgärd kommer att ge vedklyven en yxa. Slutligen, den CollectBranches Åtgärder kommer också att producera ved, utan att kräva en yxa, men veden kommer inte att vara så hög i kvalitet.

När vi ger agenten den MakeFirewood mål, vi får dessa två olika åtgärdssekvenser:

  • Behöver ved -> GetAxe -> ChopLog = gör ved
  • Behöver ved -> CollectBranches = gör ved

Om agenten kan få en ax, så kan de hugga en timmer för att göra ved. Men kanske kan de inte få en öxa; då kan de bara gå och samla grenar. Var och en av dessa sekvenser kommer att uppfylla målet för MakeFirewood

GOAP kan välja den bästa sekvensen baserat på vilka förutsättningar som finns tillgängliga. Om det inte finns någon öx handy, måste träskäraren ta hand om att plocka upp grenar. Att plocka upp grenar kan ta en mycket lång tid och ge brist av bristfällig kvalitet, så vi vill inte att den ska springa hela tiden, bara när den måste.

Vem GOAP är för

Du är förmodligen bekant med Finite State Machines (FSM), men om inte, ta en titt på denna fantastiska handledning. 

Du kan ha stött på mycket stora och komplexa tillstånd för några av dina FSM-agenter, där du så småningom kommer till en punkt där du inte vill lägga till nya beteenden eftersom de orsakar för många biverkningar och luckor i AI.

GOAP gör det här:

Finite State Machine anger: ansluten överallt.

In i detta:

GOAP: snyggt och hanterbart.


Genom att koppla bort handlingarna från varandra kan vi nu fokusera på varje åtgärd individuellt. Detta gör koden modulär och lätt att testa och underhålla. Om du vill lägga till en annan åtgärd kan du bara plunk det och inga andra åtgärder måste ändras. Försök att göra det med en FSM!

Du kan också lägga till eller ta bort åtgärder som är på gång för att ändra agenternas beteende för att göra dem ännu mer dynamiska. Har en ogre som plötsligt började rasande? Ge dem en ny "rage attack" -åtgärd som blir borttagen när de lugnar sig. Att bara lägga till åtgärden till listan över åtgärder är allt du behöver göra; GOAP-planeraren tar hand om resten.

Om du tycker att du har en mycket komplex FSM för dina agenter, ska du ge GOAP ett försök. Ett tecken på att din MSM blir alltför komplicerad är när varje stat har en myriad av if-else-utlåtanden som testa vilket tillstånd de ska gå till nästa och att lägga till i en ny stat gör dig stön över alla de konsekvenser det kan ha.

Om du har en mycket enkel agent som bara utför en eller två uppgifter, kan GOAP vara lite tunghänt och en FSM räcker. Det är dock värt att titta på koncepten här och se om de skulle vara tillräckligt lätta för att du ska ansluta till din agent.

Åtgärder

En verkan är något som agenten gör. Vanligtvis spelar det bara en animering och ett ljud, och byter lite stat (till exempel lägger ved). Att öppna en dörr är en annan åtgärd (och animering) än att plocka upp en penna. En åtgärd är inkapslad och borde inte behöva oroa sig för vad de andra åtgärderna är.

För att hjälpa GOAP bestämma vilka åtgärder vi vill använda, varje åtgärd ges a kosta. En högkostnadsåtgärd kommer inte att väljas utöver en lägre kostnadsåtgärd. När vi sekvenserar åtgärderna tillsammans lägger vi upp kostnaderna och väljer sedan sekvensen med lägsta kostnad.

Låt oss ange några kostnader för åtgärderna:

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

Om vi ​​tittar på sekvensen av åtgärder igen och lägger till de totala kostnaderna ser vi vad den billigaste sekvensen är:

  • Behöver ved -> GetAxe (2) -> ChopLog(4) = gör ved(totalt: 6)
  • Behöver ved -> CollectBranches(8) = gör ved(totalt: 8)

Att ta en öx och hugga en timmer producerar ved till en lägre kostnad av 6, medan insamling av grenarna producerar trä till den högre kostnaden av 8. Så väljer vår agent att få en yxa och hugga ved.

Men kommer inte samma sekvens att köras hela tiden? Inte om vi presenterar förutsättningar...

Förutsättningar och effekter

Åtgärder har förutsättningar och effekter. En förutsättning är det tillstånd som krävs för att åtgärden ska springa, och effekterna är förändringen till staten efter att åtgärden har gått.

Till exempel, ChopLog åtgärd kräver att agenten har en ös handy. Om agenten inte har en yxa behöver den hitta en annan åtgärd som kan uppfylla denna förutsättning för att låta ChopLog action run. Lyckligtvis, den GetAxe åtgärd gör det - det här är effekten av åtgärden.

GOAP Planner

GOAP-planeraren är ett stycke kod som tittar på åtgärdernas förutsättningar och effekter och skapar köer av åtgärder som kommer att uppfylla ett mål. Det målet tillhandahålls av agenten tillsammans med ett världsland och en lista över åtgärder som agenten kan utföra. Med denna information kan GOAP-planeraren beställa åtgärderna, se vilka som kan köras och vilka inte kan, och sedan bestämma vilka åtgärder som är bäst att utföra. Lyckligtvis för dig, jag har skrivit den här koden, så du behöver inte.

För att ställa upp detta kan vi lägga till förutsättningar och effekter för våra trähackers handlingar:

  • GetAxe Kostnad: 2. Förutsättningar: "en ax är tillgänglig", "har ingen ax". Effekt: "har en yxa".
  • ChopLog Kostnad: 4. Förutsättningar:"har en yxa". Effekt: "gör ved"
  • CollectBranches Kostnad: 8. Förutsättningar: (ingen). Effekt: "gör ved".

GOAP-planeraren har nu den information som behövs för att beställa sekvensen av åtgärder för att göra ved (vårt mål). 

Vi börjar med att leverera GOAP Planner med världens nuvarande tillstånd och agentens tillstånd. Detta kombinerade världsstat är:

  • "har inte en yxa"
  • "en axel är tillgänglig"
  • "solen skiner"

Med tanke på våra nuvarande tillgängliga åtgärder är den enda delen av de stater som är relevanta för dem "den inte har en yxa" och "en axel är tillgänglig" den andra kan användas för andra agenter med andra åtgärder.

Okej, vi har vårt nuvarande världsstat, våra handlingar (med deras förutsättningar och effekter) och målet. Låt oss planera!

MÅL: "gör ved" Aktuell stat: "har ingen yxa", "en yxa är tillgänglig" Kan åtgärd ChopLog springa? NEJ - kräver förutsättning "har en yxa" Kan inte använda den nu, försök med en annan åtgärd. Kan åtgärder GetAxe springa? JA, förutsättningar "en ax är tillgänglig" och "har ingen ax" är sant. PUSH-åtgärd i kö, uppdatera tillstånd med åtgärds effekt New State "har en yxa" Ta bort stat "en ax är tillgänglig" eftersom vi bara tog en. Kan åtgärder ChopLog springa? JA, förutsättning att "har en yxa" är sann PUSH-åtgärd i kö, uppdatera tillstånd med åtgärds effekt. Nya staten "har en yxa", "gör ved". Vi har nått vår mål av "gör ved". Åtkomstsekvens: GetAxe -> ChopLog

Planeraren kommer också att köra genom de andra åtgärderna, och det kommer inte bara sluta när det hittar en lösning på målet. Vad händer om en annan sekvens har en lägre kostnad? Det kommer att löpa igenom alla möjligheter för att hitta den bästa lösningen.

När den planerar bygger den upp en träd. Varje gång en åtgärd tillämpas appliceras den av listan över tillgängliga åtgärder, så vi har inte en sträng på 50 GetAxe åtgärder bakåt mot rygg. Staten förändras med den effekten av den här åtgärden.

Träet som planeraren bygger upp ser så här ut:

Vi kan se att det faktiskt kommer att hitta tre vägar till målet med sina totala kostnader:

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

Fastän GetAxe -> CollectBranches fungerar, den billigaste vägen är GetAxe -> ChopLog, så den här är tillbaka.

Vilka förutsättningar och effekter ser faktiskt ut i koden? Tja, det är upp till dig, men jag har funnit det enklast att lagra dem som en nyckelvärdespar, där nyckeln alltid är en sträng och värdet är ett objekt eller en primitiv typ (float, int, booleska eller liknande). I C # kan det se ut så här:

HashSet< KeyValuePair > förutsättningar HashSet< KeyValuePair > effekter;

När verkan utförs, hur ser dessa effekter ut och vad gör de? Tja, de behöver inte göra någonting. De är egentligen bara vana vid planering och påverkar inte den reella agentens tillstånd förrän de går för riktiga. 

Det här är värt att betona: planeringsåtgärder är inte detsamma som att köra dem. När en agent utför GetAxe åtgärd kommer det förmodligen att vara nära en hög med verktyg, spela en bend-down-and-pick-animation och sedan lagra ett axelobjekt i sin ryggsäck. Detta ändrar agentens tillstånd. Men under GOAP planera, Statens förändring är bara tillfällig, så att planeraren kan räkna ut den optimala lösningen.

Procedurella förutsättningar

Ibland måste åtgärder göra lite mer för att avgöra om de kan köras. Till exempel, GetAxe åtgärd har förutsättningen att "en yxa är tillgänglig" som måste söka i världen, eller omedelbar närhet, för att se om det finns en yxa som agenten kan ta. Det kan avgöra att närmaste axel är bara för långt bort eller bakom fiendens linjer, och säger att den inte kan springa. Denna förutsättning är procedurell och behöver köra någon kod; Det är inte en enkel booleskt operatör som vi bara kan byta.

Självklart kan vissa av dessa procedurbetingelser ta ett tag att springa och bör utföras på något annat än rendertråden, helst som en bakgrundstråd eller som Coroutines (i Unity).

Du kan också ha processuella effekter, om du så önskar. Och om du vill introducera ännu mer dynamiska resultat kan du ändra kosta av åtgärder på flyg!

GOAP och State

Vårt GOAP-system kommer att behöva leva i en liten Finite State Machine (FSM), av den enda anledningen att åtgärder i många spel måste vara nära ett mål för att kunna utföra. Vi hamnar i tre stater:

  • På tomgång
  • Flytta till
  • PerformAction

När den är tomgång, kommer agenten att ta reda på vilket mål de vill uppfylla. Denna del hanteras utanför GOAP; GOAP kommer bara att berätta vilka åtgärder du kan köra för att utföra det målet. När ett mål väljs överförs det till GOAP-planeraren tillsammans med start- och startmedlet för världen och agent och planeraren kommer att returnera en lista över åtgärder (om det kan uppfylla det målet).

När planeraren är klar och agenten har sin lista över åtgärder, försöker den att utföra den första åtgärden. Alla åtgärder måste veta om de måste ligga inom ett målområde. Om de gör det, kommer FSM att trycka på nästa tillstånd: Flytta till.

De Flytta till staten kommer att berätta för agenten att det behöver flyttas till ett visst mål. Agenten kommer att flytta (och spela promenadanimationen), och låt sedan FSM veta när den ligger inom räckvidden av målet. Detta tillstånd poppas sedan av och åtgärden kan utföra.

De PerformAction tillstånd kommer att köra nästa åtgärd i kön av åtgärder som returneras av GOAP-planeraren. Åtgärden kan vara momentan eller sist över många ramar, men när den är klar blir den avstängd och därefter utförs nästa åtgärd (igen efter att ha kontrollerat om den nästa åtgärden behöver utföras inom ett objekts räckvidd).

Allt detta upprepas tills det inte finns några åtgärder kvar att utföra, vid vilken tidpunkt går vi tillbaka till På tomgång stat, få ett nytt mål och planera igen.

Ett riktigt kodexempel

Det är dags att ta en titt på ett riktigt exempel! Oroa dig inte; det är inte så komplicerat, och jag har skrivit en arbetsexempel i Unity och C # för dig att prova. Jag ska bara prata om det kort här så att du får en känsla för arkitekturen. Koden använder några av samma WoodChopper-exempel som ovan.

Om du vill gräva rätt in, huvud här för koden: http://github.com/sploreg/goap

Vi har fyra arbetare:

  • Smed: förvandlar järnmalm till verktyg.
  • Logger: använder ett verktyg för att hugga ner träd för att producera stockar.
  • Miner: miner gruvor med ett verktyg för att producera järnmalm.
  • Wood cutter: använder ett verktyg för att hugga loggar för att producera ved.

Verktyg slits ut över tiden och måste bytas ut. Lyckligtvis gör smeden verktyg. Men järnmalm behövs för att göra verktyg; Det är där Mineren kommer in (som också behöver verktyg). Wood Cutter behöver loggar, och de kommer från loggen; båda behöver också verktyg.

Verktyg och resurser lagras på matriser. Agenterna samlar in de material eller verktyg de behöver från staplarna, och släpper också av deras produkt på dem.

Koden har sex huvudsakliga GOAP-klasser:

  • GoapAgent: förstår tillstånd och använder FSM och GoapPlanner att driva.
  • GoapAction: Åtgärder som agenter kan utföra.
  • GoapPlanner: planerar åtgärderna för GoapAgent.
  • FSM: den finita statliga maskinen.
  • FSMState: en stat i fsm.
  • IGoap: gränssnittet som våra verkliga Laborers aktörer använder. Slår till händelser för GOAP och FSM.

Låt oss titta på GoapAction klass, eftersom det är det du kommer att underklassera:

offentlig abstrakt klass GoapAction: MonoBehaviour private HashSet> förutsättningar privat HashSet> effekter; privat bool inRange = false; / * Kostnaden för att utföra åtgärden. * Skriv ut en vikt som passar åtgärden. * Ändra det kommer att påverka vilka åtgärder som väljs under planering. * / Public float cost = 1f; / ** * En åtgärd måste ofta utföra på ett objekt. Detta är det objektet. Kan vara null. * / offentligt GameObject-mål offentliga GoapAction () preconditions = new HashSet> (); effects = new HashSet> ();  public void doReset () inRange = false; mål = null; återställ ();  / ** * Återställ alla variabler som måste återställas innan planeringen händer igen. * / Offentlig abstrakt tomt återställning (); / ** * Är åtgärden klar? * / offentlig abstrakt bool isDone (); / ** * Kontrollera proceduren om den här åtgärden kan köras. Inte alla åtgärder * behöver detta, men vissa kanske. * / offentlig abstrakt bool checkProceduralPrecondition (GameObject agent); / ** * Kör åtgärden. * Returnerar True om åtgärden utfördes framgångsrikt eller felaktigt * om något hände och det kan inte längre utföra. I det här fallet bör åtgärdskön rensa ut och målet kan inte nås. * / public abstract bool utför (GameObject agent); / ** * Behöver denna åtgärd vara inom räckvidd för ett målspelobjekt? * Om inte, behöver inte moveTo-staten springa för den här åtgärden. * / public abstract bool requiresInRange (); / ** * Är vi inom räckvidden av målet? * MoveTo-staten ställer in detta och det återställs varje gång denna åtgärd utförs. * / public bool isInRange () returnera iRange;  public void setInRange (bool inRange) this.inRange = inRange;  public void addPrecondition (strängnyckel, objektvärde) preconditions.Add (ny KeyValuePair(nyckelvärde) );  public void removePrecondition (strängnyckel) KeyValuePair remove = default (KeyValuePair); foreach (KeyValuePair kvp i förutsättningar) if (kvp.Key.Equals (key)) remove = kvp;  om (! default (KeyValuePair) .Equals (remove)) preconditions.Remove (remove);  public void addEffect (strängnyckel, objektvärde) effects.Add (ny KeyValuePair(nyckelvärde) );  public void removeEffect (strängnyckel) KeyValuePair remove = default (KeyValuePair); foreach (KeyValuePair kvp i effekter) if (kvp.Key.Equals (key)) remove = kvp;  om (! default (KeyValuePair) .Equals (remove)) effects.Remove (remove);  offentliga HashSet> Förutsättningar get return preconditions;  offentliga HashSet> Effekter get return effects; 

Inget alltför fint här: det lagrar förutsättningar och effekter. Det vet också om det måste ligga inom ett mål, och om så är fallet, vet MK att man trycker på Flytta till Ange när det behövs. Det vet när det är gjort också; som bestäms av handlingsklassen.

Här är en av handlingarna:

allmän klass MineOreAction: GoapAction privat bool mined = false; privat IronRockComponent targetRock; // där vi får malmen från privat float startTime = 0; offentlig float miningDuration = 2; // sekunder allmän MineOreAction () addPrecondition ("hasTool", true); // Vi behöver ett verktyg för att göra detta addPrecondition ("hasOre", false); // Om vi ​​har malm vill vi inte ha mer addEffect ("hasOre", true);  Offentlig åsidosätt nollställs () mined = false; targetRock = null; starttid = 0;  offentlig åsidosätt bool isDone () return mined;  public override bool requiresInRange () return true; // ja vi måste vara nära en rock public override bool checkProceduralPrecondition (GameObject agent) // hitta närmaste rock som vi kan mina IronRockComponent [] rocks = FindObjectsOfType (typeof (IronRockComponent)) som IronRockComponent []; IronRockComponent närmast = null; float closestDist = 0; foreach (IronRockComponent rock i stenar) om (närmaste == null) // första, så välj det för närmaste = rock; closestDist = (rock.gameObject.transform.position - agent.transform.position) .magnitude;  annars // är den här närmare än den sista? float dist = (rock.gameObject.transform.position - agent.transform.position) .magnitude; om (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) // färdig gruvdrift BackpackComponent ryggsäck = (BackpackComponent) agent.GetComponent (typof (BackpackComponent)); backpack.numOre + = 2; minas = sant; ToolComponent tool = backpack.tool.GetComponent (typof (ToolComponent)) som ToolComponent; tool.use (0.5f); om (tool.destroyed ()) Destroy (backpack.tool); backpack.tool = null;  returnera sant; 

Den största delen av åtgärden är checkProceduralPreconditions metod. Det letar efter närmaste spelobjekt med en IronRockComponent, och sparar denna målrock. Då, när den utförs, blir det som sparat målrock och kommer att utföra åtgärden på den. När åtgärden återanvänds i planeringen återställs alla fält så att de kan beräknas igen.

Dessa är alla komponenter som läggs till Gruvarbetare enhet objekt i enhet:


För att din agent ska kunna fungera måste du lägga till följande komponenter för den:

  • GoapAgent.
  • En klass som implementerar IGoap (i ovanstående exempel är det Miner.cs).
  • Några åtgärder.
  • En ryggsäck (bara för att åtgärderna använder den, det är inte relaterat till GOAP).
Du kan lägga till vilka åtgärder du vill ha, och detta skulle förändra hur agenten beter sig. Du kan till och med ge alla handlingar så att det kan min malm, smidiga verktyg och hugga ved.

Här är demonstrationen igen:

Varje arbetare går till målet som de behöver för att uppfylla sina åtgärder (träd, sten, huggblock eller vad som helst), utför åtgärden och återvänder ofta till leveranshallen för att släppa ut sina varor. Smeden väntar en liten stund tills det finns järnmalm i en av tillförselpinnarna (tillsatt av miner). Smeden går sedan av och gör verktyg, och släpper av verktygen vid tillförselhögen närmast honom. När en arbetares verktyg bryts kommer de att leda till leveranshögen nära smeden där de nya verktygen är.

Du kan ta tag i koden och hela appen här: http://github.com/sploreg/goap.

Slutsats

Med GOAP kan du skapa en stor serie av handlingar utan huvudvärk hos sammankopplade stater som ofta levereras med en Finite State Machine. Åtgärder kan läggas till och tas bort från en agent för att ge dynamiska resultat, liksom att hålla dig ren när du behåller koden. Du kommer att sluta med en flexibel, smart och dynamisk AI.