Handlingslistedatastrukturen Bra for brukergrensesnitt, AI, animasjoner og mer

De handlingsliste er en enkel datastruktur som er nyttig for mange ulike oppgaver i en spillmotor. Man kan hevde at handlingslisten alltid skal brukes i stedet for noen form for statlig maskin.

Den vanligste formen (og enkleste form) av oppførsel er en finite state machine. Vanligvis implementert med brytere eller arrays i C eller C ++, eller slews of hvis og ellers uttalelser på andre språk, statlige maskiner er stive og ufleksible. Handlingslisten er en sterkere organisasjonsordning ved at den på en tydelig måte modellerer hvordan ting vanligvis skjer i virkeligheten. Av denne grunn er handlingslisten mer intuitiv og fleksibel enn en endelig statlig maskin.


Raskt Overblikk

Handlingslisten er bare en organisasjonsordning for begrepet a tidsbestemt handling. Handlinger lagres i en første i første ut (FIFO) bestilling. Dette betyr at når en handling er satt inn i en handlingsliste, blir den siste handlingen satt inn i fronten den første som skal fjernes. Handlingslisten følger ikke FIFO-formatet eksplisitt, men i kjernen forbli de samme.

Hvert spillsløyfe er handlingslisten oppdatert og Hver handling i listen oppdateres i rekkefølge. Når en handling er ferdig, blir den fjernet fra listen.

en handling er en slags funksjon å ringe som gjør noe slags arbeid på en eller annen måte. Her er noen forskjellige typer områder og det arbeidet som handlinger kan utføre innenfor dem:

  • Brukergrensesnitt: Viser korte sekvenser som "prestasjoner", spiller sekvenser av animasjoner, bla gjennom vinduer, viser dynamisk innhold: flytte; rotere; flip; falme; generell tweening.
  • Kunstig intelligens: Kjøreadferd: Flytt; vente; patrulje; flykte; angrep.
  • Nivålogikk eller oppførsel: Flytende plattformer; hindringsbevegelser; skiftende nivåer.
  • Animasjon / Lyd: Spill; Stoppe.

Lågt nivå ting som sti finne eller flocking er ikke effektivt representert med en handlingsliste. Bekjempelse og andre høyt spesialiserte spillspesifikke spillområder er også ting som man sannsynligvis ikke bør implementere via en handlingsliste.


Handlingsklasse

Her er en rask titt på hva som skal ligge inne i handlingslistedatastrukturen. Vær oppmerksom på at mer detaljerte detaljer vil følge senere i artikkelen.

 klasse ActionList public: void Update (float dt); void PushFront (Action * action); void PushBack (Action * action); void InsertBefore (Action * action); void InsertAfter (Action * action); Handling * Fjern (Handling * handling); Handling * Begynn (tom); Handling * Slutt (tom) bool IsEmpty (void) const; flyte TimeLeft (void) const; bool IsBlocking (void) const; privat: flytevarighet; float timeElapsed; float percentDone; bool blokkering; usignerte baner; Handling ** handlinger; // kan være en vektor eller lenket liste;

Det er viktig å merke seg at den faktiske lagringen av hver handling ikke behøver å være en faktisk koblet liste - noe som C++ std :: vektor ville fungere helt bra. Min egen preferanse er å samle alle handlinger inne i en fordeler og lenke lister sammen med påtrengende lister. Vanligvis blir handlingslister benyttet i mindre ytelsesfølsomme områder, så det vil trolig være tung datorientert optimalisering når man utvikler en handlingliste datastruktur.


Handlingen

Kjernen i denne hele shebang er handlingene selv. Hver handling bør være helt selvstendig slik at handlingslisten selv ikke vet noe om handlingenes internaler. Dette gjør handlingslisten til et ekstremt fleksibelt verktøy. En handlingsliste bryr seg ikke om det kjører brukergrensesnitthandlinger eller styrer bevegelsene til et 3D-modellert tegn.

