Finite-State Machines Squad-mønster ved hjelp av styreadferd

Finite-state maskiner og styringsadferd er et perfekt match: deres dynamiske natur tillater kombinasjonen av enkle stater og krefter for å skape komplekse oppførselsmønstre. I denne opplæringen lærer du hvordan du kodes a Tall mønster ved hjelp av en stabelbasert finite-state-maskin kombinert med styringsadferd.

Alle FSM ikoner laget av Lorc og tilgjengelig på http://game-icons.net. Eiendeler i demo: Top / Down Shoot 'Em Up Spritesheet av takomogames and Alien Breed (esque) Top-Down Tilesheet av SpicyPixel.

Merk: Selv om denne opplæringen er skrevet med AS3 og Flash, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.


Endelig resultat

Etter å ha fullført denne opplæringen, vil du kunne implementere et gruppemønster der en gruppe soldater vil følge lederen, jakte på fiender og plageelementer:


Feltmønster implementert med stabelbasert FSM og styringsadferd. Flytt musepekeren for å lede lederen og klikk for å skyte.

Kombinere Stack-Based FSM og styringsadferd

Den forrige veiledningen om finite-state-maskiner beskrev hvor nyttig de er for å implementere kunstig intelligenslogikk: i stedet for å skrive en svært kompleks haug med AI-kode, kan logikken spres over et sett med enkle stater, hver som utfører svært spesifikke oppgaver, som for eksempel løper vekk fra en fiende.

Kombinasjonen av tilstander resulterer i et sofistikert AI, men lett å forstå, tilpasse og vedlikeholde. Den strukturen er også en av pilarene bak styringsadferd: kombinasjonen av enkle krefter for å skape komplekse mønstre.

Det er derfor FSMs og styringsadferd gjør en god kombinasjon. Statene kan brukes til å kontrollere hvilke krefter som skal fungere på et tegn, og forbedre det allerede kraftige settet av mønstre som kan opprettes ved hjelp av styringsadferd.


Kontrollere atferd ved hjelp av en Stack-basert FSM

For å organisere alle oppføringer, vil de bli spredt over statene. Hver stat vil generere en bestemt oppførselskraft, eller et sett av dem, som å søke, fly, og kollisjon unngås.

Når en bestemt tilstand er aktiv, vil bare dens resulterende kraft bli brukt på tegnet, slik at den oppfører seg tilsvarende. For eksempel, hvis den nåværende aktive tilstanden er Runaway og dens styrker er en kombinasjon av flykte og kollisjon unngås, tegnet vil fly et sted samtidig som det unngås noen hindring.

Styringskrefter beregnes hver spilloppdatering, og legges deretter til tegnets hastighetsvektor. Som en konsekvens, når den aktive tilstanden endres (og dermed bevegelsesmønsteret), overgår tegnet jevnt til det nye mønsteret etter hvert som de nye kreftene legges til etter hver oppdatering.

Den dynamiske naturen til styreadferd sikrer denne fluidovergangen; statene samordner bare hvilke styringskrefter som er aktive når som helst.


Kodekonstruksjonen

Strukturen for å implementere et lagsmønster vil inkapslere FSMs og styringsadferd i egenskapene til en klasse. Enhver klasse som representerer en enhet som beveger seg eller ellers påvirkes av styringskrefter, vil ha en eiendom som kalles boid, som er en forekomst av Boid klasse:

offentlig klasse Boid public var posisjon: Vector3D; offentlig varhastighet: Vector3D; offentlig varstyring: Vector3D; offentlig var masse: tall; offentlig funksjonssøk (mål: Vector3D, slowingRadius: Number = 0): Vector3D (...) offentlig funksjon flyr (posisjon: Vector3D): Vector3D (...) offentlig funksjon oppdatering (): void (...) )

De Boid Klassen ble brukt i styringsadferdsserien og det gir egenskaper som hastighet og stilling (begge matematiske vektorer), sammen med metoder for å legge til styringskrefter, for eksempel søke(), flykte(), etc.

En enhet som bruker en stabelbasert FSM vil ha samme struktur av Maur klasse fra forrige FSM-opplæringen: Stack-basert FSM styres av hjerne eiendom og hver stat er implementert som en metode.

Nedenfor er Soldat klasse, som har styringsadferd og FSM evner:

offentlig klasse soldat privat var hjerne: StackFSM; // Kontrollerer FSM ting private var boid: Boid; // Kontrollerer styreregistrering offentlig funksjon Soldat (posX: Nummer, posY: Nummer, totalMasse: Nummer) (...) hjerne = ny StackFSM (); // Trykk på "følg" -tilstanden, så soldaten vil følge lederen brain.pushState (følg);  offentlig funksjon oppdatering (): void // Oppdater hjernen. Den vil kjøre den nåværende tilstandsfunksjonen. brain.update (); // Oppdater styringsadferdene boid.update (); 

Planlegger "Brain"

Truppemønsteret vil bli implementert ved hjelp av en stabelbasert finite-state-maskin. Soldatene, som er medlemmer av truppen, vil følge lederen (kontrollert av spilleren), jakte på noen nærliggende fiender.

Når en fiende dør, kan det falle et element som kan være bra eller dårlig (a Medkit eller a badkit, henholdsvis). En soldat vil bryte troppformasjonen og samle nærliggende gode ting, eller vil unnslippe stedet for å unngå eventuelle dårlige gjenstander.

Nedenfor er en grafisk fremstilling av den stabelbaserte FSM som styrer soldatens "hjerne":


FSM representerer hjernen til en soldat.

De neste avsnittene presenterer implementeringen av hver stat. Alle kodestykker i denne opplæringen beskriver hovedideen til hvert trinn, unnlatelse av alle detaljer angående spillmotoren som brukes (Flixel, i dette tilfellet).


Etter lederen

Den første staten som skal gjennomføres, er den som vil forbli aktiv nesten hele tiden: Følg lederen. Plyndringsdelen vil bli implementert senere, så for nå Følg staten vil bare få soldaten til å følge lederen, bytte nåværende tilstand til jakt hvis det er en fiende i nærheten:

offentlig funksjon følger (): void var aLeader: Boid = Game.instance.boids [0]; // få en peker til lederen addSteeringForce (boid.followLeader (aLeader)); // Følg lederen // Er det et monster i nærheten? hvis (getNearestEnemy ()! = null) // Ja, det er! Hunt det ned! // Trykk på "jakten" -tilstanden. Det vil få soldaten til å stoppe etter lederen og / / begynne å jakte på monsteret. brain.pushState (jakt);  privat funksjon getNearestEnemy (): Monster // Her går implementeringen for å få nærmeste fiende

Til tross for tilstedeværelsen av fiender, mens staten er aktiv, vil den alltid generere en styrke for å følge lederen ved å bruke lederens oppførsel.

Hvis getNearestEnemy () Returnerer noe, det betyr at det er en fiende rundt. I så fall er det jakt tilstanden skyves inn i stabelen gjennom samtalen brain.pushState (hunt), slik at soldaten slutter å følge lederen og begynner å jakte på fiender.

For nå, implementeringen av jakt() Staten kan bare stikke seg selv fra stabelen, slik at soldatene ikke vil bli sittende fast i jaktstaten:

offentlig funksjon jakt (): void // For nå, la oss bare pop jakten () staten fra hjernen. brain.popState (); 

Merk at ingen informasjon er sendt til jakt stat, for eksempel hvem er nærmeste fiende. Denne informasjonen må samles inn av jakt stat selv fordi den bestemmer om jakt bør forbli aktiv eller pop seg selv fra stabelen (returnerer kontrollen til Følg stat).

Resultatet hittil er en soldatgruppe som følger lederen (merk at soldatene ikke vil jakte fordi jakt() Metoden bare popper seg):


Squad mønster med "følge" og ikke-fungerende "jakt" stater. Flytt musepekeren for å lede lederen og klikk for å skyte.

Tips: hver stat burde være ansvarlig for å avslutte sin eksistens ved å poppe seg fra stakken.


Bryte formasjon og jakt

Den neste tilstanden som skal implementeres er jakt, som vil få soldater til å jakte på en nærliggende fiende. Koden for jakt() er:

offentlig funksjon jakt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Har vi et monster i nærheten? hvis (aNestestEnemy! = null) // Ja, det gjør vi. La oss beregne hvor langt det er. var aDistance: Number = calculateDistance (aNearestEnemy, dette); // Er monsteret nært nok til å skyte? hvis (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();  

Staten begynner med å tildele aNearestEnemy med nærmeste fiende. Hvis aNearestEnemy er null det betyr at det ikke er noen fiende rundt, så staten må ende. Samtalen brain.popState () popper på jakt state, bytte soldaten til neste tilstand i stabelen.

