Doelgerichte actie Planning voor een slimmere AI

Doelgeoriënteerde actieplanning (GOAP) is een AI-systeem dat uw agenten gemakkelijk keuzes en tools geeft om slimme beslissingen te nemen zonder een grote en complexe eindige toestandsmachine te hoeven onderhouden.

Bekijk de demo

In deze demo zijn er vier tekenklassen, elk met tools die breken nadat ze een tijdje zijn gebruikt:

  • Mijnwerker: Mijnen op rotsen. Heeft een tool nodig om te werken.
  • Logger: hakt bomen om logs te produceren. Heeft een tool nodig om te werken.
  • Wood Cutter: snijdt bomen in bruikbaar hout. Heeft een tool nodig om te werken.
  • Smid: smeedt gereedschappen bij de smidse. Iedereen gebruikt deze hulpmiddelen.

Elke klas komt automatisch tot ontwikkeling, met behulp van doelgerichte actieplanning, welke acties ze moeten uitvoeren om hun doelen te bereiken. Als hun gereedschap breekt, gaan ze naar een voorraadstapel die door de smid is gemaakt.

Wat is GOAP?

Doelgerichte actieplanning is een artificieel intelligentiesysteem voor agenten dat hen in staat stelt een reeks acties te plannen om aan een bepaald doel te voldoen. De specifieke volgorde van acties hangt niet alleen af ​​van het doel, maar ook van de huidige toestand van de wereld en de agent. Dit betekent dat als hetzelfde doel wordt geleverd voor verschillende agenten of wereldstaten, u een geheel andere reeks acties kunt krijgen. Dit maakt de AI dynamischer en realistischer. Laten we eens kijken naar een voorbeeld, zoals te zien in de demo hierboven.

We hebben een agent, een houthakker, die boomstammen neemt en ze in brandhout hakt. De helikopter kan worden geleverd met het doel MakeFirewood, en heeft de acties ChopLog, GetAxe, en CollectBranches.

De ChopLog actie zal een stamhout in brandhout veranderen, maar alleen als de houtsnijder een bijl heeft. De GetAxe actie zal de houtsnijder een bijl geven. eindelijk, de CollectBranches actie zal ook brandhout produceren, zonder dat een bijl nodig is, maar het brandhout zal niet zo hoog in kwaliteit zijn.

Wanneer we de agent het MakeFirewood doel, we krijgen deze twee verschillende actiereeksen:

  • Heeft brandhout nodig -> GetAxe -> ChopLog = maakt brandhout
  • Heeft brandhout nodig -> CollectBranches = maakt brandhout

Als de agent een bijl kan krijgen, kunnen ze een boomstam hakken om brandhout te maken. Maar misschien kunnen ze geen bijl krijgen; dan kunnen ze gewoon takken gaan halen. Elk van deze sequenties zal het doel van MakeFirewood

GOAP kan de beste volgorde kiezen op basis van welke randvoorwaarden beschikbaar zijn. Als er geen bijl bij de hand is, moet de houthakker zijn toevlucht nemen tot het oppakken van takken. Het oppakken van takken kan heel lang duren en brandhout van slechte kwaliteit opleveren, dus we willen niet dat het altijd draait, alleen als het moet.

Voor wie is GOAP

U bent waarschijnlijk inmiddels bekend met Finite State Machines (FSM), maar als dat niet het geval is, bekijk dan deze geweldige tutorial. 

Misschien ben je voor sommige van je FSM-agenten tegen zeer grote en complexe staten aangelopen, waar je uiteindelijk een punt bereikt waarop je geen nieuw gedrag wilt toevoegen omdat ze te veel bijwerkingen en hiaten in de AI veroorzaken.

GOAP verandert dit:

Finite State Machine stelt: overal verbonden.

In dit:

GOAP: leuk en beheersbaar.


Door de acties van elkaar te ontkoppelen, kunnen we ons nu concentreren op elke actie afzonderlijk. Dit maakt de code modulair en eenvoudig te testen en te onderhouden. Als u een andere actie wilt toevoegen, kunt u deze gewoon inpakken en mogen geen andere acties worden gewijzigd. Probeer dat te doen met een FSM!