En god måte å implementere handlinger på er gjennom et enkelt abstrakt grensesnitt. Noen få spesifikke funksjoner blir eksponert fra handlingsobjektet til handlingslisten. Her er et eksempel på hvordan en bashandling kan se ut:

 class Action public: virtual update (float dt); virtuell OnStart (tomrom); virtuell onEnd (void); bool isFinished; bool isBlocking; usignerte baner; flyte forlenget; flytevarighet; privat: ActionList * ownerList; ;

De ONSTART () og OnEnd () funksjoner er integrert her. Disse to funksjonene skal utføres når en handling settes inn i en liste, og når handlingen er ferdig, henholdsvis. Disse funksjonene tillater handlinger å være helt selvstendige.

Blokkering og ikke-blokkering av handlinger

En viktig utvidelse til handlingslisten er muligheten til å betegne handlinger som enten blokkerer og ikke-blokker. Sondringen er enkel: En blokkering utfører handlingslistens oppdateringsrutine, og ingen videre handlinger blir oppdatert. En ikke-blokkerende handling tillater at den etterfølgende handlingen blir oppdatert.

En enkelt boolsk verdi kan brukes til å avgjøre om en handling blokkerer eller ikke blokkerer. Her er noen psuedocode som viser en handlingsliste Oppdater rutine:

 void ActionList :: Oppdater (float dt) int i = 0; mens (i! = numActions) Action * action = actions + i; handling-> Oppdatering (dt); hvis (handling-> isBlocking) pause; hvis (handling-> erfinansiert) action-> OnEnd (); action = this-> Fjern (handling);  ++ i; 

Et godt eksempel på bruken av ikke-blokkerende handlinger ville være å tillate at noen atferd til alle løper samtidig. Hvis vi for eksempel har en kø med handlinger for å kjøre og vifte hender, burde karakteren som utfører disse handlingene, kunne gjøre begge samtidig. Hvis en fiende løper fra tegnet, ville det være veldig dumt hvis det måtte løpe, så stopp og bøl hendene frantically, og fortsett å løpe.

Som det viser seg, samsvarer begrepet blokkerende og ikke-blokkerende handlinger intuitivt med de fleste typer enkle atferd som kreves for å bli implementert i et spill.


Sakseksempel

La oss dekke et eksempel på hva som kjører en handlingsliste, ville se ut i et ekte scenario. Dette vil bidra til å utvikle intuisjon om hvordan du bruker en handlingsliste, og hvorfor handlingslister er nyttige.

Problem

En fiende i et enkelt topp-down 2D-spill må patrulje frem og tilbake. Når denne fienden er innenfor rekkevidde av spilleren, trenger den å kaste en bombe mot spilleren, og pause patruljen. Det bør være en liten nedkjøling etter at en bombe er kastet der fienden står helt stille. Hvis spilleren fortsatt er i rekkevidde, må en annen bombe etterfulgt av en nedkjøling kastes. Hvis spilleren er utenfor rekkevidde, bør patruljen fortsette nøyaktig hvor den slått av.

Hver bombe bør flyte gjennom 2D-verdenen og følge lovene i flisbasert fysikk implementert i spillet. Bomben venter bare til sikringstimeren er ferdig, og blåser deretter opp. Eksplosjonen bør bestå av en animasjon, en lyd og en fjerning av bombeens kollisjonskasse og visuell sprite.

Å bygge en statlig maskin for denne oppførselen vil være mulig og ikke for vanskelig, men det vil ta litt tid. Overganger fra hver stat må kodes for hånd, og lagring av tidligere tilstander for å fortsette senere kan føre til hodepine.

Handlingsliste Løsning

Heldigvis er dette et ideelt problem å løse med handlingslister. Først, la oss forestille en tom handlingsliste. Denne tomme handlingslisten vil representere en liste over "å gjøre" ting for fienden å fullføre; En tom liste angir en inaktiv fiende.

Det er viktig å tenke på hvordan man "compartmentalize" den ønskede oppførselen til små nuggets. Den første tingen å gjøre er å få ned patruljeadferd. La oss anta at fienden skal patruljere etter en avstand, så patruljerer rett ved samme avstand, og gjenta.