Hvis aNearestEnemy er ikke null, det betyr at det er en fiende som skal jages ned og staten skal forbli aktiv. Jaktalgoritmen er basert på avstanden mellom soldaten og fienden: hvis avstanden er større enn 80, vil soldaten søke fiendens posisjon; hvis avstanden er mindre enn 80, vil soldaten møte fienden og skyte mens du står stille.

Siden jakt() vil bli påkalt hver spilloppdatering, hvis en fiende er rundt da vil soldaten søke eller skyte den fienden. Beslutningen om å flytte eller skyte styres dynamisk av avstanden mellom soldaten og fienden.

Resultatet er en gruppe soldater som kan følge lederen og jakte på nærliggende fiender:


Squad mønster med "følge" og "jakten". Flytt musepekeren for å lede lederen og klikk for å skyte.

Looting og løpende bort

Hver gang en fiende blir drept, kan det falle et element. Soldaten må samle varen hvis den er en god, eller flykte varen hvis den er dårlig. Atferden er representert av to stater i den tidligere beskrevne FSM:


FSM fremhever collectItem og Runaway stater.

De collectItem staten vil få en soldat til å komme til det tapt elementet, mens Runaway staten vil få soldaten til å flykte fra den dårlige varens plassering. Begge statene er nesten identiske, den eneste forskjellen er ankomst- eller flyktekraften:

offentlig funksjon runAway (): void var aItem: Item = getNearestItem (); hvis (aItem! = null && aItem.alive && aItem.type == Item.BADKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.flee (aItemPos));  ellers brain.popState ();  offentlig funksjon collectItem (): void var aItem: Item = getNearestItem (); hvis (aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = ny Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50));  ellers brain.popState ();  privat funksjon getNearestItem (): Objekt // her går koden for å få nærmeste element

Her kommer en optimalisering av overgangene til nytte. Koden til overgang fra Følg stat til collectItem eller Runaway stater er det samme: sjekk om det finnes et element i nærheten, og trykk deretter på den nye tilstanden.

Staten som skal skyves, avhenger av varens type. Som følge av overgangen til collectItem eller Runaway kan implementeres som en enkelt metode, oppkalt checkItemsNearby ():

privat funksjon checkItemsNearby (): void var aItem: Item = getNearestItem (); hvis (aItem! = null) brain.pushState (aItem.type == Item.BADKIT? runAway: collectItem); 

Denne metoden kontrollerer nærmeste element. Hvis det er en god en, så collectItem staten blir skjøvet inn i hjernen; hvis det er en dårlig en, Runaway tilstanden er presset. Hvis det ikke er noe å samle, gjør metoden ingenting.

At optimalisering tillater bruk av checkItemsNearby () å kontrollere overgangen fra hvilken som helst tilstand til collectItem eller Runaway. Ifølge soldaten FSM, eksisterer denne overgangen i to stater: Følg og jakt.

Deres gjennomføring kan endres litt for å imøtekomme den nye overgangen:

offentlig funksjon følger (): void var aLeader: Boid = Game.instance.boids [0]; // få en peker til lederen addSteeringForce (boid.followLeader (aLeader)); // følg lederen // Sjekk om det er et element som skal samles inn (eller løpe bort fra) checkItemsNearby (); // Er det et monster i nærheten? hvis (getNearestEnemy ()! = null) // Ja, det er! Hunt det ned! // Trykk på "jakten" -tilstanden. Det vil få soldaten til å stoppe etter lederen og / / begynne å jakte på monsteret. brain.pushState (jakt);  offentlig funksjon jakt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Sjekk om det er et element å samle inn (eller løpe bort fra) checkItemsNearby (); // Har vi et monster i nærheten? hvis (aNestestEnemy! = null) // Ja, det gjør vi. La oss beregne hvor langt det er. var aDistance: Number = calculateDistance (aNearestEnemy, dette); // Er monsteret nært nok til å skyte? hvis (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();  

Mens lederen følger, vil en soldat sjekke for nærliggende gjenstander. Når du jakter på en fiende, vil en soldat også sjekke for nærliggende gjenstander.

Resultatet er demoen nedenfor. Legg merke til at en soldat vil prøve å samle inn eller unngå et objekt når som helst der er en i nærheten, selv om det er fiender å jakte og lederen følger.


Squad mønster med "følge", "jakten", "collectItem" og "runAway". Flytt musepekeren for å lede lederen og klikk for å skyte.

