O mașină cu stare finită este un model utilizat pentru a reprezenta și controla fluxul de execuție. Este perfect pentru implementarea AI în jocuri, producând rezultate excelente fără un cod complex. Acest tutorial descrie teoria, implementarea și utilizarea mașinilor simple și cu stack-based.
Toate icoanele făcute de Lorc și disponibile pe http://game-icons.net.
Notă: Deși acest tutorial este scris folosind AS3 și Flash, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.
O mașină cu stare finită sau FSM pe scurt este un model de calcul bazat pe o mașină ipotetică formată din una sau mai multe state. Numai o singură stare poate fi activă în același timp, așa că mașina trebuie să treacă de la o stare la alta pentru a efectua diferite acțiuni.
FSM-urile sunt utilizate în mod obișnuit pentru a organiza și reprezenta un flux de execuție, care este util pentru implementarea AI în jocuri. "Creierul" unui inamic, de exemplu, poate fi implementat folosind un FSM: fiecare stat reprezintă o acțiune, cum ar fi atac
sau se sustrage
:
Un FSM poate fi reprezentat de un grafic, unde nodurile sunt stările, iar marginile sunt tranzițiile. Fiecare margine are o etichetă care informează când trebuie să se întâmple trecerea, cum ar fi jucătorul este aproape
eticheta din figura de mai sus, care indică faptul că utilajul va trece de la umbla
la atac
dacă jucătorul este aproape.
Implementarea unui FSM începe cu statele și tranzițiile pe care le va avea. Imaginați-vă următoarele FSM, reprezentând creierul unui furnicu care duce acasă frunzele:
FSM reprezentând creierul unei furnici.Punctul de plecare este găsiți frunze
stat, care va rămâne activ până când furnica nu găsește frunza. Când se întâmplă acest lucru, se trece la starea actuală du-te acasă
, care rămâne activ până când furnica ajunge acasă. Atunci când furnica ajunge în sfârșit acasă, starea activă devine găsiți frunze
din nou, așa că furnica își repetă călătoria.
Dacă starea activă este găsiți frunze
și cursorul mouse-ului se apropie de furnică, există o tranziție la fugi
stat. În timp ce această stare este activă, furnica va fugi de cursorul mouse-ului. Atunci când cursorul nu mai este o amenințare, există o tranziție înapoi la găsiți frunze
stat.
Deoarece există legături de tranziție găsiți frunze
și fugi
, furnica va fugi mereu de cursorul mouse-ului când se apropie atâta timp cât furnica găsește frunza. Acea nu voi se întâmplă dacă starea activă este du-te acasă
(a se vedea figura de mai jos). În acest caz, furnica va merge acasă fără teamă, trecând doar la găsiți frunze
atunci când ajunge acasă.
fugi
și du-te acasă
. Un FSM poate fi implementat și încapsulat într-o singură clasă, numită FSM
de exemplu. Ideea este de a pune în aplicare fiecare stat ca o funcție sau o metodă, folosind o proprietate numită ActiveState
în clasă pentru a determina care stare este activă:
clasa publică FSM private var activeState: Function; // indică funcția curentă activă funcția publică FSM () funcția publică setState (stare: Funcție): void activeState = state; funcția publică (): void if (activeState! = null) activeState ();
Deoarece fiecare stare este o funcție, în timp ce o stare specifică este activă, funcția care reprezintă acea stare va fi invocată la fiecare actualizare de joc. ActiveState
proprietatea este un pointer la o funcție, deci va indica funcția statului activ.
Actualizați()
metodă a FSM
clasa trebuie invocată fiecărui cadru de joc, astfel încât să poată apela funcția indicată de ActiveState
proprietate. Apelul respectiv va actualiza acțiunile din starea activă curentă.
setState ()
metoda va trece FSM într-o stare nouă prin îndreptarea lui ActiveState
proprietății unei noi funcții de stat. Funcția de stat nu trebuie să fie membru al FSM; poate aparține unei alte clase, ceea ce face FSM
clase mai generice și reutilizabile.
Utilizarea FSM
clasa deja descrisă, este timpul să punem în aplicare "creierul" unui personaj. Antena explicată anterior va fi utilizată și controlată de un FSM. Următoarele reprezintă o reprezentare a statelor și a tranzițiilor, concentrându-se pe cod:
Antona este reprezentată de Furnică
clasă, care are o proprietate numită creier
și o metodă pentru fiecare stat. creier
proprietatea este o instanță a FSM
clasă:
clasa publica Ant public var pozitie: Vector3D; viteza publică var: Vector3D; creier public var: FSM; funcția publică Ant (posX: Număr, posY: Număr) position = new Vector3D (posX, posY); viteza = Vector3D nou (-1, -1); creier = FSM nou (); // Spuneți creierului să înceapă să caute frunza. brain.setState (findLeaf); / ** * Starea "findLeaf". * Aceasta face ca furnica să se deplaseze spre frunză. * / funcția publică findLeaf (): void / ** * Starea "goHome". * Aceasta face ca furnica să se deplaseze spre casă. * / funcția publică goHome (): void / ** * Starea "runAway". * Aceasta face ca furnica să fugă de cursorul mouse-ului. * / funcția publică runAway (): void update public function (): void // Actualizați FSM controlând "creierul". Se va invoca funcția curentă // active: findLeaf (), goHome () sau runAway (). brain.update (); // Aplicați vectorul vitezei în poziție, făcând mișcarea furnicii. moveBasedOnVelocity (); (...)
Furnică
clasa are, de asemenea, un viteză
și a poziţie
proprietate, ambele folosite pentru a calcula mișcarea utilizând integrarea Euler. Actualizați()
metoda se numește fiecare cadru de joc, așa că va actualiza FSM.
Pentru a păstra lucrurile simple, codul folosit pentru a deplasa furnica, cum ar fi moveBasedOnVelocity ()
, va fi omisă. Mai multe informații despre asta pot fi găsite în seria Înțelegerea comportamentelor de direcție.
Mai jos este implementarea fiecărui stat, începând cu findLeaf ()
, statul responsabil pentru ghidarea furnicii la poziția frunzelor:
funcția publică findLeaf (): void // Mișcați furnica spre frunză. velocitate = Vector3D nou (Game.instance.leaf.x - position.x, Game.instance.leaf.y - position.y); dacă (distanța (Game.instance.leaf, this) <= 10) // The ant is extremelly close to the leaf, it's time // to go home. brain.setState(goHome); if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) // Mouse cursor is threatening us. Let's run away! // It will make the brain start calling runAway() from // now on. brain.setState(runAway);
du-te acasă()
stat, folosit pentru a ghida acasă furnică:
funcția publică goHome (): void // Deplasarea furnicii spre viteza inițială = Vector3D nou (Game.instance.home.x - position.x, Game.instance.home.y - position.y); dacă (distanța (Game.instance.home, aceasta) <= 10) // The ant is home, let's find the leaf again. brain.setState(findLeaf);
În cele din urmă, fugi()
stat, folosit pentru a face furnica fuga cursorul mouse-ului:
funcția publică runAway (): void // Deplasați furnica departe de viteza cursorului mouse-ului = Vector3D nou (position.x - Game.mouse.x, position.y - Game.mouse.y); // Cursorul mouse-ului este încă aproape? dacă (distanță (Game.mouse, this)> MOUSE_THREAT_RADIUS) // Nu, cursorul mouse-ului a dispărut. Să mergem înapoi în căutarea frunzei. brain.setState (findLeaf);
Rezultatul este un furnică controlată de un "creier" al FSM:
Ant controlat de un FSM. Mutați cursorul mouse-ului pentru a amenința furnica.Imaginați-vă că și furnica trebuie să fugă de cursorul mouse-ului atunci când se întoarce acasă. FSM poate fi actualizat la următoarele:
Ant FSM actualizat cu noi tranziții.Se pare o modificare trivială, adăugarea unei noi tranziții, dar creează o problemă: dacă starea actuală este fugi
și cursorul mouse-ului nu mai este aproape, ce stare ar trebui să treacă trandafirul la: du-te acasă
sau găsiți frunze
?
Soluția pentru această problemă este a stack-based FSM. Spre deosebire de FSM existente, FSM bazat pe stack folosește o stivă pentru a controla stările. Partea de sus a stiva conține starea activă; tranzițiile sunt tratate prin împingerea sau stingerea stărilor din stivă:
Stack-based FSMStarea activă actuală poate decide ce trebuie să facă în timpul unei tranziții:
Tranziții într-un FSM bazat pe stack: pop în sine + împingere nouă; pop în sine; împingeți nou.Se poate popă din stivă și împingeți o altă stare, ceea ce înseamnă o tranziție completă (la fel ca și FSM-ul simplu). Poate apărea din stack, ceea ce înseamnă că starea curentă este completă și că starea următoare din stack ar trebui să devină activă. În cele din urmă, poate împinge doar o nouă stare, ceea ce înseamnă că starea activă actuală se va schimba pentru un timp, dar când se va dezvălui din stack, starea activă anterior va prelua din nou.
Un FSM bazat pe stack poate fi implementat folosind aceeași abordare ca înainte, dar de această dată folosind o serie de pointeri funcționali pentru a controla stiva. ActiveState
proprietatea nu mai este necesară, deoarece vârful stiva indică deja starea activă curentă:
clasa publică StackFSM private var stack: Array; funcția publică StackFSM () this.stack = array nou (); funcția publică funcțională (): void var currentStateFunction: Function = getCurrentState (); dacă (currentStateFunction! = null) currentStateFunction (); funcția publică popState (): Funcția return stack.pop (); functie publica pushState (stare: Function): void if (getCurrentState ()! = state) stack.push (state); funcția publică getCurrentState (): Funcția return stack.length> 0? stivă [stack.length - 1]: null;
setState ()
metoda a fost înlocuită cu două metode noi: pushState ()
și popstate ()
; pushState ()
adaugă o nouă stare în partea de sus a stivei, în timp ce popstate ()
elimină starea din partea de sus a stivei. Ambele metode transformă automat aparatul într-o stare nouă, deoarece schimba partea superioară a stivei.
Atunci când utilizați un FSM bazat pe stack, este important să rețineți că fiecare stat este responsabil pentru scoaterea din stivă. De obicei, o stare se elimină din stivă când nu mai este necesară, ca de exemplu atac()
este activ, dar țintă a murit.
Folosind exemplul furnică, sunt necesare doar câteva modificări pentru a adapta codul pentru a folosi FSM bazat pe stiva. Problema lipsei cunoașterii statului de tranziție este acum rezolvată perfect datorită naturii FSM bazate pe stack-uri:
clasa publică Ant (...) public var brain: StackFSM; funcția publică Ant (posX: Număr, posY: Număr) (...) creier = nou StackFSM (); // Spuneți creierului să înceapă să caute frunza. brain.pushState (findLeaf); (...) / ** * Starea "findLeaf". * Aceasta face ca furnica să se deplaseze spre frunză. * / funcția publică findLeaf (): void // Mișcați furnica spre frunză. velocitate = Vector3D nou (Game.instance.leaf.x - position.x, Game.instance.leaf.y - position.y); dacă (distanța (Game.instance.leaf, this) <= 10) // The ant is extremelly close to the leaf, it's time // to go home. brain.popState(); // removes "findLeaf" from the stack. brain.pushState(goHome); // push "goHome" state, making it the active state. if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) // Mouse cursor is threatening us. Let's run away! // The "runAway" state is pushed on top of "findLeaf", which means // the "findLeaf" state will be active again when "runAway" ends. brain.pushState(runAway); /** * The "goHome" state. * It makes the ant move towards its home. */ public function goHome() :void // Move the ant towards home velocity = new Vector3D(Game.instance.home.x - position.x, Game.instance.home.y - position.y); if (distance(Game.instance.home, this) <= 10) // The ant is home, let's find the leaf again. brain.popState(); // removes "goHome" from the stack. brain.pushState(findLeaf); // push "findLeaf" state, making it the active state if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) // Mouse cursor is threatening us. Let's run away! // The "runAway" state is pushed on top of "goHome", which means // the "goHome" state will be active again when "runAway" ends. brain.pushState(runAway); /** * The "runAway" state. * It makes the ant run away from the mouse cursor. */ public function runAway() :void // Move the ant away from the mouse cursor velocity = new Vector3D(position.x - Game.mouse.x, position.y - Game.mouse.y); // Is the mouse cursor still close? if (distance(Game.mouse, this) > MOUSE_THREAT_RADIUS) // Nu, cursorul mouse-ului a dispărut. Să revenim la starea activă anterior //. brain.popState (); (...)
Rezultatul este un furnică capabilă să fugă de cursorul mouse-ului, trecând înapoi la starea activă anterior înainte de amenințare:
Ant controlat de FSM pe bază de stive. Mutați cursorul mouse-ului pentru a amenința furnica.Mașinile de stat finite sunt utile pentru implementarea logicii AI în jocuri. Ele pot fi reprezentate cu ușurință folosind un grafic, care permite unui dezvoltator să vadă imaginea de ansamblu, ajustând și optimizând rezultatul final.
Implementarea unui FSM utilizând funcții sau metode pentru a reprezenta statele este simplă, dar puternică. Chiar și rezultate mai complexe pot fi obținute folosind un FSM bazat pe stack, care asigură un flux de execuție gestionabil și concis, fără a afecta negativ codul. E timpul să-i faci pe toți dușmanii tăi de joc mai inteligenți folosind un FSM!