Eindige-toestandsmachines en stuurgedrag zijn een perfecte match: hun dynamische aard maakt de combinatie van eenvoudige toestanden en krachten mogelijk om complexe gedragspatronen te creëren. In deze zelfstudie leer je hoe je een a moet coderen rot patroon met behulp van een op een stapel gebaseerde eindige toestandsmachine gecombineerd met stuurgedrag.
Alle FSM-iconen gemaakt door Lorc en beschikbaar op http://game-icons.net. Activa in de demo: Top / Down Shoot 'Em Up Spritesheet door takomogames en Alien Breed (esque) Top-Down Tilesheet door SpicyPixel.
Notitie: Hoewel deze tutorial geschreven is met behulp van AS3 en Flash, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.
Na het voltooien van deze tutorial, kun je een squadronpatroon implementeren waarin een groep soldaten de leider volgt, vijanden jaagt en plundering gebruikt:
In de vorige zelfstudie over machines met een eindige staat werd beschreven hoe nuttig ze zijn voor het implementeren van kunstmatige intelligentie: in plaats van een zeer complexe stapel AI-code te schrijven, kan de logica worden verspreid over een reeks eenvoudige toestanden, die elk zeer specifieke taken uitvoeren, zoals wegrennen van een vijand.
De combinatie van toestanden resulteert in een geavanceerde AI, maar toch gemakkelijk te begrijpen, aan te passen en te onderhouden. Die structuur is ook een van de pijlers achter stuurgedrag: de combinatie van eenvoudige krachten om complexe patronen te creëren.
Dat is de reden waarom FSM's en stuurgedrag een geweldige combinatie vormen. De toestanden kunnen worden gebruikt om te bepalen welke krachten op een personage zullen reageren, waardoor de reeds krachtige reeks patronen die kunnen worden gecreëerd met behulp van stuurgedrag, wordt verbeterd.
Om alle gedragingen te organiseren, zullen ze verspreid zijn over de staten. Elke staat genereert een specifieke gedragskracht, of een reeks van hen, zoals zoeken, vluchten en aanvaringen vermijden.
Wanneer een bepaalde status actief is, wordt alleen de resulterende kracht op het personage toegepast, waardoor deze zich overeenkomstig gedraagt. Bijvoorbeeld, als de huidige actieve status is Weglopen
en de krachten zijn een combinatie van vluchten
en het uit de weg gaan van botsingen
, het personage zal een plaats ontvluchten zonder obstakels te ontwijken.
De stuurkrachten worden bij elke spelupdate berekend en vervolgens toegevoegd aan de snelheidsvector van het personage. Als gevolg hiervan, wanneer de actieve status verandert (en daarmee het bewegingspatroon), zal het personage vloeiend overschakelen naar het nieuwe patroon wanneer de nieuwe krachten worden toegevoegd na elke update.
De dynamische aard van stuurgedrag zorgt voor deze vloeiende overgang; de staten coördineren alleen welke stuurkrachten op een bepaald moment actief zijn.
De structuur om een squadronpatroon te implementeren, zal FSM's en stuurgedrag inkapselen in eigenschappen van een klasse. Elke klasse die een entiteit vertegenwoordigt die beweegt of op een andere manier wordt beïnvloed door stuurkrachten, krijgt een eigenschap genaamd Boid
, wat een instantie is van de Boid
klasse:
public class Boid public var position: Vector3D; public var velocity: Vector3D; openbare var-besturing: Vector3D; openbare var-massa: Number; public function seek (target: Vector3D, slowingRadius: Number = 0): Vector3D (...) openbare functie vlucht (positie: Vector3D): Vector3D (...) update openbare functie (): void (...) (... )
De Boid
klasse werd gebruikt in de serie stuurgedrag en biedt eigenschappen als snelheid
en positie
(beide wiskundige vectoren), samen met methoden om stuurkrachten toe te voegen, zoals zoeken()
, vluchten()
, enz.
Een entiteit die een stack-gebaseerde FSM gebruikt, heeft dezelfde structuur als de Mier
klas uit de vorige FSM-tutorial: de stack-gebaseerde FSM wordt beheerd door de hersenen
eigendom en elke staat is geïmplementeerd als een methode.
Hieronder staat de Soldaat
klasse, met stuurgedrag en FSM-mogelijkheden:
openbare klasse Soldier private var brain: StackFSM; // Bestuurt het FSM-materiaal privé var boid: Boid; // Besturing stuurgedrag publieke functie Soldier (posX: Number, posY: Number, totalMass: Number) (...) brain = new StackFSM (); // Duw de "volg" -status zodat de soldaat de leider volgt brain.pushState (volg); public function update (): void // Update de hersenen. Het zal de huidige statusfunctie uitvoeren. brain.update (); // Update het stuurgedrag boid.update ();
Het squadronpatroon wordt geïmplementeerd met behulp van een op een stapel gebaseerde machine met eindige toestanden. De soldaten, die de leden van het squadron zijn, volgen de leider (bestuurd door de speler) en jagen vijanden in de buurt aan.
Wanneer een vijand sterft, kan er een item vallen dat goed of slecht kan zijn (a medkit of a badkit, respectievelijk). Een soldaat zal de squadronformatie verbreken en goede voorwerpen in de buurt verzamelen, of zal de plaats ontwijken om slechte items te vermijden.
Hieronder is een grafische weergave van de op een stapel gebaseerde FSM die het "brein" van de soldaat bestuurt:
De volgende secties presenteren de implementatie van elke staat. Alle codefragmenten in deze tutorial beschrijven het hoofdidee van elke stap en laten alle details over de gebruikte game-engine weg (Flixel, in dit geval).
De eerste te implementeren status is degene die bijna altijd actief blijft: Volg de leider. Het plunderende deel zal later worden geïmplementeerd, dus voor nu is het volgen
de staat zal de soldaat alleen maar de leider doen volgen, en de huidige toestand veranderen naar jacht
als er een vijand in de buurt is:
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // krijg een verwijzing naar de leider addSteeringForce (boid.followLeader (aLeader)); // volg de leider // Is er een monster in de buurt? if (getNearestEnemy ()! = null) // Ja, dat is zo! Zoek het op! // Druk op de "jacht" -status. Het zorgt ervoor dat de soldaat stopt met het volgen van de leider en // begin met het jagen op het monster. brain.pushState (jagen); private functie getNearestEnemy (): Monster // hier gaat de implementatie om de dichtstbijzijnde vijand te krijgen
Ondanks de aanwezigheid van vijanden genereert de staat, terwijl de staat actief is, altijd een kracht om de leider te volgen, met behulp van het leidende volgende gedrag.
Als getNearestEnemy ()
geeft iets terug, het betekent dat er een vijand in de buurt is. In dat geval, de jacht
status wordt door de oproep in de stapel geduwd brain.pushState (jacht)
, de soldaat laten stoppen met het volgen van de leider en beginnen met het jagen op vijanden.
Voor nu, de implementatie van de jacht()
toestand kan zichzelf gewoon van stapel storten, op die manier zullen de soldaten niet vastzitten in de jachtstaat:
openbare functie-jacht (): ongeldig // Laten we voorlopig gewoon de staat van de jacht () uit de hersenen halen. brain.popState ();
Merk op dat er geen informatie wordt doorgegeven aan de jacht
staat, zoals wie is de dichtstbijzijnde vijand. Die informatie moet worden verzameld door de jacht
staat zichzelf, omdat het bepaalt of het jacht
moet actief blijven of zichzelf uit de stapel verwijderen (het besturingselement terugzetten naar de volgen
staat).
Het resultaat tot nu toe is een groep soldaten die de leider volgen (merk op dat de soldaten niet zullen jagen omdat het jacht()
methode komt gewoon tevoorschijn):
Tip: elke staat zou verantwoordelijk moeten zijn voor het beëindigen van zijn bestaan door zichzelf uit de stapel te laten vallen.
De volgende staat die moet worden geïmplementeerd is jacht
, wat ervoor zorgt dat soldaten op elke vijand in de buurt jagen. De code voor jacht()
is:
openbare functie-jacht (): void var aNearestEnemy: Monster = getNearestEnemy (); // Hebben we een monster in de buurt? if (aNearestEnemy! = null) // Ja, dat doen we. Laten we berekenen hoe ver het is. var aDistance: Number = calculationDistance (aNearestEnemy, this); // Is het monster dichtbij genoeg om te schieten? als (afstand <= 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();
De staat begint met toewijzen aNearestEnemy
met de dichtstbijzijnde vijand. Als aNearestEnemy
is nul
het betekent dat er geen vijand in de buurt is, dus de staat moet eindigen. De oproep brain.popState ()
springt jacht
staat, schakel de soldaat naar de volgende staat in de stapel.
Als aNearestEnemy
is niet nul
, het betekent dat er een vijand moet worden opgejaagd en dat de staat actief moet blijven. Het jachtalgoritme is gebaseerd op de afstand tussen de soldaat en de vijand: als de afstand groter is dan 80, zoekt de soldaat de positie van de vijand; als de afstand kleiner is dan 80, zal de soldaat de vijand aankijken en schieten terwijl hij stilstaat.
Sinds jacht()
wordt elke game-update aangeroepen, als er een vijand in de buurt is, dan zal de soldaat die vijand zoeken of neerschieten. De beslissing om te bewegen of te schieten wordt dynamisch bepaald door de afstand tussen de soldaat en de vijand.
Het resultaat is een team van soldaten die de leider kunnen volgen en vijanden uit de buurt kunnen opsporen:
Elke keer dat een vijand wordt vermoord, kan er een voorwerp vallen. De soldaat moet het item verzamelen als het een goede is, of het item ontvluchten als het slecht is. Dat gedrag wordt vertegenwoordigd door twee toestanden in de eerder beschreven FSM:
collectItem
en Weglopen
staten. De collectItem
staat zal een soldaat laten arriveren bij het laten vallen item, terwijl de Weglopen
staat zorgt ervoor dat de soldaat de locatie van het slechte item ontvlucht. Beide staten zijn bijna identiek, het enige verschil is de aankomst- of vluchtmacht:
openbare functie runAway (): void var aItem: Item = getNearestItem (); if (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)); else brain.popState (); openbare functie collectItem (): void var aItem: Item = getNearestItem (); if (aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); else brain.popState (); persoonlijke functie getNearestItem (): item // hier wordt de code weergegeven om het dichtstbijzijnde item te krijgen
Hier komt een optimalisatie over de overgangen van pas. De code om over te gaan van de volgen
verklaren aan de collectItem
of de Weglopen
Staten is hetzelfde: controleer of er een item in de buurt is en druk vervolgens op de nieuwe status.
De status die moet worden gepusht, hangt af van het type van het item. Als gevolg hiervan is de overgang naar collectItem
of Weglopen
kan worden geïmplementeerd als een enkele methode, genaamd checkItemsNearby ()
:
private function checkItemsNearby (): void var aItem: Item = getNearestItem (); if (aItem! = null) brain.pushState (aItem.type == Item.BADKIT? runAway: collectItem);
Met deze methode wordt het dichtstbijzijnde item gecontroleerd. Als het een goede is, de collectItem
staat wordt in de hersenen geduwd; als het een slechte is, de Weglopen
toestand wordt ingedrukt. Als er geen item is om te verzamelen, doet de methode niets.
Die optimalisatie maakt het gebruik van checkItemsNearby ()
om de overgang van elke staat naar collectItem
of Weglopen
. Volgens de soldaat FSM bestaat die overgang in twee staten: volgen
en jacht
.
Hun implementatie kan enigszins worden gewijzigd om tegemoet te komen aan die nieuwe overgang:
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // krijg een verwijzing naar de leider addSteeringForce (boid.followLeader (aLeader)); // volg de leider // Controleer of er een item is om te verzamelen (of weg te rennen) checkItemsNearby (); // Is er een monster in de buurt? if (getNearestEnemy ()! = null) // Ja, dat is zo! Zoek het op! // Druk op de "jacht" -status. Het zorgt ervoor dat de soldaat stopt met het volgen van de leider en // begin met het jagen op het monster. brain.pushState (jagen); openbare functie-jacht (): void var aNearestEnemy: Monster = getNearestEnemy (); // Controleer of er een item is om te verzamelen (of weg te rennen) checkItemsNearby (); // Hebben we een monster in de buurt? if (aNearestEnemy! = null) // Ja, dat doen we. Laten we berekenen hoe ver het is. var aDistance: Number = calculationDistance (aNearestEnemy, this); // Is het monster dichtbij genoeg om te schieten? als (afstand <= 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();
Tijdens het volgen van de leider controleert een soldaat naar voorwerpen in de buurt. Bij het opsporen van een vijand, zal een soldaat ook naar voorwerpen in de buurt zoeken.
Het resultaat is de demo hieronder. Merk op dat een soldaat zal proberen een item te verzamelen of te ontwijken wanneer er een in de buurt is, ook al zijn er vijanden om te jagen en de leider om te volgen.
Een belangrijk aspect met betrekking tot toestanden en overgangen is de prioriteit onder hen. Afhankelijk van de regel waar een transitie wordt geplaatst binnen de implementatie van een staat, verandert de prioriteit van die overgang.
De ... gebruiken volgen
staat en de overgang gemaakt door checkItemsNearby ()
Bekijk bijvoorbeeld de volgende implementatie:
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // krijg een verwijzing naar de leider addSteeringForce (boid.followLeader (aLeader)); // volg de leider // Controleer of er een item is om te verzamelen (of weg te rennen) checkItemsNearby (); // Is er een monster in de buurt? if (getNearestEnemy ()! = null) // Ja, dat is zo! Zoek het op! // Druk op de "jacht" -status. Het zorgt ervoor dat de soldaat stopt met het volgen van de leider en // begin met het jagen op het monster. brain.pushState (jagen);
Die versie van volgen()
zal een soldaat doen overschakelen naar collectItem
of Weglopen
voor controleren of er een vijand in de buurt is. Als gevolg hiervan zal de soldaat een item verzamelen (of ontvluchten), zelfs als er vijanden in de buurt zijn die moeten worden opgejaagd door de jacht
staat.
Hier is nog een implementatie:
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // krijg een verwijzing naar de leider addSteeringForce (boid.followLeader (aLeader)); // volg de leider // Is er een monster in de buurt? if (getNearestEnemy ()! = null) // Ja, dat is zo! Zoek het op! // Druk op de "jacht" -status. Het zorgt ervoor dat de soldaat stopt met het volgen van de leider en // begin met het jagen op het monster. brain.pushState (jagen); else // Controleer of er een item is om te verzamelen (of weg te rennen) checkItemsNearby ();
Die versie van volgen()
zal een soldaat doen overschakelen naar collectItem
of Weglopen
enkel en alleen na hij ontdekt dat er geen vijanden zijn om te doden.
De huidige implementatie van volgen()
, jacht()
en collectItem ()
lijden aan prioriteitsproblemen. De soldaat zal proberen een item te verzamelen, zelfs als er belangrijker dingen zijn om te doen. Om dat op te lossen, zijn een paar aanpassingen nodig.
Betreffende de volgen
staat kan de code worden bijgewerkt naar:
(volg () met prioriteiten)
public function follow (): void var aLeader: Boid = Game.instance.boids [0]; // krijg een verwijzing naar de leider addSteeringForce (boid.followLeader (aLeader)); // volg de leider // Is er een monster in de buurt? if (getNearestEnemy ()! = null) // Ja, dat is zo! Zoek het op! // Druk op de "jacht" -status. Het zorgt ervoor dat de soldaat stopt met het volgen van de leider en // begin met het jagen op het monster. brain.pushState (jagen); else // Controleer of er een item is om te verzamelen (of weg te rennen) checkItemsNearby ();
De jacht
status moet worden gewijzigd in:
openbare functie-jacht (): void var aNearestEnemy: Monster = getNearestEnemy (); // Hebben we een monster in de buurt? if (aNearestEnemy! = null) // Ja, dat doen we. Laten we berekenen hoe ver het is. var aDistance: Number = calculationDistance (aNearestEnemy, this); // Is het monster dichtbij genoeg om te schieten? als (afstand <= 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();
eindelijk, de collectItem
status moet worden gewijzigd om eventuele plunderingen af te breken als er een vijand in de buurt is:
openbare functie collectItem (): void var aItem: Item = getNearestItem (); var aMonsterNearby: Boolean = getNearestEnemy ()! = null; if (! aMonsterNearby && aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); else brain.popState ();
Het resultaat van al deze veranderingen is de demo vanaf het begin van de tutorial:
In deze tutorial heb je geleerd een squadelpatroon te coderen waarbij een groep soldaten een leider zal volgen, jagen en nabij vijanden plunderen. De AI wordt geïmplementeerd met behulp van een stack-gebaseerde FSM gecombineerd met verschillende stuurgedragingen.
Zoals aangetoond, zijn eindige-toestandsmachines en stuurgedrag een krachtige combinatie en een geweldige match. Verspreiding van de logica over de FSM-staten, het is mogelijk om dynamisch te selecteren welke stuurkrachten op een karakter zullen werken, waardoor het mogelijk wordt om complexe AI-patronen te creëren.
Combineer het stuurgedrag dat je al kent met FSM's en creëer nieuwe en uitstekende patronen!