U kunt ook acties on the fly toevoegen of verwijderen om het gedrag van een agent te wijzigen om ze nog dynamischer te maken. Heb je een boeman die plotseling begon te razen? Geef ze een nieuwe "woedeaanval" -actie die wordt verwijderd wanneer ze kalmeren. Eenvoudigweg het toevoegen van de actie aan de lijst met acties is alles wat u hoeft te doen; de GOAP-planner zorgt voor de rest.

Als je merkt dat je een erg complexe FSM hebt voor je agenten, dan moet je GOAP een kans geven. Eén teken dat je FSM te ingewikkeld wordt, is wanneer elke staat een groot aantal if-else-statements heeft die testen in welke staat ze naar de volgende moeten gaan, en het toevoegen van een nieuwe staat maakt dat je kreunt over alle implicaties die het kan hebben.

Als je een erg eenvoudige agent hebt die slechts één of twee taken uitvoert, is GOAP mogelijk een beetje hardhandig en is een FSM voldoende. Het is echter de moeite waard om hier naar de concepten te kijken en te zien of ze eenvoudig genoeg zijn om in te pluggen in uw agent.

acties

Een actie is iets dat de agent doet. Meestal is het gewoon het spelen van een animatie en een geluid, en het veranderen van een klein beetje status (bijvoorbeeld het toevoegen van brandhout). Een deur openen is een andere actie (en animatie) dan een potlood pakken. Een actie is ingekapseld en hoeft zich geen zorgen te maken over wat de andere acties zijn.

Om GOAP te helpen bepalen welke acties we willen gebruiken, krijgt elke actie een kosten. Een actie met hoge kosten zal niet worden gekozen voor acties met lagere kosten. Wanneer we de acties samenvoegen, tellen we de kosten bij elkaar en kiezen vervolgens de reeks met de laagste kosten.

Laten we wat kosten toewijzen aan de acties:

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

Als we de volgorde van acties opnieuw bekijken en de totale kosten optellen, zullen we zien wat de goedkoopste volgorde is:

  • Heeft brandhout nodig -> GetAxe (2) -> ChopLog(4) = maakt brandhout(totaal: 6)
  • Heeft brandhout nodig -> CollectBranches(8) = maakt brandhout(totaal: 8)

Een bijl krijgen en een boomstammen hakken levert brandhout tegen lagere kosten van 6, terwijl het verzamelen van de takken hout oplevert tegen de hogere kosten van 8. Onze agent kiest er dus voor een bijl te maken en hout te hakken.

Maar zal niet dezelfde reeks de hele tijd worden uitgevoerd? Niet als we introduceren randvoorwaarden...

Randvoorwaarden en effecten

Acties hebben randvoorwaarden en bijwerkingen. Een voorwaarde is de status die nodig is om de actie uit te voeren en de effecten zijn de wijziging in de status nadat de actie is uitgevoerd.

Bijvoorbeeld de ChopLog actie vereist dat de agent een bijl bij de hand heeft. Als de agent geen bijl heeft, moet hij een andere actie vinden die aan die voorwaarde kan voldoen om de ChopLog actie uitgevoerd. Gelukkig, de GetAxe actie doet dat - dit is het effect van de actie.

De GOAP-planner

De GOAP-planner is een stuk code dat kijkt naar de randvoorwaarden en effecten van acties en creëert wachtrijen voor acties die een doel zullen bereiken. Dat doel wordt geleverd door de agent, samen met een wereldstaat en een lijst met acties die de agent kan uitvoeren. Met deze informatie kan de GOAP-planner de acties bestellen, bekijken welke kunnen worden uitgevoerd en welke niet, en vervolgens beslissen welke acties het beste zijn om uit te voeren. Gelukkig voor jou, heb ik deze code geschreven, dus je hoeft het niet te doen.