Prioriterende stater og overganger

Et viktig aspekt ved tilstander og overganger er prioritet blant dem. Avhengig av linjen der en overgang er plassert innenfor en stats implementering, endres prioriteten for den overgangen.

Bruker Følg stat og overgangen laget av checkItemsNearby () For eksempel, ta en titt på følgende implementering:

offentlig funksjon følger (): void var aLeader: Boid = Game.instance.boids [0]; // få en peker til lederen addSteeringForce (boid.followLeader (aLeader)); // følg lederen // Sjekk om det er et element som skal samles inn (eller løpe bort fra) checkItemsNearby (); // Er det et monster i nærheten? hvis (getNearestEnemy ()! = null) // Ja, det er! Hunt det ned! // Trykk på "jakten" -tilstanden. Det vil få soldaten til å stoppe etter lederen og / / begynne å jakte på monsteret. brain.pushState (jakt); 

Den versjonen av Følg() vil gjøre en soldat bytte til collectItem eller Runaway før sjekke om det er en fiende rundt. Som en konsekvens vil soldaten samle (eller flykte fra) et element selv når det er fiender rundt det som skal jages ned av jakt stat.

Her er en annen implementering:

offentlig funksjon følger (): void var aLeader: Boid = Game.instance.boids [0]; // få en peker til lederen addSteeringForce (boid.followLeader (aLeader)); // Følg lederen // Er det et monster i nærheten? hvis (getNearestEnemy ()! = null) // Ja, det er! Hunt det ned! // Trykk på "jakten" -tilstanden. Det vil få soldaten til å stoppe etter lederen og / / begynne å jakte på monsteret. brain.pushState (jakt);  ellers // Sjekk om det er et element å samle inn (eller løpe bort fra) checkItemsNearby (); 

Den versjonen av Følg() vil gjøre en soldat bytte til collectItem eller Runaway bare etter han finner ut at det ikke er noen fiender å drepe.

Den nåværende gjennomføringen av Følg(), jakt() og collectItem () lider av prioriterte problemer. Soldaten vil prøve å samle et element selv når det er viktigere ting å gjøre. For å fikse det, er det behov for noen tweaks.

Angående Følg stat, koden kan oppdateres til:

(følg () med prioriteter)

offentlig funksjon følger (): void var aLeader: Boid = Game.instance.boids [0]; // få en peker til lederen addSteeringForce (boid.followLeader (aLeader)); // Følg lederen // Er det et monster i nærheten? hvis (getNearestEnemy ()! = null) // Ja, det er! Hunt det ned! // Trykk på "jakten" -tilstanden. Det vil få soldaten til å stoppe etter lederen og / / begynne å jakte på monsteret. brain.pushState (jakt);  ellers // Sjekk om det er et element å samle inn (eller løpe bort fra) checkItemsNearby (); 

De jakt Staten må endres til:

offentlig funksjon jakt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Har vi et monster i nærheten? hvis (aNestestEnemy! = null) // Ja, det gjør vi. La oss beregne hvor langt det er. var aDistance: Number = calculateDistance (aNearestEnemy, dette); // Er monsteret nært nok til å skyte? hvis (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();  

Endelig, den collectItem Staten må endres for å avbryte noen plager hvis det er en fiende rundt:

offentlig funksjon collectItem (): void var aItem: Item = getNearestItem (); var aMonsterNearby: Boolean = getNearestEnemy ()! = null; hvis (! aMonsterNearby && aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aIemPos: Vector3D = ny Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50));  ellers brain.popState (); 

Resultatet av alle disse endringene er demoen fra begynnelsen av opplæringen:


Feltmønster implementert med stabelbasert FSM og styringsadferd. Flytt musepekeren for å lede lederen og klikk for å skyte.

Konklusjon

I denne opplæringen lærte du hvordan du kodes et gruppemønster der en gruppe soldater vil følge en leder, jakte ned og plyndre nærliggende fiender. AI er implementert ved hjelp av en stabelbasert FSM kombinert med flere styringsegenskaper.

Som vist er finite-state maskiner og styring atferd en kraftig kombinasjon og en flott kamp. Spredning av logikken over FSM-statene, det er mulig å dynamisk velge hvilken styringskrefter som skal fungere på et tegn, slik at det blir mulig å lage komplekse AI-mønstre.

Kombiner styringsadferdene du allerede kjenner med FSMs, og opprett nye og fremragende mønstre!