Her er hva patrulje til venstre Handlingen kan se ut som:

 klasse PatrolLeft: offentlig handling virtuell oppdatering (float dt) // Flytt fienden venstre fiende-> posisjon.MoveLeft (); // Timer til ferdigstillingen er ferdig + = dt; hvis (forløpt> = varighet) isFinished = true;  virtuell OnStart (void); // gjør ingenting virtuelt OnEnd (void) // Sett inn en ny handling i listen listen-> Sett inn (ny PatrolRight ());  bool isFinished = false; bool isBlocking = true; Fiende * fiende; flytevarighet = 10; // sekunder til slutt float forsvunnet = 0; // sekunder;

PatrolRight vil se nesten identisk, med retningene vendt. Når en av disse handlingene er plassert i fiendens handlingsliste, vil fienden faktisk patruljere til venstre og høyre uendelig.

Her er et kort diagram som viser strømmen av en handlingsliste, med fire stillbilder av tilstanden til gjeldende handlingsliste for patruljering:

Det neste tillegget bør være gjenkjenning av når spilleren er i nærheten. Dette kan gjøres med en ikke-blokkerende handling som aldri fullfører. Denne handlingen vil sjekke om spilleren er nær fienden, og i så fall vil det opprettes en ny handling kalt ThrowBomb rett foran seg selv i handlingslisten. Det vil også plassere a Utsette handling rett etter ThrowBomb handling.

Den ikke-blokkerende handlingen vil sitte der og bli oppdatert, men handlingslisten fortsetter å oppdatere alle påfølgende handlinger utenfor den. Blokkering av handlinger (for eksempel Patrulje) vil bli oppdatert og handlingslisten vil slutte å oppdatere eventuelle påfølgende handlinger. Husk at denne handlingen er her for å se om spilleren er i rekkevidde, og vil aldri forlate handlingslisten!

Her er hva denne handlingen ser ut som:

 class DetectPlayer: offentlig handling virtuell oppdatering (float dt) // Kast en bombe og pause hvis spilleren er i nærheten hvis (PlayerNearby ()) this-> InsertInFrontOfMe (new ThrowBomb ()); // Pause i 2 sekunder dette-> InsertInFrontOfMe (ny pause (2.0));  virtuell OnStart (void); // gjør ingenting virtuelt OnEnd (void) // gjør ingenting bool isFinished = false; bool isBlocking = false; ;

De ThrowBomb Handlingen vil være en blokkerende handling som kaster en bombe mot spilleren. Det skal nok følges av a ThrowBombAnimation, som blokkerer og spiller en fiendtlig animasjon, men jeg har forlatt dette ut for konsistens. Pause bak bomben vil finne sted for animasjonen, og vent litt før du er ferdig.

La oss se på et diagram over hva denne handlingslisten kan se ut mens du oppdaterer:


Blå sirkler blokkerer handlinger. Hvite sirkler er ikke-blokkerende handlinger.

Bomben i seg selv bør være et helt nytt spillobjekt, og har tre eller så handlinger i sin egen handlingsliste. Den første handlingen er en blokkering Pause handling. Etter dette bør være en handling for å spille en animasjon for en eksplosjon. Selve bombenet, sammen med kollisjonskassen, må fjernes. Til slutt bør en eksplosjon lydeffekt spilles.

I alt bør det være rundt seks til ti forskjellige typer handlinger som alle brukes sammen for å konstruere den nødvendige oppførselen. Den beste delen om disse handlingene er at de kan være gjenbrukes i oppførselen av enhver fiendtlig type, ikke bare den som er demonstrert her.


Mer om handlinger

Handlingsbaner

Hver handlingsliste i sin nåværende form har en enkelt kjørefelt i hvilke handlinger kan eksistere. En bane er en rekke handlinger som skal oppdateres. En kjørefelt kan enten være blokkert eller ikke blokkert.

Den perfekte gjennomføringen av baner gjør bruk av bitmasks. (For detaljer om hva en bitmask er, vennligst se En rask Bitmask Hvordan-til for programmerere og Wikipedia-siden for en rask introduksjon.) Ved hjelp av et 32-biters heltall kan 32 forskjellige baner konstrueres.

En handling bør ha et heltall for å representere alle de forskjellige banene den ligger på. Dette gjør det mulig for 32 forskjellige baner å representere ulike typer handlinger. Hver bane kan enten bli blokkert eller ikke blokkert under oppdateringsrutinen i listen selv.

