Planificarea acțiunilor orientate spre obiectiv (GOAP) este un sistem AI care va da cu ușurință opțiunilor agenților dvs. și instrumente pentru a lua decizii inteligente fără a trebui să mențină o mașină de stat finită și complexă.
În această demonstrație, există patru clase de caractere, fiecare folosind instrumente care se rup după ce a fost folosit pentru o perioadă de timp:
Fiecare clasă își va da seama automat, folosind planificarea de acțiune orientată spre scopuri, ce acțiuni au nevoie pentru a-și atinge obiectivele. În cazul în care instrumentul lor se rupe, ei vor merge la o grămadă de aprovizionare care are una realizată de fierar.
Planificarea acțiunilor orientate spre ambele părți este un sistem de inteligență artificială pentru agenții care le permite să planifice o secvență de acțiuni pentru a satisface un anumit scop. Secvența specială a acțiunilor depinde nu numai de obiectiv, ci și de starea actuală a lumii și a agentului. Aceasta înseamnă că, dacă același obiectiv este furnizat pentru diferiți agenți sau state lumii, puteți obține o succesiune complet diferită de acțiuni, ceea ce face AI mai dinamică și mai realistă. Să examinăm un exemplu, așa cum se vede în demo-ul de mai sus.
Avem un agent, un tocător de lemn, care ia busteni și le aruncă în lemn de foc. Elicopterul poate fi livrat cu scopul MakeFirewood
, și are acțiunile ChopLog
, GetAxe
, și CollectBranches
.
ChopLog
acțiunea va transforma un jurnal în lemn de foc, dar numai dacă tăietorul de lemn are un topor. GetAxe
acțiunea va da tăietorul de lemn un topor. În cele din urmă, CollectBranches
Acțiunea va produce și lemn de foc, fără a necesita un topor, dar lemnul de foc nu va avea o calitate ridicată.
Când îi dăm agentului MakeFirewood
obiectiv, obținem aceste două secvențe diferite de acțiune:
GetAxe
-> ChopLog
= face lemn de focCollectBranches
= face lemn de focDacă agentul poate obține un topor, atunci pot tăia un jurnal pentru a face lemn de foc. Dar poate că nu pot obține un topor; atunci, ei pot merge doar și colecta ramuri. Fiecare dintre aceste secvențe va îndeplini obiectivul MakeFirewood
.
GOAP poate alege cea mai bună secvență pe baza condițiilor prealabile disponibile. Dacă nu există nici un topor, atunci tăietorul trebuie să recurgă la strângerea ramurilor. Ridicarea ramurilor poate dura foarte mult timp și produce lemne de foc de proastă calitate, așa că nu vrem să funcționeze tot timpul, numai atunci când trebuie.
Sunteți, probabil, acum familiarizat cu utilajele finite de stat (FSM), dar dacă nu, atunci aruncați o privire la acest tutorial teribil.
S-ar putea să fi intrat în stări foarte mari și complexe pentru unii dintre agenții dvs. FSM, unde veți ajunge până la un moment în care nu doriți să adăugați comportamente noi, deoarece acestea provoacă prea multe efecte secundare și lacune în AI.
GOAP transformă acest lucru:
Declarația de stare a mașinii finite: conectată peste tot.În acest caz:
GOAP: frumos și ușor de gestionat.Prin decuplarea acțiunilor unii de la alții, putem acum să ne concentrăm asupra fiecărei acțiuni în mod individual. Acest lucru face codul modular, ușor de testat și de întreținut. Dacă doriți să adăugați o altă acțiune, puteți să-l plângeți și nu trebuie modificate alte acțiuni. Încearcă să faci asta cu un FSM!
De asemenea, puteți adăuga sau elimina acțiuni în mișcare pentru a schimba comportamentul unui agent pentru a le face și mai dinamice. Ai un căpcăun care a început brusc? Dați-le o nouă acțiune "atac de furie", care este eliminată atunci când se calmează. Pur și simplu adăugarea acțiunii la lista de acțiuni este tot ce trebuie să faceți; planificatorul GOAP va avea grijă de restul.
Dacă găsiți că aveți un FSM foarte complex pentru agenții dvs., atunci ar trebui să încercați GOAP. Un semn că FSM-ul devine prea complex este atunci când fiecare stat are o multitudine de declarații de tip if-else care testează în ce stare ar trebui să meargă la următoarea și adăugarea într-un nou stat te face să stârnești toate implicațiile pe care le-ar putea avea.
Dacă aveți un agent foarte simplu care îndeplinește doar una sau două sarcini, atunci GOAP ar putea fi un pic mai greu și un FSM va fi suficient. Cu toate acestea, merită să vă uitați la conceptele de aici și să vedeți dacă acestea ar fi destul de ușor pentru a vă conecta la agentul dvs..
Un acțiune este ceva pe care îl face agentul. De obicei, se joacă doar o animație și un sunet, și se schimbă un pic de stat (de exemplu, adăugând lemn de foc). Deschiderea unei uși este o acțiune diferită (și animație) decât ridicarea unui creion. O acțiune este încapsulată și nu trebuie să vă faceți griji cu privire la celelalte acțiuni.
Pentru a ajuta GOAP să determine ce acțiuni vrem să folosim, fiecare acțiune este dată a cost. O acțiune cu costuri ridicate nu va fi aleasă pentru o acțiune cu costuri mai mici. Când ordonăm acțiunile împreună, adăugăm costurile și apoi alegem secvența cu cel mai mic cost.
Permiteți alocarea unor costuri acțiunilor:
GetAxe
Cost: 2ChopLog
Cost: 4CollectBranches
Cost: 8Dacă ne uităm din nou la secvența de acțiuni și adăugăm costurile totale, vom vedea care este cea mai ieftină secvență:
GetAxe
(2) -> ChopLog
(4) = face lemn de foc(total: 6)CollectBranches
(8) = face lemn de foc(total: 8)Obținerea unui topor și tăierea unui buștean produce lemne de foc la un cost mai mic de 6, în timp ce colectarea ramurilor produce lemne la un cost mai mare de 8. Deci, agentul nostru alege să obțină o toporă și taie lemnul.
Dar nu va continua aceași secvență tot timpul? Nu dacă introducem precondiții...
Acțiunile au precondiții și efecte. O condiție preliminară este starea care este necesară pentru ca acțiunea să se execute, iar efectele sunt schimbarea stării după ce acțiunea a rulat.
De exemplu, ChopLog
acțiune necesită ca agentul să aibă un topor la îndemână. Dacă agentul nu are un topor, trebuie să găsească o altă acțiune care să poată îndeplini acea condiție prealabilă pentru a le permite ChopLog
acțiune de alergare. Din fericire, GetAxe
acțiunea face asta - acesta este efectul acțiunii.
Planificatorul GOAP este o bucată de cod care analizează precondițiile și efectele acțiunilor și creează cozi de acțiuni care vor îndeplini un obiectiv. Acest obiectiv este furnizat de agent, alături de un stat mondial și o listă de acțiuni pe care agentul o poate face. Cu ajutorul acestor informații, planificatorul GOAP poate ordona acțiunile, poate vedea ce pot rula și care nu pot, și apoi decide ce acțiuni sunt cele mai bune de realizat. Din fericire pentru tine, am scris acest cod, deci nu trebuie.
Pentru a seta acest lucru, permiteți adăugarea de precondiții și efecte la acțiunile elicopterului nostru:
GetAxe
Cost: 2. Precondiții: "un topor este disponibil", "nu are un topor". Efect: "are un topor".ChopLog
Cost: 4. Precondiții:"are un topor". Efect: "face lemn de foc"CollectBranches
Cost: 8. Precondiții: (niciuna). Efect: "face lemn de foc".Planificatorul GOAP are acum informațiile necesare pentru a ordona succesiunea de acțiuni pentru a face lemne de foc (scopul nostru).
Începem prin furnizarea planificatorului GOAP cu starea actuală a lumii și cu starea agentului. Acest stat mondial combinat este:
Privind acțiunile actuale disponibile, singura parte a statelor care sunt relevante pentru ele este "nu are o toporă" și "un topor este disponibil"; celălalt ar putea fi folosit pentru alți agenți cu alte acțiuni.
Bine, avem lumea noastră actuală, acțiunile noastre (cu precondițiile și efectele lor) și scopul. Să planificăm!
GOAL: "face lemn de foc" Statul actual: "nu are o topor", "un topor este disponibil" Poate actiona ChopLog? NU - necesită precondiție "are un topor" Nu o puteți folosi acum, încercați o altă acțiune. Poate funcționa GetAxe? DA, precondițiile "un topor este disponibil" și "nu are un topor" sunt adevărate. Acțiunea PUSH pe coadă, actualizați starea cu efectul acțiunii Statul nou "are un topor" Eliminați statul "un topor este disponibil" deoarece tocmai am luat unul. Poate funcționa acțiunea ChopLog? Da, precondiția "are un topor" este adevărat Acțiunea PUSH pe coadă, actualizați starea cu efectul acțiunii Statul nou "are un topor", "face lemne de foc" Am ajuns la scopul nostru de a face lemn de foc Secvență de acțiune: GetAxe -> ChopLog
Planificatorul va trece și prin celelalte acțiuni și nu se va opri doar când va găsi o soluție pentru obiectiv. Ce se întâmplă dacă o altă secvență are un cost mai mic? Va trece prin toate posibilitățile de a găsi cea mai bună soluție.
Când intenționează să construiască o copac. De fiecare dată când se aplică o acțiune, este scos din lista acțiunilor disponibile, deci nu avem un șir de 50 GetAxe
acțiuni de back-to-back. Statul se schimbă cu efectul acelei acțiuni.
Arborele pe care planificatorul îl construiește arată astfel:
Putem vedea că va găsi de fapt trei căi spre obiectiv cu costurile totale:
GetAxe
-> ChopLog
(total: 6)GetAxe
-> CollectBranches
(total: 10)CollectBranches
(total: 8)Cu toate ca GetAxe
-> CollectBranches
lucrări, calea cea mai ieftină este GetAxe
-> ChopLog
, așa că acesta este returnat.
Cum arată, de fapt, precondițiile și efectele în cod? Ei bine, este de până la tine, dar am găsit mai ușor să le păstrăm ca o pereche cheie-valoare, unde cheia este întotdeauna un String și valoarea este un obiect sau un tip primitiv (float, int, boolean sau similar). În C #, ar putea arăta astfel:
HashSet< KeyValuePair> condiții prealabile; HashSet< KeyValuePair > efecte;
Atunci când acțiunea se desfășoară, ce arată de fapt efectele și ce fac ei? Ei bine, ei nu trebuie să facă nimic - sunt într-adevăr folosiți pentru planificare și nu afectează starea reală a agentului până când nu se execută.
Este important să subliniem faptul că acțiunile de planificare nu sunt aceleași ca și desfășurarea acestora. Atunci când un agent efectuează GetAxe
acțiune, probabil că va fi aproape de o grămadă de unelte, va juca o animație îndoită și în jos și apoi va stoca un obiect ax în rucsac. Aceasta schimbă starea agentului. Dar, în timpul GOAP planificare, schimbarea de stat este doar temporară, astfel încât planificatorul să poată găsi soluția optimă.
Uneori, acțiunile trebuie să facă ceva mai mult pentru a determina dacă pot rula. De exemplu, GetAxe
acțiunea are condiția prealabilă a "unui topor este disponibil", care va trebui să caute în lume sau în imediata vecinătate, pentru a vedea dacă există un topor pe care agentul îl poate lua. S-ar putea determina că cel mai apropiat topor este prea departe sau în spatele liniilor inamice și va spune că nu se poate executa. Această condiție preliminară este procedurală și trebuie să ruleze un anumit cod; nu este un operator boolean simplu pe care îl putem schimba.
Evident, unele dintre aceste precondiții procedurale pot dura un timp pentru a alerga și ar trebui să fie efectuate pe altceva decât firul de randare, în mod ideal ca un fir de fundal sau ca Coroutines (în unitate).
De asemenea, puteți avea efecte procedurale, dacă doriți. Și dacă doriți să introduceți rezultate și mai dinamice, puteți modifica cost de acțiuni în zbor!
Sistemul nostru GOAP va trebui să trăiască într-o mică mașină de stat finită (FSM), singurul motiv că în multe jocuri acțiunile vor trebui să fie aproape de o țintă pentru a fi realizate. Încheiem cu trei stări:
Inactiv
MoveTo
Efectuați o acțiune
Când este inactiv, agentul își va da seama ce scop vrea să îndeplinească. Această parte este gestionată în afara GOAP; GOAP vă va spune exact ce acțiuni puteți executa pentru a îndeplini acest obiectiv. Atunci când se selectează un obiectiv, acesta este transmis Planificatorului GOAP, împreună cu statul de pornire a lumii și agentului, iar planificatorul va returna o listă de acțiuni (dacă poate îndeplini acest obiectiv).
Când planificatorul este terminat și agentul are lista cu acțiuni, acesta va încerca să efectueze prima acțiune. Toate acțiunile vor trebui să știe dacă trebuie să fie în intervalul de țintă. Dacă o fac, atunci FSM va împinge următoarea stare: MoveTo
.
MoveTo
statul va spune agentului că trebuie să treacă la o anumită țintă. Agentul va face mișcarea (și va juca animația pe jos), apoi va lăsa FSM să știe când se află în raza de acțiune a țintei. Această stare este apoi dezactivată și acțiunea poate funcționa.
Efectuați o acțiune
stat va executa următoarea acțiune în coada de acțiuni returnate de planificatorul GOAP. Acțiunea poate fi instantanee sau ultima în mai multe cadre, dar când se termină, aceasta se declanșează și apoi se efectuează următoarea acțiune (din nou, după ce se verifică dacă acțiunea următoare trebuie efectuată în interiorul unui obiect).
Toate acestea se repetă până când nu mai sunt făcute acțiuni, moment în care ne întoarcem la Inactiv
stat, obțineți un nou obiectiv și planificați din nou.
Este timpul să aruncați o privire la un exemplu real! Nu-ți face griji; nu este așa de complicat și am oferit o copie de lucru în Unity și C # pentru a încerca. Voi vorbi doar despre asta pentru scurt timp aici, pentru a obține o simț pentru arhitectură. Codul folosește unele din aceleași exemple de WoodChopper ca mai sus.
Dacă doriți să vă grăbiți, mergeți aici pentru codul: http://github.com/sploreg/goap
Avem patru muncitori:
Uneltele se uzează în timp și vor trebui înlocuite. Din fericire, Fierarul face instrumente. Dar minereul de fier este necesar pentru a produce unelte; acolo vine Minerul (care de asemenea are nevoie de unelte). Cutterul de lemn are nevoie de busteni, iar cei vin de la Logger; ambele au nevoie și de unelte.
Instrumentele și resursele sunt stocate pe coloanele de alimentare. Agenții vor colecta materialele sau instrumentele de care au nevoie din grămezi și, de asemenea, își vor lăsa produsul la ele.
Codul are șase clase principale GOAP:
GoapAgent
: înțelege starea și folosește FSM și GoapPlanner
să funcționeze.GoapAction
: acțiunile pe care agenții pot să le efectueze.GoapPlanner
: planifică acțiunile pentru GoapAgent
.FSM
: mașina finită de stat.FSMState
: o stare în FSM.IGoap
: interfața utilizată de realii noștri actori Laborer. Legături în evenimente pentru GOAP și FSM.Să ne uităm la GoapAction
clasa, deoarece aceasta este cea pe care o veți subclasa:
public abstract clasa GoapAction: MonoBehavior private HashSet> condiții prealabile; privat HashSet > efecte; bool privat inRange = false; / * Costul efectuării acțiunii. * Figurați o greutate care se potrivește acțiunii. * Schimbarea acesteia va afecta acțiunile selectate în timpul planificării. * / Costul public al flotorului = 1f; / ** * O acțiune trebuie adesea efectuată pe un obiect. Acesta este obiectul acela. Poate fi nul. * / target target public GameObject; public GoapAction () preconditions = nou HashSet > (); efecte = noul HashSet > (); void public doReset () inRange = false; target = null; resetați (); / ** * Resetați toate variabilele care trebuie resetate înainte ca planificarea să se întâmple din nou. * / void reset public (); / ** * Acționează acțiunea? * / bool abstract public isDone (); / ** * Verificați procedural dacă această acțiune poate fi executată. Nu toate acțiunile * vor avea nevoie de acest lucru, dar unele ar putea. * / declarație publică bool checkProcedură precondiționată (agent GameObject); / ** * Rulați acțiunea. * Returnă True dacă acțiunea a fost efectuată cu succes sau false * dacă sa întâmplat ceva și nu mai poate funcționa. În acest caz *, coada de acțiune trebuie să se elimine și obiectivul nu poate fi atins. * / executa abolirea publica (Agent GameObject); / ** * Această acțiune trebuie să fie în raza unui obiect de joc țintă? * Dacă nu, atunci starea moveTo nu va trebui să ruleze pentru această acțiune. * / bool abstract public necesităInRange (); / ** * Suntem în raza de țintă? * Starea MoveTo va seta aceasta și va fi resetată de fiecare dată când această acțiune este efectuată. * / bool public esteInRange () return inRange; void public setInRange (bool inRange) this.inRange = inRange; void public addPrecondition (cheie șir, valoare obiect) preconditions.Add (new KeyValuePair (valoare cheie) ); public void removePrecondition (cheie de șir) KeyValuePair remove = implicit (KeyValuePair ); foreach (KeyValuePair kvp în precondiții) if (kvp.Key.Equals (key)) remove = kvp; dacă (! implicit (KeyValuePair ) .Equals (eliminați)) precondiții.Remove (eliminați); void public addEffect (cheia de șir, valoarea obiectului) effects.Add (new KeyValuePair (valoare cheie) ); public void removeEffect (cheia de șir) KeyValuePair remove = implicit (KeyValuePair ); foreach (KeyValuePair kvp în efecte) if (kvp.Key.Equals (key)) remove = kvp; dacă (! implicit (KeyValuePair ) .Equals (eliminare)) efecte.Remove (elimina); HashSet public > Precondiții get return precondiții; HashSet public > Efecte get efecte returnate;
Nimic prea fantezist aici: stochează precondiții și efecte. De asemenea, știe dacă trebuie să fie în intervalul de țintă și, dacă da, atunci FSM știe să împingă MoveTo
atunci când este necesar. Știe și când se face; care este determinată de clasa de acțiune de implementare.
Iată una dintre următoarele acțiuni:
clasa publică MineOreAction: GoapAction private bool mined = false; privat IronRockComponent targetRock; // unde primim minereul din float privat startTime = 0; exploatarea plutitoare publicăDurația = 2; // secunde public MineOreAction () addPrecondition ("hasTool", true); // avem nevoie de un instrument pentru a face acest addPrecondition ("hasOre", false); // dacă avem ore, nu vrem mai mult addEffect ("hasOre", true); suprascrie public void reset () mined = false; targetRock = null; startTime = 0; boolean suprascrie public isDone () retur minat; suprascrie bool public necesităInRange () return true; // da trebuie să fim lângă o stâncă suprascrieți publicul bool checkProceduralPrecondition (agent GameObject) // găsiți cea mai apropiată stâncă pe care o putem mina IronRockComponent [] rocks = FindObjectsOfType (typeof (IronRockComponent)) ca IronRockComponent []; IronRockComponentul cel mai apropiat = null; float closestDist = 0; foreach (rocă IronRockComponent în roci) if (cel mai apropiat == null) // primul, deci alegeți-l acum pentru cel mai apropiat = rock; closestDist = (rock.gameObject.transform.position - agent.transform.position) .magnitude; altfel // este aceasta mai apropiată decât ultima? float dist = (ro.gameObject.transform.position - agent.transform.position) .magnitude; dacă (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) // finisate miniere BackpackComponent Backpack = (BackpackComponent) agent.GetComponent (typeof (BackpackComponent)); backpack.numOre + = 2; mined = true; Instrumentul ToolComponent = backpack.tool.GetComponent (typeof (ToolComponent)) ca ToolComponent; tool.use (0.5f); dacă (tool.destroyed ()) Destroy (backpack.tool); backpack.tool = null; return true;
Cea mai mare parte a acțiunii este checkProceduralPreconditions
metodă. Se caută cel mai apropiat obiect de joc cu un IronRockComponent
, și salvează această stâncă țintă. Apoi, când are loc, devine acea stâncă țintă salvată și va efectua acțiunea pe ea. Când acțiunea este reutilizată din nou în planificare, toate câmpurile sale sunt resetate, astfel încât acestea să poată fi calculate din nou.
Acestea sunt toate componentele care sunt adăugate la Miner
entitate obiect în unitate:
Pentru ca agentul dvs. să funcționeze, trebuie să adăugați următoarele componente:
GoapAgent
.IGoap
(în exemplul de mai sus, asta e Miner.cs
).Iată demo-ul în acțiune din nou:
Fiecare muncitor merge la ținta pe care trebuie să-și îndeplinească acțiunea (copac, rocă, bloc de tăiere sau orice altceva), efectuează acțiunea și se întoarce adesea la grămada de aprovizionare pentru a renunța la bunurile lor. Blacksmith va aștepta puțin până când nu există minereu de fier într-una dintre pilele de aprovizionare (adăugate de Miner). Blacksmith-ul se stinge apoi și face unelte și va lăsa sculele de la grămada de alimentare apropiată de el. Când se rupe unealta unui muncitor, se vor îndrepta spre grămada de aprovizionare din apropierea Blacksmithului, unde se găsesc instrumentele noi.
Puteți lua codul și aplicația completă aici: http://github.com/sploreg/goap.