Om dit in te stellen, kunnen we voorwaarden en effecten toevoegen aan de acties van onze houthakker:

  • GetAxe Kosten: 2. Precondities: "een bijl is beschikbaar", "heeft geen bijl". Effect: "heeft een bijl".
  • ChopLog Kosten: 4. Randvoorwaarden:"heeft een bijl". Effect: "brandhout maken"
  • CollectBranches Kosten: 8. Randvoorwaarden: (geen). Effect: "brandhout maken".

De GOAP-planner heeft nu de informatie die nodig is om de volgorde van acties voor het maken van brandhout te bestellen (ons doel). 

We beginnen met het leveren van de GOAP Planner aan de huidige toestand van de wereld en de status van de agent. Deze gecombineerde wereldstaat is:

  • "heeft geen bijl"
  • "een bijl is beschikbaar"
  • "de zon schijnt"

Als we naar onze huidige beschikbare acties kijken, is het enige deel van de staten dat voor hen relevant is, de status 'heeft geen bijl' en de status 'een bijl is beschikbaar'; de andere kan worden gebruikt voor andere agenten met andere acties.

Oké, we hebben onze huidige wereldstaat, onze acties (met hun randvoorwaarden en effecten) en het doel. Laten we plannen!

DOEL: "brandhout maken" Huidige status: "heeft geen bijl", "een bijl is beschikbaar" Kan actie ChopLog worden uitgevoerd? NEE - vereist voorwaarde "heeft een bijl" Kan het nu niet gebruiken, probeer een andere actie. Kan actie GetAxe worden uitgevoerd? JA, de voorwaarden "een bijl is beschikbaar" en "heeft geen bijl" zijn waar. PUSH-actie in de wachtrij, update status met effect van actie Nieuwe staat "heeft een bijl" Verwijder de status "een bijl is beschikbaar" omdat we er net een hebben genomen. Kan actie ChopLog worden uitgevoerd? JA, preconditie "heeft een bijl" is waar DRUK actie in wachtrij, update status met effect actie Nieuwe staat "heeft een bijl", "maakt brandhout" We hebben ons DOEL bereikt van "maakt brandhout" Volgorde: GetAxe -> ChopLog

De planner zal ook de andere acties doorlopen en hij stopt niet alleen wanneer hij een oplossing voor het doel vindt. Wat als een andere reeks goedkoper is? Het zal alle mogelijkheden doorlopen om de beste oplossing te vinden.

Wanneer het van plan is, bouwt het een op boom. Telkens wanneer een actie wordt toegepast, wordt deze uit de lijst met beschikbare acties geschrapt, dus we hebben geen reeks van 50 GetAxe acties back-to-back. De toestand is veranderd met het effect van die actie.

De boom die de planner opbouwt ziet er als volgt uit:

We kunnen zien dat het daadwerkelijk drie paden naar het doel zal vinden met hun totale kosten:

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

Hoewel GetAxe -> CollectBranches werkt, het goedkoopste pad is GetAxe -> ChopLog, dus deze is terug.

Hoe zien randvoorwaarden en effecten er eigenlijk uit in code? Welnu, dat is aan jou, maar ik heb gemerkt dat het het gemakkelijkst is om ze op te slaan als een sleutel / waarde-paar, waarbij de sleutel altijd een String is en de waarde een object of primitief type is (float, int, Boolean of vergelijkbaar). In C # zou dat er als volgt kunnen uitzien:

HashSet< KeyValuePair > randvoorwaarden; HashSet< KeyValuePair > effecten;

Wanneer de actie presteert, hoe zien deze effecten er eigenlijk uit en wat doen ze? Welnu, ze hoeven niets te doen - ze worden eigenlijk alleen maar gebruikt voor planning en hebben geen invloed op de status van de echte agent totdat ze echt rennen. 

Dit is de moeite waard om te benadrukken: planningsacties zijn niet hetzelfde als ze uitvoeren. Wanneer een agent de uitvoert GetAxe actie, het zal waarschijnlijk in de buurt van een stapel gereedschappen zijn, een bend-down-en-pick-up animatie spelen en dan een bijl-object opslaan in zijn rugzak. Dit verandert de status van de agent. Maar tijdens GOAP planning, de statuswijziging is slechts tijdelijk, zodat de planner de optimale oplossing kan vinden.