Her er et raskt eksempel på Oppdater Metode for en handlingsliste med bitmaskbaner:

 void ActionList :: Oppdater (float dt) int i = 0; usignerte baner = 0; mens (i! = numActions) Action * action = actions + i; hvis (baner og handling-> baner) fortsetter; handling-> Oppdatering (dt); hvis (handling-> isBlocking) baner | = action-> baner; hvis (handling-> erfinansiert) action-> OnEnd (); action = this-> Fjern (handling);  ++ i; 

Dette gir økt fleksibilitet, da en handlingsliste nå kan kjøre 32 forskjellige typer handlinger, hvor det på forhånd ville være 32 forskjellige handlingslister for å oppnå det samme.

Forsinkelseshandling

En handling som ikke gjør noe, men forsinker alle handlinger i en bestemt tidsperiode, er en veldig nyttig ting å ha. Tanken er å forsinke alle påfølgende handlinger fra å finne sted til en tidtaker er gått.

Gjennomføringen av forsinkelseshandlingen er veldig enkel:

 klasseforsinkelse: offentlig handling offentlig: ugyldig oppdatering (float dt) elapsed + = dt; hvis (forløpt> varighet) isFinished = true; ;

Synkroniser handling

En nyttig type handling er en som blokkerer til den er den første handlingen i listen. Dette er nyttig når noen få forskjellige ikke-blokkerende handlinger blir kjørt, men du er ikke sikker på hvilken rekkefølge de vil fullføre i synkron handling sikrer at ingen tidligere ikke-blokkerende handlinger kjører for tiden før de fortsetter.

Implementeringen av synkroniseringsvirksomheten er så enkel som man kan forestille seg:

 klassesynkronisering: offentlig handling offentlig: ugyldig oppdatering (float dt) hvis (ownerList-> Begynn () == dette) isFinished = true; ;

Avanserte funksjoner

Handlingslisten som er beskrevet så langt, er et ganske kraftig verktøy. Men det er et par tillegg som kan gjøres for å virkelig la handlingslisten skinne. Disse er litt avanserte og jeg anbefaler ikke å implementere dem, med mindre du kan gjøre det uten for mye trøbbel.

Meldinger

Muligheten til å sende en melding direkte til en handling, eller tillate en handling for å sende meldinger til andre handlinger og spillobjekter, er ekstremt nyttig. Dette tillater handlinger å være ekstremt fleksible. Ofte en handlingsliste over denne kvaliteten kan fungere som en "fattig manns skriptspråk".

Noen svært nyttige meldinger å legge ut fra en handling kan inkludere følgende: startet; endte; pauset; gjenopptatt; fullført; avbrutt; blokkert. Den blokkerte er ganske interessant - når en ny handling blir satt inn i en liste, kan den blokkere andre handlinger. Disse andre handlingene vil vite om det, og muligens la andre abonnenter vite om arrangementet også.

Implementeringsdetaljer for meldinger er språkspesifikke og heller ikke trivielle. Som sådan vil detaljene i implementeringen ikke bli diskutert her, da meldingen ikke er i fokus for denne artikkelen.

Hierarkiske tiltak

Det er noen forskjellige måter å representere handlingshierarkier på. En måte er å la en handlingsliste selv være en handling innenfor en annen handlingsliste. Dette tillater bygging av handlingslister for å pakke sammen store grupper av handlinger under en enkelt identifikator. Dette øker brukervennligheten og gjør det enklere å utvikle og feilsøke en mer kompleks handlingsliste.

En annen metode er å ha handlinger hvis eneste formål er å gyte andre handlinger rett før seg selv i eiersaksjonslisten. Jeg foretrekker denne metoden for det nevnte, selv om det kan være litt vanskeligere å implementere.


Konklusjon

Konseptet med en handlingsliste og implementeringen er blitt diskutert i detalj for å gi et alternativ til stive ad hoc-statlige maskiner. Handlingslisten gir en enkel og fleksibel måte å raskt utvikle et bredt spekter av dynamiske oppføringer. Handlingslisten er en ideell datastruktur for spillprogrammering generelt.