Listă de acțiuni este o structură de date simplă utilă pentru multe sarcini diferite dintr-un motor de joc. S-ar putea argumenta că lista de acțiuni ar trebui să fie întotdeauna utilizată în locul unei anumite forme de mașină de stat.
Cea mai obișnuită formă (și cea mai simplă formă) a organizării comportamentului este a mașină de stat finită. De obicei implementate cu switch-uri sau matrice în C sau C ++, sau slews de dacă
și altfel
declarațiile în alte limbi, mașinile de stat sunt rigide și inflexibile. Lista de acțiuni este o schemă organizatorică mai puternică în sensul că modelează în mod clar cum se întâmplă de obicei lucrurile în realitate. Din acest motiv, lista de acțiuni este mai intuitivă și mai flexibilă decât o mașină de stat finită.
Lista de acțiuni este doar o schemă organizatorică a conceptului de a acțiune temporizată. Acțiunile sunt stocate într-o primă comandă (FIFO). Aceasta înseamnă că atunci când o acțiune este inserată într-o listă de acțiuni, ultima acțiune inserată în față va fi prima care va fi eliminată. Lista de acțiuni nu respectă în mod explicit formatul FIFO, dar în centrul ei acestea rămân aceleași.
Fiecare buclă de joc, lista de acțiuni este actualizată și fiecare acțiune din listă este actualizată în ordine. După terminarea unei acțiuni, este eliminată din listă.
Un acțiune este un fel de funcție pentru a apela care face un fel de muncă într-un fel. Iată câteva tipuri diferite de domenii și activitatea pe care acțiunile ar putea să le îndeplinească:
Lucrurile la nivel scăzut, cum ar fi găsirea căii sau flocul, nu sunt reprezentate efectiv cu o listă de acțiuni. Combaterea și alte zone extrem de specializate de joc specifice jocurilor sunt, de asemenea, lucruri pe care probabil că nu ar trebui să le implementați printr-o listă de acțiuni.
Iată o privire rapidă asupra a ceea ce ar trebui să se găsească în structura de date a listei de acțiuni. Rețineți că detaliile mai detaliate vor urma mai târziu în articol.
clasa ActionList public: void Update (float dt); void PushFront (acțiune * acțiune); void PushBack (acțiune * acțiune); void InsertBefore (acțiune * acțiune); void InsertAfter (acțiune * acțiune); Acțiune * Eliminați (acțiune * acțiune); Acțiune * Începeți (vid); Acțiune * Sfârșit (void); bool IsEmpty (void) const; flotare TimeLeft (void) const; bool IsBlocking (void) const; privat: durata flotorului; timpul de flotareRecundat; procent float; bool blocare; benzi nesemnate; Acțiuni ** acțiuni; // poate fi un vector sau o listă legată;
Este important să rețineți că stocarea efectivă a fiecărei acțiuni nu trebuie să fie o listă actuală legată - ceva asemănător C++ std :: vector
ar funcționa foarte bine. Preferința mea este să pun în comun toate acțiunile din cadrul unui alocator și al listelor de linkuri, împreună cu liste intrusiv legate. De obicei, liste de acțiuni sunt utilizate în zone mai puțin sensibile la performanță, astfel încât optimizarea orientată spre date va fi probabil inutilă atunci când se elaborează o structură de date a listei de acțiuni.
Cea mai mare parte a acestui shebang sunt acțiunile în sine. Fiecare acțiune ar trebui să fie complet autonomă, astfel încât lista de acțiuni să nu știe nimic despre interiorul acțiunii. Aceasta face lista de acțiuni un instrument extrem de flexibil. O listă de acțiuni nu va interesa dacă rulează acțiuni ale interfeței utilizator sau gestionează mișcările unui caracter 3D modelat.
O modalitate bună de a implementa acțiunile este printr-o interfață abstractă unică. Câteva funcții specifice sunt expuse din obiectul de acțiune în lista de acțiuni. Iată un exemplu care poate arăta o acțiune de bază:
clasă Acțiune public: actualizare virtuală (float dt); virtuale OnStart (void); virtuale OnEnd (void); bool este finalizat; bool isBlocking; benzi nesemnate; float scurs; durata flotării; privat: ActionList * ownerList; ;
OnStart ()
și Pe sfarsit()
funcțiile sunt integrate aici. Aceste două funcții trebuie să fie executate ori de câte ori o acțiune este introdusă într-o listă și când acțiunea se termină, respectiv. Aceste funcții permit ca acțiunile să fie în întregime autonome.
O extensie importantă a listei de acțiuni este capacitatea de a denumi acțiuni ca fiind una dintre ele blocarea și non-blocare. Distincția este simplă: o acțiune de blocare încheie rutina actualizării listei de acțiuni și nu se actualizează alte acțiuni; o acțiune fără blocare permite actualizarea acțiunii ulterioare.
O singură valoare booleană poate fi utilizată pentru a determina dacă o acțiune este blocată sau nu este blocată. Iată câteva psuedocode care demonstrează o listă de acțiuni Actualizați
rutină:
void ActionList :: Actualizare (float dt) int i = 0; în timp ce (i! = numActions) Acțiune * acțiune = acțiuni + i; acțiune-> actualizare (dt); dacă (acțiunea-> este Blocarea) pauză; dacă (acțiune-> este finalizată) action-> OnEnd (); action = this-> Remove (acțiune); ++ i;
Un bun exemplu de utilizare a acțiunilor non-blocante ar fi să permită ca anumite comportamente să fie difuzate în același timp. De exemplu, dacă avem o coadă de acțiuni pentru a alerga și a flutura mâinile, personajul care efectuează aceste acțiuni ar trebui să poată face simultan ambele. Dacă un inamic rulează de la caracter, ar fi foarte prost dacă ar trebui să alerge, apoi să se oprească și să-și dea mâinile frenez, apoi continuă să alerge.
După cum se dovedește, conceptul de blocare și acțiuni non-blocante intuitiv se potrivește cu cele mai multe tipuri de comportamente simple necesare pentru a fi implementate în cadrul unui joc.
Permiteți să acoperiți un exemplu despre cum va arăta o listă de acțiuni într-un scenariu real. Acest lucru vă va ajuta să dezvoltați o intuiție despre cum să utilizați o listă de acțiuni și de ce liste de acțiuni sunt utile.
Un dușman într-un joc simplu de sus în jos 2D trebuie să patruleze înainte și înapoi. Ori de câte ori acest inamic se află în raza de acțiune a jucătorului, trebuie să arunce o bombă spre jucator și să-i întrerupă patrula. Ar trebui să existe o mică cooldown după ce o bombă este aruncată acolo unde inamicul se oprește complet. Dacă jucătorul este încă în raza de acțiune, ar trebui aruncată o altă bomba urmată de o cooldown. Dacă jucătorul este în afara razei de acțiune, patrula ar trebui să continue exact acolo unde a rămas.
Fiecare bomba trebuie să plutească prin lumea 2D și să respecte legile fizicii bazate pe țiglă implementată în joc. Bomba așteaptă doar până când timerul de siguranțe se termină și apoi explodează. Explozia ar trebui să cuprindă o animație, un sunet și o eliminare a cutiei de ciocnire a bombei și a spritei vizuale.
Construirea unei mașini de stat pentru acest comportament va fi posibilă și nu prea tare, dar va dura ceva timp. Tranzițiile din fiecare stat trebuie să fie codificate manual, iar salvarea stărilor anterioare pentru a continua mai târziu ar putea provoca o durere de cap.
Din fericire, aceasta este o problemă ideală de rezolvat cu liste de acțiuni. Mai întâi, să vedem o listă de acțiuni goale. Această listă de acțiuni goale va reprezenta o listă de elemente "de făcut" pentru ca inamicul să se finalizeze; o listă goală indică un inamic inactiv.
Este important să ne gândim cum să "compartmentalizăm" comportamentul dorit în nuggeturi mici. Primul lucru pe care ar trebui să-l facem este să scăpăm comportamentele de patrulare. Să presupunem că dușmanul ar trebui să patruleze la stânga de la distanță, apoi să o patruleze la aceeași distanță și să repete.
Iată ce înseamnă patrulare stânga acțiune ar putea să arate ca:
clasa PatrolLeft: acțiune publică virtual Update (float dt) // Mută dușmanul stânga inamic-> position.MoveLeft (); // Timer până la expirarea acțiunii + = dt; dacă (scurs> = durata) isFinished = true; virtuale OnStart (void); // nu face nimic virtual OnEnd (void) // Introduce o noua actiune in lista list-> Insert (new PatrolRight ()); bool isFinished = false; bool isBlocking = true; Dușman inamic *; durata flotării = 10; // secunde până la terminarea flotorului scurs = 0; // secunde;
PatrolRight
vor arăta aproape identice, cu direcțiile răsucite. Când una dintre aceste acțiuni este plasată în lista de acțiuni a dușmanului, inamicul va cădea într-adevăr stâng și drept infinit.
Aici este o scurtă diagramă care arată fluxul unei liste de acțiuni, cu patru instantanee ale stării actualei liste de acțiuni pentru patrulare:
Următoarea adăugare ar trebui să fie detectarea când jucătorul se află în apropiere. Acest lucru se poate face printr-o acțiune non-blocantă care nu sa terminat vreodată. Această acțiune ar verifica dacă jucătorul se află în apropierea inamicului și, dacă da, va crea o nouă acțiune numită ThrowBomb
direct în fața sa în lista de acțiuni. De asemenea, va plasa a Întârziere
acțiune imediat după ThrowBomb
acțiune.
Acțiunea non-blocantă va sta acolo și va fi actualizată, dar lista de acțiuni va continua să actualizeze toate acțiunile ulterioare dincolo de aceasta. Acțiunile de blocare (cum ar fi Patrulare
) vor fi actualizate, iar lista de acțiuni va înceta să actualizeze toate acțiunile ulterioare. Rețineți că această acțiune este aici doar pentru a vedea dacă jucătorul se află în rază de acțiune și nu va părăsi niciodată lista de acțiuni!
Iată cum poate arăta această acțiune:
clasa DetectPlayer: public Acțiune virtual Update (float dt) // Aruncați o bombă și întrerupeți dacă jucătorul este în apropiere dacă (PlayerNearby ()) this-> InsertInFrontOfMe (new ThrowBomb ()); // Întrerupeți timp de 2 secunde acest lucru-> InsertInFrontOfMe (nou Pause (2.0)); virtuale OnStart (void); // nu face nimic virtual OnEnd (void) // nu face nimic bool isFinished = false; bool isBlocking = false; ;
ThrowBomb
acțiunea va fi o acțiune de blocare care aruncă o bomba spre jucator. Ar trebui probabil să fie urmată de a ThrowBombAnimation
, care blochează și joacă o animație inamic, dar am lăsat asta pentru concis. Pauza din spatele bombei va avea loc în animație și așteptați puțin înainte de finisare.
Să aruncăm o privire la o diagramă a ceea ce ar putea arăta această listă de acțiuni în timpul actualizării:
Bomba în sine ar trebui să fie un obiect de joc cu totul nou și să aibă trei acțiuni în lista de acțiuni proprii. Prima acțiune este o blocare Pauză
acțiune. După aceasta ar trebui să fie o acțiune pentru a juca o animație pentru o explozie. Sprite-ul bombei, împreună cu cutia de coliziune, va trebui eliminat. În cele din urmă, ar trebui să se joace un efect sonor de explozie.
În toate ar trebui să existe aproximativ șase până la zece tipuri diferite de acțiuni care sunt utilizate împreună pentru a construi comportamentul necesar. Cea mai bună parte a acestor acțiuni este că ele pot fi reutilizate în comportamentul oricărui tip de inamic, nu doar cel demonstrat aici.
Fiecare listă de acțiuni în forma sa actuală are un singur bandă în care pot exista acțiuni. O bandă este o secvență de acțiuni care trebuie actualizate. O bandă poate fi blocată sau blocată.
Implementarea perfectă a benzilor face uz de bitmasks. (Pentru detalii despre ce este o mască bitmaps, vă rugăm să consultați Cum se instalează Bitmask Quick pentru Programatori și pagina Wikipedia pentru o introducere rapidă.) Folosind un singur număr întreg pe 32 de biți, pot fi construite 32 de benzi diferite.
O acțiune ar trebui să aibă un număr întreg care să reprezinte toate benzile pe care se află. Acest lucru permite ca 32 de benzi diferite să reprezinte diferite categorii de acțiuni. Fiecare bandă poate fi blocată sau blocată în timpul rutinei de actualizare a listei în sine.
Iată un exemplu rapid al lui Actualizați
metoda unei liste de acțiune cu benzi de mască bitmaps:
void ActionList :: Actualizare (float dt) int i = 0; rute nesemnate = 0; în timp ce (i! = numActions) Acțiune * acțiune = acțiuni + i; dacă continuă (benzi și acțiuni-> benzi); acțiune-> actualizare (dt); dacă (action-> isBlocking) benzi | = acțiune-> benzi; dacă (acțiune-> este finalizată) action-> OnEnd (); action = this-> Remove (acțiune); ++ i;
Acest lucru oferă un nivel sporit de flexibilitate, deoarece în prezent o listă de acțiuni poate executa 32 de tipuri diferite de acțiuni, în cazul cărora ar fi necesar în prealabil 32 de liste de acțiuni diferite pentru a realiza același lucru.
O acțiune care nu face decât să întârzie toate acțiunile pentru o anumită perioadă de timp este un lucru foarte util. Ideea este de a întârzia toate acțiunile ulterioare să aibă loc până când nu mai există un cronometru.
Punerea în aplicare a acțiunii de întârziere este foarte simplă:
clasă Întârziere: public Acțiune public: void Update (float dt) elapsed + = dt; dacă (scurs> durata) este Finalizat = adevărat; ;
Un tip util de acțiune este unul care blochează până când este prima acțiune din listă. Acest lucru este util atunci când sunt difuzate câteva acțiuni non-blocante, dar nu sunteți sigur ce ordine vor termina sincroniza acțiunea asigură faptul că nu mai sunt lansate în prezent acțiuni non-blocante anterioare înainte de a continua.
Implementarea acțiunii de sincronizare este la fel de simplă:
Sincronizare clasă: public Acțiune public: void Actualizare (float dt) if (ownerList-> Begin () == this) isFinished = true; ;
Lista de acțiuni descrisă până acum este un instrument destul de puternic. Cu toate acestea, există câteva adăugiri care pot fi făcute pentru a lăsa într-adevăr lista de acțiune străluci. Acestea sunt un pic mai avansate și nu le recomand să le implementăm dacă nu puteți face acest lucru fără probleme prea mari.
Abilitatea de a trimite un mesaj direct la o acțiune sau de a permite o acțiune de a trimite mesaje către alte acțiuni și obiecte de joc este extrem de utilă. Acest lucru permite acțiunile să fie extrem de flexibile. Adesea, o listă de acțiuni de această calitate poate acționa ca o limbă de scriere a "săracului om".
Unele mesaje foarte utile de postat dintr-o acțiune pot include următoarele: au început; încheiat; întrerupte; reluate; finalizat; anulat; blocat. Blocat este destul de interesant - ori de câte ori o nouă acțiune este introdusă într-o listă, aceasta poate bloca alte acțiuni. Aceste alte acțiuni vor dori să știe despre acest lucru și, eventual, vor permite altor abonați să știe despre eveniment.
Detaliile implementării mesageriei sunt specifice limbii și mai degrabă non-trivial. Ca atare, detaliile implementării nu vor fi discutate aici, deoarece mesajele nu sunt în centrul acestui articol.
Există câteva modalități diferite de a reprezenta ierarhiile acțiunilor. O modalitate este de a permite ca o listă de acțiuni să fie o acțiune în cadrul unei alte liste de acțiuni. Aceasta permite construirea de liste de acțiuni care să cuprindă grupuri mari de acțiuni sub un singur identificator. Acest lucru sporește gradul de utilizare și face lista de acțiuni mai complexă mai ușor de dezvoltat și depanat.
O altă metodă este de a avea acțiuni al căror unic scop este de a crea alte acțiuni chiar înaintea ei înșiși în lista de acțiuni proprii. Eu însumi prefer această metodă cu cele de mai sus, deși poate fi un pic mai dificil de implementat.
Conceptul de listă de acțiuni și implementarea acesteia au fost discutate în detaliu pentru a oferi o alternativă la mașinile rigide ad-hoc de stat. Lista de acțiuni oferă un mijloc simplu și flexibil de a dezvolta rapid o gamă largă de comportamente dinamice. Lista de acțiuni este o structură ideală de date pentru programarea jocurilor în general.