Mașinile de stat finit și comportamentele de direcție sunt potrivite perfect: natura lor dinamică permite combinarea unor stări simple și a forțelor pentru a crea modele complexe de comportament. În acest tutorial, veți afla cum să codificați a echipă model folosind o mașină de stat finit bazată pe stack, combinată cu comportamente de direcție.
Toate pictogramele FSM realizate de Lorc și disponibile pe http://game-icons.net. Active în demo: Top / Down Shoot 'Em Up Spritesheet de takomogames și Alien Breed (esque) Top-Down Tilesheet de SpicyPixel.
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.
După finalizarea acestui tutorial, veți putea pune în aplicare un model de echipă în care un grup de soldați vor urma liderul, vânând dușmani și jigging:
Tutorialul anterior despre masini finite a descris cat de utile sunt pentru implementarea logicii inteligentei artificiale: in loc sa scrie o gramada foarte complexa de cod AI, logica poate fi raspandita intr-un set de stari simple, fiecare realizand sarcini foarte specifice, cum ar fi fugind de un inamic.
Combinația de state are ca rezultat o AI sofisticată, dar ușor de înțeles, optimizată și menținută. Această structură este, de asemenea, unul dintre pilonii din spatele comportamentelor de direcție: combinația de forțe simple pentru a crea modele complexe.
De aceea, FSM-urile și comportamentele de direcție fac o combinație excelentă. Stările pot fi folosite pentru a controla care forțe vor acționa asupra unui personaj, îmbunătățind setul deja puternic de modele care pot fi create folosind comportamentele de direcție.
Pentru a organiza toate comportamentele, ele vor fi răspândite peste state. Fiecare stat va genera o forță de comportament specifică sau un set de ele, cum ar fi căutarea, fuga și evitarea coliziunilor.
Atunci când o anumită stare este activă, numai forța rezultată va fi aplicată caracterului, făcându-i să se comporte în mod corespunzător. De exemplu, dacă starea activă este în prezent fugi
și forțele sale sunt o combinație de fugă
și evitarea coliziunii
, personajul va fugi dintr-un loc, evitând orice obstacol.
Forțele de direcție se calculează la fiecare actualizare de joc și apoi se adaugă la vectorul vitezei caracterului. În consecință, atunci când se schimbă starea activă (și cu ea modelul de mișcare), caracterul va trece cu ușurință la noul tip, pe măsură ce noile forțe vor fi adăugate după fiecare actualizare.
Caracterul dinamic al comportamentelor de direcție asigură această tranziție a fluidului; statele doar coordonează forțele de direcție active la un moment dat.
Structura de implementare a unui model de echipă va încapsula FSM-urile și comportamentele de direcție în proprietățile unei clase. Orice clasă care reprezintă o entitate care se mișcă sau este influențată în alt mod de forțele de conducere va avea o proprietate numită boid
, care este un exemplu al Boid
clasă:
clasa publică Boid public var position: Vector3D; viteza publică var: Vector3D; public var steering: Vector3D; public var mass: Număr; funcția publică funcția (Vector3D): Vector3D (...) funcția publică funcțională (poziția: Vector3D): Vector3D (...) )
Boid
clasa a fost utilizată în seria comportamentului de direcție și oferă proprietăți ca viteză
și poziţie
(ambele vectori de matematică), împreună cu metode de adăugare a forțelor de direcție, cum ar fi căuta()
, fugă ()
, etc.
O entitate care folosește FSM pe bază de stive va avea aceeași structură cu Furnică
clasa din tutorialul FSM anterior: FSM bazat pe stack este gestionat de către creier
proprietate și fiecare stat este implementat ca o metodă.
Mai jos este Soldat
clasa, care are comportament de direcție și capabilități FSM:
clasa publica Soldier private var brain: StackFSM; // Controlează chestiunea FSM private var boid: Boid; // Controlează comportamentele de conducere funcția publică Soldier (posX: Număr, posY: Număr, totalMass: Număr) (...) brain = new StackFSM (); // Împingeți starea "urmați" astfel încât soldatul să urmeze liderul brain.pushState (urmați); update public function (): void // Actualizează creierul. Acesta va rula funcția de stare curentă. brain.update (); // Actualizați comportamentul de direcție boid.update ();
Modelul de echipă va fi implementat utilizând o mașină cu stack finite. Soldații, care sunt membri ai echipei, vor urma liderul (controlat de jucători), care va vâna pe toți inamicii din apropiere.
Când un dușman moare, ar putea să renunțe la un element care poate fi bun sau rău (a trusă medicală sau a badkit, respectiv). Un soldat va sparge formația de echipă și va aduna obiecte bune din apropiere sau va evita locul pentru a evita orice obiecte proaste.
Mai jos este o reprezentare grafică a SFM-ului bazat pe stive care controlează "creierul" soldatului:
Următoarele secțiuni prezintă implementarea fiecărui stat. Toate fragmentele de cod din acest tutorial descriu ideea principală a fiecărui pas, omisând toate specificațiile referitoare la motorul de joc folosit (Flixel, în acest caz).
Primul stat care urmează să fie pus în aplicare este cel care va rămâne activ aproape tot timpul: urmați liderul. Partea de jefuire va fi implementată mai târziu, deci pentru moment urma
statul va face doar soldatul să urmeze liderul, schimbând starea actuală în vânătoare
dacă există un inamic în apropiere:
funcția publică (): void var aLeader: Boid = Game.instance.boids [0]; // a obține un pointer la leader addSteeringForce (boid.followLeader (aLeader)); // urmați liderul // Există un monstru în apropiere? dacă (getNearestEnemy ()! = null) // Da, nu există! Vânătați-o! // Apăsați pe starea "vânătoare". Aceasta va face ca soldatul să nu mai urmeze liderul și / / să înceapă să vâneze monstrul. brain.pushState (vânătoare); funcția privată getNearestEnemy (): Monster // aici merge implementarea pentru a obține cel mai apropiat inamic
În ciuda prezenței dușmanilor, în timp ce statul este activ, va genera întotdeauna o forță de a urma liderul, folosind conducătorul după un comportament.
Dacă getNearestEnemy ()
întoarce ceva, înseamnă că există un inamic în jur. În acest caz, vânătoare
statul este împins în teancul prin apel brain.pushState (vanatoare)
, făcând soldatul să nu mai urmeze liderul și să înceapă vânătorii de vânătoare.
Pentru moment, punerea în aplicare a vânătoare()
statul poate doar să se poată stăpâni din stivă, astfel soldații nu vor fi blocați la statul de vânătoare:
funcția publică (): void // Deocamdată, hai să scoatem din creier statul de vânătoare (). brain.popState ();
Rețineți că nu sunt transmise informații vânătoare
stat, cum ar fi care este cel mai apropiat inamic. Aceste informații trebuie colectate de către vânătoare
stat, deoarece determină dacă vânătoare
ar trebui să rămână activ sau să se afișeze din stivă (revenind controlul la urma
stat).
Rezultatul până acum este o echipă de soldați care urmează liderul (rețineți că soldații nu vor vâna pentru că vânătoare()
metoda se descopera singura):
Bacsis: fiecare stat ar trebui să fie responsabil pentru încheierea existenței sale prin eliminarea din stivă.
Următorul stat care urmează să fie implementat este vânătoare
, care va face soldații să vâneze orice inamic din apropiere. Codul pentru vânătoare()
este:
funcția publică (): void var aNearestEnemy: Monster = getNearestEnemy (); // Avem un monstru în apropiere? dacă (aNearestEnemy! = null) // Da, o facem. Să calculăm cât de îndepărtat este. varDistance: Număr = calculDistance (aNearestEnemy, this); Este monstru suficient de aproape pentru a trage? dacă (aDistance <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState();
Statul începe prin atribuirea aNearestEnemy
cu cel mai apropiat inamic. Dacă aNearestEnemy
este nul
înseamnă că nu există nici un inamic în jur, deci statul trebuie să se încheie. Apelul brain.popState ()
afișează vânătoare
stat, schimbând soldatul în starea următoare în stack.
Dacă aNearestEnemy
nu este nul
, înseamnă că există un dușman care urmează să fie vânat și că statul ar trebui să rămână activ. Algoritmul de vânătoare se bazează pe distanța dintre soldat și inamic: dacă distanța este mai mare de 80, soldatul va căuta poziția inamicului; dacă distanța este mai mică de 80 de kilometri, soldatul se va confrunta cu inamicul și va trage în picioare în picioare.
De cand vânătoare()
va fi invocată fiecare actualizare de joc, în cazul în care un inamic este în jurul valorii de atunci soldatul va căuta sau trage inamicul. Decizia de a muta sau de a trage este controlată în mod dinamic de distanța dintre soldat și inamic.
Rezultatul este o echipă de soldați capabili să urmeze liderul și să vâneze dușmanii din apropiere:
De fiecare dată când un ucigaș este ucis, s-ar putea cădea un element. Soldatul trebuie să colecteze elementul dacă este unul bun sau să fugă din articol dacă este rău. Acest comportament este reprezentat de două stări în FSM descrise anterior:
collectItem
și fugi
statele. collectItem
stat va face un soldat ajunge la elementul abandonat, în timp ce fugi
statul îl va face pe soldat să fugă de locația elementului rău. Ambele state sunt aproape identice, singura diferență fiind sosirea sau forța de fugă:
funcția publică runAway (): void var aItem: Item = getNearestItem (); dacă aItem! = null && aItem.alive && aItem.type == Item.BADKIT) var aItemPos: Vector3D = nou Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.flee (aItemPos)); altceva brain.popState (); funcția publică collectItem (): void var aItem: Item = getNearestItem (); dacă aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = nou Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); altceva brain.popState (); funcția privată getNearestItem (): Elementul // merge codul pentru a obține cel mai apropiat element
Aici o optimizare despre tranziții vine la îndemână. Codul de trecere de la urma
de stat la collectItem
sau fugi
este același: verificați dacă există un element în apropiere, apoi împingeți noua stare.
Statul care trebuie împins depinde de tipul articolului. Ca urmare, trecerea la collectItem
sau fugi
pot fi implementate ca o singură metodă, numită checkItemsNearby ()
:
funcția privată checkItemsNearby (): void var aItem: Item = getNearestItem (); dacă (aItem! = null) brain.pushState (aItem.type == Item.BADKIT? runAway: collectItem);
Această metodă verifică cel mai apropiat element. Dacă este unul bun, collectItem
statul este împins în creier; dacă este unul rău, fugi
statul este împins. Dacă nu există niciun element de colectat, metoda nu face nimic.
Această optimizare permite utilizarea checkItemsNearby ()
pentru a controla trecerea de la orice stat la collectItem
sau fugi
. Potrivit soldatului FSM, această tranziție există în două state: urma
și vânătoare
.
Implementarea lor poate fi ușor modificată pentru a se adapta noii tranziții:
funcția publică (): void var aLeader: Boid = Game.instance.boids [0]; // a obține un pointer la leader addSteeringForce (boid.followLeader (aLeader)); // urmați liderul // Verificați dacă există un element pentru a colecta (sau a fugi de la) checkItemsNearby (); Există un monstru în apropiere? dacă (getNearestEnemy ()! = null) // Da, nu există! Vânătați-o! // Apăsați pe starea "vânătoare". Aceasta va face ca soldatul să nu mai urmeze liderul și / / să înceapă să vâneze monstrul. brain.pushState (vânătoare); hunt funcția publică (): void var aNearestEnemy: Monster = getNearestEnemy (); // Verificați dacă există un element pentru a colecta (sau a fugi de la) checkItemsNearby (); // Avem un monstru în apropiere? dacă (aNearestEnemy! = null) // Da, o facem. Să calculăm cât de îndepărtat este. varDistance: Număr = calculDistance (aNearestEnemy, this); Este monstru suficient de aproape pentru a trage? dacă (aDistance <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState();
În timp ce urmați liderul, un soldat va verifica elementele din apropiere. Atunci când vânează un inamic, un soldat va verifica, de asemenea, obiectele din apropiere.
Rezultatul este demo-ul de mai jos. Rețineți că un soldat va încerca să colecteze sau să se sustragă unui obiect oricând există unul în apropiere, chiar dacă există dușmani de vânătoare și liderul să urmeze.
Un aspect important privind statele și tranzițiile este prioritate printre ei. În funcție de linia în care se află o tranziție în cadrul implementării unui stat, se schimbă prioritatea acelei tranziții.
Utilizarea urma
stat și tranziția făcută de checkItemsNearby ()
ca exemplu, aruncăm o privire la următoarea implementare:
funcția publică (): void var aLeader: Boid = Game.instance.boids [0]; // a obține un pointer la leader addSteeringForce (boid.followLeader (aLeader)); // urmați liderul // Verificați dacă există un element pentru a colecta (sau a fugi de la) checkItemsNearby (); Există un monstru în apropiere? dacă (getNearestEnemy ()! = null) // Da, nu există! Vânătați-o! // Apăsați pe starea "vânătoare". Aceasta va face ca soldatul să nu mai urmeze liderul și / / să înceapă să vâneze monstrul. brain.pushState (vânătoare);
Această versiune a urma()
va face ca un soldat să treacă collectItem
sau fugi
inainte de verificând dacă există un inamic în jur. În consecință, soldatul va colecta (sau va fugi de la) un obiect chiar și atunci când există dușmani care ar trebui să fie vânate de către vânătoare
stat.
Iată o altă implementare:
funcția publică (): void var aLeader: Boid = Game.instance.boids [0]; // a obține un pointer la leader addSteeringForce (boid.followLeader (aLeader)); // urmați liderul // Există un monstru în apropiere? dacă (getNearestEnemy ()! = null) // Da, nu există! Vânătați-o! // Apăsați pe starea "vânătoare". Aceasta va face ca soldatul să nu mai urmeze liderul și / / să înceapă să vâneze monstrul. brain.pushState (vânătoare); altceva // Verificați dacă există un element care să colecteze (sau să fugă de) checkItemsNearby ();
Această versiune a urma()
va face ca un soldat să treacă collectItem
sau fugi
numai după el descoperă că nu există nici un dușman să omoare.
Actuala implementare a urma()
, vânătoare()
și collectItem ()
suferă de probleme prioritare. Soldatul va încerca să colecteze un element chiar și atunci când sunt lucruri mai importante de făcut. Pentru a rezolva asta, sunt necesare câteva modificări.
În ceea ce privește urma
stat, codul poate fi actualizat la:
(urmați () cu priorități)
funcția publică (): void var aLeader: Boid = Game.instance.boids [0]; // a obține un pointer la leader addSteeringForce (boid.followLeader (aLeader)); // urmați liderul // Există un monstru în apropiere? dacă (getNearestEnemy ()! = null) // Da, nu există! Vânătați-o! // Apăsați pe starea "vânătoare". Aceasta va face ca soldatul să nu mai urmeze liderul și / / să înceapă să vâneze monstrul. brain.pushState (vânătoare); altceva // Verificați dacă există un element care să colecteze (sau să fugă de) checkItemsNearby ();
vânătoare
statul trebuie schimbat la:
funcția publică (): void var aNearestEnemy: Monster = getNearestEnemy (); // Avem un monstru în apropiere? dacă (aNearestEnemy! = null) // Da, o facem. Să calculăm cât de îndepărtat este. varDistance: Număr = calculDistance (aNearestEnemy, this); Este monstru suficient de aproape pentru a trage? dacă (aDistance <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState(); // Check if there is an item to collect (or run away from) checkItemsNearby();
În cele din urmă, collectItem
statul trebuie să fie schimbat pentru a opri orice jefuire dacă există un inamic în jurul valorii de:
funcția publică collectItem (): void var aItem: Item = getNearestItem (); var aMonsterNormal: Boolean = getNearestEnemy ()! = null; dacă (! aMonsterNearby && aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = Vector3D () nou; aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); altceva brain.popState ();
Rezultatul tuturor acestor modificări este demo-ul de la începutul tutorialului:
În acest tutorial, ați învățat cum să codificați un model de echipă în cazul în care un grup de soldați vor urma un lider, vânând în jos și jefuind inamicii din apropiere. AI este implementat utilizând un FSM bazat pe stack, combinat cu mai multe comportamente de direcție.
După cum sa demonstrat, mașinile de stat finit și comportamentele de direcție sunt o combinație puternică și o potrivire excelentă. Răspândind logica asupra stărilor FSM, este posibil să selectați în mod dinamic forțele de direcție care vor acționa asupra unui personaj, permițând crearea unor modele complexe de AI.
Combinați comportamentele de direcție pe care le cunoașteți deja cu FSM-urile și creați modele noi și remarcabile!