Procedurele randvoorwaarden

Soms moeten acties iets meer doen om te bepalen of ze kunnen uitvoeren. Bijvoorbeeld, de GetAxe actie heeft de voorwaarde dat 'er een bijl beschikbaar is' die de wereld of de directe omgeving moet doorzoeken om te zien of er een bijl is die de agent kan nemen. Het zou kunnen bepalen dat de dichtstbijzijnde bijl net te ver weg is of achter de vijandelijke linies staat en zal zeggen dat hij niet kan rennen. Deze voorwaarde is procedureel en moet wat code uitvoeren; het is geen eenvoudige Booleaanse operator die we gewoon kunnen schakelen.

Het is duidelijk dat sommige van deze procedurele randvoorwaarden enige tijd in beslag kunnen nemen en uitgevoerd moeten worden op iets anders dan de renderdraad, idealiter als een achtergrondthread of als Coroutines (in Unity).

Je zou ook procedurele effecten kunnen hebben, als je dat zou willen. En als u nog meer dynamische resultaten wilt introduceren, kunt u de kosten van acties tijdens de vlucht!

GOAP en State

Ons GOAP-systeem moet in een kleine Finite State Machine (FSM) leven, alleen omdat in veel games acties dicht bij een doel moeten staan ​​om te kunnen presteren. We eindigen met drie staten:

  • nutteloos
  • MoveTo
  • Actie ondernemen

Als de agent inactief is, zal hij uitvinden welk doel hij wil bereiken. Dit deel wordt afgehandeld buiten GOAP; GOAP vertelt u alleen welke acties u kunt uitvoeren om dat doel te bereiken. Wanneer een doel wordt gekozen, wordt het doorgegeven aan de GOAP Planner, samen met de wereld en de starterstatus van de agent, en de planner retourneert een lijst met acties (als het doel kan worden bereikt).

Wanneer de planner klaar is en de agent zijn lijst met acties heeft, probeert deze de eerste actie uit te voeren. Alle acties moeten weten of ze zich binnen het bereik van een doelwit bevinden. Als ze dat doen, zal de FSM de volgende status inschakelen: MoveTo.

De MoveTo staat zal de agent vertellen dat het naar een specifiek doel moet gaan. De agent doet het verplaatsen (en speelt de loopanimatie af) en laat de FSM weten wanneer deze zich binnen het bereik van het doelwit bevindt. Deze staat wordt vervolgens uitgeschakeld en de actie kan worden uitgevoerd.

De Actie ondernemen state voert de volgende actie uit in de wachtrij van acties geretourneerd door de GOAP Planner. De actie kan onmiddellijk of over meerdere frames duren, maar wanneer deze is voltooid, wordt deze verwijderd en vervolgens wordt de volgende actie uitgevoerd (opnieuw, na te hebben gecontroleerd of de volgende actie moet worden uitgevoerd binnen het bereik van een object).

Dit alles herhaalt zich totdat er geen acties meer zijn om uit te voeren, en op dat moment gaan we terug naar de nutteloos staat, een nieuw doel krijgen en opnieuw plannen.

Een voorbeeld van een echte code

Het is tijd om een ​​echt voorbeeld te bekijken! Maak je geen zorgen; het is niet zo ingewikkeld, en ik heb een werkkopie in Unity en C # voor je uitgestald om uit te proberen. Ik zal er hier kort even over praten zodat je een gevoel krijgt voor de architectuur. De code gebruikt enkele van dezelfde WoodChopper-voorbeelden als hierboven.

Als je meteen wilt graven, kun je hier de code vinden: http://github.com/sploreg/goap

We hebben vier arbeiders:

  • Smid: verandert ijzererts in gereedschappen.
  • Logger: gebruikt een hulpmiddel om bomen te kappen om logboeken te maken.
  • Mijnwerker: mijnen maken stenen met een gereedschap om ijzererts te produceren.
  • Houtsnijder: gebruikt een hulpmiddel om stammen te hakken om brandhout te produceren.

