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.
I den här demonstrationen finns det fyra teckenklasser, som varje använder verktyg som bryts efter att ha använts ett tag:
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.
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:
GetAxe
-> ChopLog
= gör vedCollectBranches
= gör vedOm 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.
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.
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: 2ChopLog
Kostnad: 4CollectBranches
Kostnad: 8Om vi tittar på sekvensen av åtgärder igen och lägger till de totala kostnaderna ser vi vad den billigaste sekvensen är:
GetAxe
(2) -> ChopLog
(4) = gör ved(totalt: 6)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...
Å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-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:
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.
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!
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.
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:
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
.IGoap
(i ovanstående exempel är det Miner.cs
).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.