Gereedschap slijt na verloop van tijd en moet worden vervangen. Gelukkig maakt de smid gereedschap. Maar er is ijzererts nodig om gereedschappen te maken; dat is waar de mijnwerker komt (die ook gereedschap nodig heeft). De houtsnijder heeft boomstammen nodig en die komen van de logger; beide hebben ook gereedschap nodig.

Tools en middelen worden opgeslagen op bevoorradingspalen. De agenten verzamelen de materialen of gereedschappen die ze nodig hebben uit de stapels en brengen ook hun product naar hen toe.

De code heeft zes hoofd-GOAP-klassen:

  • GoapAgent: begrijpt status en gebruikt de FSM en GoapPlanner opereren.
  • GoapAction: acties die agenten kunnen uitvoeren.
  • GoapPlanner: plant de acties voor de GoapAgent.
  • FSM: de eindige toestandsmachine.
  • FSMState: een staat in de FSM.
  • IGoap: de interface die onze echte Laborer-acteurs gebruiken. Sluit aan bij evenementen voor GOAP en de FSM.

Laten we naar de GoapAction klasse, want dat is degene die je zult subclasseren:

openbare abstracte klasse GoapAction: MonoBehaviour private HashSet> randvoorwaarden; privé HashSet> effecten; private bool inRange = false; / * De kosten van het uitvoeren van de actie. * Zoek een gewicht uit dat bij de actie past. * Wijziging heeft invloed op welke acties worden gekozen tijdens de planning. * / Public float cost = 1f; / ** * Een actie moet vaak op een object worden uitgevoerd. Dit is dat object. Kan nul zijn. * / publiek doel GameObject; public GoapAction () preconditions = new HashSet> (); effecten = nieuwe hashset> ();  public void doReset () inRange = false; doel = null; reset ();  / ** * Reset alle variabelen die opnieuw moeten worden ingesteld voordat de planning opnieuw plaatsvindt. * / public abstract void reset (); / ** * Is de actie uitgevoerd? * / public abstract bool isDone (); / ** * Controleer tijdens het proces of deze actie kan worden uitgevoerd. Niet alle acties * hebben dit nodig, maar misschien wel. * / public abstract bool checkProceduralPrecondition (GameObject-agent); / ** * Voer de actie uit. * Retourneert True als de actie met succes of met false * is uitgevoerd als er iets is gebeurd en het niet langer kan worden uitgevoerd. In dit geval * moet de actiewachtrij worden gewist en het doel niet worden bereikt. * / public abstract bool perform (GameObject-agent); / ** * Moet deze actie zich binnen het bereik van een doelgame-object bevinden? * Zo niet, dan hoeft de moveTo-status niet voor deze actie te worden uitgevoerd. * / public abstract bool requiresInRange (); / ** * Zijn we binnen bereik van het doelwit? * De MoveTo-status stelt dit in en het wordt gereset telkens wanneer deze actie wordt uitgevoerd. * / public bool isInRange () return inRange;  public void setInRange (bool inRange) this.inRange = inRange;  public void addPrecondition (string key, object value) preconditions.Add (new KeyValuePair(sleutel waarde) );  public void removePrecondition (string key) KeyValuePair remove = default (KeyValuePair); foreach (KeyValuePair kvp in preconditions) if (kvp.Key.Equals (key)) remove = kvp;  if (! default (KeyValuePair) .Equals (remove)) preconditions.Remove (remove);  public void addEffect (string key, object value) effects.Add (new KeyValuePair(sleutel waarde) );  public void removeEffect (string key) KeyValuePair remove = default (KeyValuePair); foreach (KeyValuePair kvp in effecten) if (kvp.Key.Equals (key)) remove = kvp;  if (! default (KeyValuePair) .Equals (verwijderen)) effecten. Verwijderen (verwijderen);  openbare HashSet> Randvoorwaarden krijg retourvoorwaarde voorwaarden;  openbare HashSet> Effecten krijg returneffecten; 

Niets bijzonders hier: het slaat voorwaarden en effecten op. Het weet ook of het zich binnen het bereik van een doelwit moet bevinden en, zo ja, dan weet de FSM het MoveTo aangeven wanneer nodig. Het weet ook wanneer het klaar is; dat wordt bepaald door de klasse van de uitvoeringsactie.

Hier is een van de acties:

public class MineOreAction: GoapAction private bool mined = false; privé IronRockComponent targetRock; // waar we het erts krijgen van private float startTime = 0; public float miningDuration = 2; // seconden public MineOreAction () addPrecondition ("hasTool", true); // we hebben een tool nodig om deze addPrecondition te doen ("hasOre", false); // als we erts hebben willen we niet meer addEffect ("hasOre", true);  openbare opheffing ongeldige reset () mined = false; targetRock = null; startTime = 0;  public override bool isDone () return mined;  public override bool requiresInRange () return true; // ja we moeten in de buurt van een rots zijn public override bool checkProceduralPrecondition (GameObject agent) // vind de dichtstbijzijnde steen die we kunnen smelten IronRockComponent [] rocks = FindObjectsOfType (typeof (IronRockComponent)) als IronRockComponent []; IronRockComponent closest = null; float closestDist = 0; foreach (IronRockComponent rock in rocks) if (nearest == null) // first one, dus kies het voor now closest = rock; closestDist = (rock.gameObject.transform.position - agent.transform.position) .magnitude;  else // is deze dichterbij dan de vorige? float dist = (rock.gameObject.transform.position - agent.transform.position) .magnitude; als (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) // klaar met mijnbouwrugzak Rugzak rugzak = (BackpackComponent) agent.GetComponent (typeof (BackpackComponent)); backpack.numOre + = 2; mined = true; ToolComponent tool = backpack.tool.GetComponent (typeof (ToolComponent)) als ToolComponent; tool.use (0.5f); if (tool.destroyed ()) Destroy (backpack.tool); backpack.tool = null;  return true; 

Het grootste deel van de actie is de checkProceduralPreconditions methode. Het zoekt naar het dichtstbijzijnde spelobject met een IronRockComponent, en bewaart deze doelwitsteen. Als het dan presteert, krijgt het opgeslagen doelwit rock en voert het de actie daarop uit. Wanneer de actie opnieuw wordt gebruikt in de planning, worden alle velden opnieuw ingesteld, zodat ze opnieuw kunnen worden berekend.

Dit zijn allemaal componenten die worden toegevoegd aan de Mijnwerker entiteit object in Unity:


Om ervoor te zorgen dat uw agent werkt, moet u de volgende componenten eraan toevoegen:

  • GoapAgent.
  • Een klasse die implementeert IGoap (in het bovenstaande voorbeeld is dat Miner.cs).
  • Sommige acties.
  • Een rugzak (alleen omdat de acties het gebruiken, het is niet gerelateerd aan GOAP).
U kunt de gewenste acties toevoegen, en dit zou de manier waarop de agent zich gedraagt, veranderen. Je zou het zelfs alle acties kunnen geven, zodat het erts kan mijnen, gereedschappen kan maken en hout kan hakken.

Hier is de demo weer actief:

Elke arbeider gaat naar het doelwit dat ze nodig hebben om hun actie te vervullen (boom, rots, hakblok, of wat dan ook), voert de actie uit en keert vaak terug naar de voorraadstapel om hun goederen af ​​te zetten. De smid zal een tijdje wachten tot er ijzererts is in een van de bevoorradingspalen (toegevoegd door de mijnwerker). De smid gaat dan weg en maakt gereedschappen en zal de gereedschappen afzetten op de voorraadstapel die zich het dichtst bij hem bevindt. Wanneer het gereedschap van een arbeider breekt, gaan ze naar de voorraadstapel bij de smid waar de nieuwe gereedschappen zijn.

Je kunt de code en de volledige app hier downloaden: http://github.com/sploreg/goap.

Conclusie

Met GOAP kun je een grote reeks acties maken zonder de hoofdpijn van onderling verbonden staten die vaak wordt geleverd met een eindige toestandsmachine. Acties kunnen worden toegevoegd en verwijderd van een agent om dynamische resultaten te produceren, en om u gezond te houden bij het onderhouden van de code. Je zult eindigen met een flexibele, slimme en dynamische AI.