Er zijn verschillende manieren om een bepaald spel te maken. Meestal kiest een ontwikkelaar iets dat bij zijn vaardigheden past en gebruikt hij de technieken die hij al weet om het best mogelijke resultaat te produceren. Soms weten mensen nog niet dat ze een bepaalde techniek nodig hebben - misschien zelfs een gemakkelijkere en betere - simpelweg omdat ze al een manier weten om dat spel te creëren.
In deze serie tutorials leer je hoe je kunstmatige intelligentie kunt maken voor een hockeywedstrijd met behulp van een combinatie van technieken, zoals stuurgedrag, die ik eerder heb uitgelegd als concepten.
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.
Hockey is een leuke en populaire sport en het bevat, als een videogame, vele gamedev-onderwerpen, zoals bewegingspatronen, teamwerk (aanval, verdediging), kunstmatige intelligentie en tactiek. Een speelbaar hockeyspel is een prima manier om de combinatie van een aantal nuttige technieken te demonstreren.
Om de hockeymonteur te simuleren, met atleten die rennen en bewegen, is een uitdaging. Als de bewegingspatronen vooraf zijn gedefinieerd, zelfs met verschillende paden, wordt het spel voorspelbaar (en saai). Hoe kunnen we zo'n dynamische omgeving implementeren terwijl we toch de controle behouden over wat er gaande is? Het antwoord is: stuurgedrag gebruiken.
Stuurgedrag is gericht op het creëren van realistische bewegingspatronen met improvisatie-navigatie. Ze zijn gebaseerd op eenvoudige krachten die elke game-update combineren, dus ze zijn extreem dynamisch van aard. Dit maakt hen de perfecte keuze om iets zo complex en dynamisch te implementeren als een hockey- of voetbalspel.
Laten we omwille van de tijd en het lesgeven de omvang van het spel een beetje beperken. Onze hockeywedstrijd zal slechts een klein aantal van de originele regels van de sport volgen: in onze game zijn er geen strafschoppen en geen keepers, dus elke atleet kan zich rond de ijsbaan bewegen:
Hockeyspel met vereenvoudigde regels.Elk doel wordt vervangen door een kleine "muur" zonder net. Om te scoren, moet een team de puck (de schijf) verplaatsen om het aan een kant van het doel van de tegenstander te laten raken. Wanneer iemand scoort, zullen beide teams zich opnieuw organiseren en wordt de puck in het midden geplaatst; de wedstrijd wordt een paar seconden later opnieuw gestart.
Wat betreft de behandeling van de puck: als een atleet, zeg A, de puck heeft en wordt aangeraakt door een tegenstander, zeg B, dan wint B de puck en wordt A een paar seconden onbeweeglijk. Als de puck de baan verlaat, wordt deze meteen in het midden van de ijsbaan gelegd.
Ik zal de Flixel-game-engine gebruiken om voor het grafische deel van de code te zorgen. In de voorbeelden wordt de engine-code echter vereenvoudigd of weggelaten om de focus op het spel zelf te houden.
Laten we beginnen met de spelomgeving, die bestaat uit een ijsbaan, een aantal atleten en twee doelen. De ijsbaan is gemaakt van vier rechthoeken geplaatst rond het ijsgebied; deze rechthoeken zullen botsen met alles wat hen raakt, dus niets zal het ijsgebied verlaten.
Een atleet wordt beschreven door de Atleet
klasse:
openbare klasse Atleet private var mBoid: Boid; // bestuurt het stuurgedrag stuff private var mld: int; // een unieke identificatie voor de athelete publieke functie Atleet (thePosX: Number, thePosY: Number, theTotalMass: Number) mBoid = new Boid (thePosX, thePosY, theTotalMass); public function update (): void // Wis alle stuurkrachten mBoid.steering = null; // Wandel rond dwalenInTheRink (); // Update alle stuurspullen mBoid.update (); private function wanderInTheRink (): void var aRinkCenter: Vector3D = getRinkCenter (); // Als de afstand vanaf het midden groter is dan 80, verplaats // dan terug naar het midden, anders blijf dwalen. if (Utils.distance (this, aRinkCenter)> = 80) mBoid.steering = mBoid.steering + mBoid.seek (aRinkCenter); else mBoid.steering = mBoid.steering + mBoid.wander ();
Het eigendom mBoid
is een instantie van de Boid
klasse, een inkapseling van de wiskundige logica die wordt gebruikt in de reeks stuurgedragingen. De mBoid
instantie heeft, onder andere elementen, wiskundige vectoren die de huidige richting, stuurkracht en positie van de entiteit beschrijven.
De bijwerken()
methode in de Atleet
klasse wordt elke keer dat de game wordt bijgewerkt aangeroepen. Voorlopig wist het alleen maar actieve stuurkracht, voegt een dwaalkracht toe en roept uiteindelijk mBoid.update ()
. Het vorige commando actualiseert alle stuurgedragslogica die is ingekapseld in mBoid
, de atleet laten bewegen (met behulp van Euler-integratie).
De gameklasse, die verantwoordelijk is voor de game-loop, wordt gebeld PlayState
. Het heeft de ijsbaan, twee groepen atleten (één groep voor elk team) en twee doelen:
openbare klasse PlayState private var mAthletes: FlxGroup; privé var mRightGoal: Doel; private var mLeftGoal: Goal; public function create (): void // Hier wordt alles gemaakt en toegevoegd aan het scherm. override public function update (): void // Zorg dat de ijsbaan botst met atleten die botsen (mRink, mAthletes); // Zorg ervoor dat alle atleten binnen de ijsbaan blijven. applyRinkContraints (); private functie applyRinkContraints (): void // controleer of atleten zich binnen de ijsbaan // grenzen bevinden.
Ervan uitgaande dat een enkele atleet is toegevoegd aan de wedstrijd, is hieronder het resultaat van alles tot nu toe:
De atleet moet de muiscursor volgen, zodat de speler daadwerkelijk iets kan besturen. Omdat de muiscursor een positie op het scherm heeft, kan deze als bestemming voor het aankomstgedrag worden gebruikt.
Het aankomstgedrag zorgt ervoor dat een atleet de cursorpositie opzoekt, de snelheid soepel vertraagt wanneer hij de cursor nadert en uiteindelijk stopt.
In de Atleet
klasse, laten we de zwervende methode vervangen door het aankomstgedrag:
public class Athlete // (...) update openbare functie (): void // Wis alle stuurkrachten mBoid.steering = null; // De atleet wordt bestuurd door de speler, // dus volg gewoon de muiscursor. followMouseCursor (); // Update alle stuurspullen mBoid.update (); private functie followMouseCursor (): void var aMouse: Vector3D = getMouseCursorPosition (); mBoid.steering = mBoid.steering + mBoid.arrive (aMouse, 50);
Het resultaat is een atleet die de muiscursor kan gebruiken. Omdat de bewegingslogica is gebaseerd op stuurgedrag, navigeren de atleten op een overtuigende en soepele manier over de ijsbaan.
Gebruik de muisaanwijzer om de atleet te begeleiden in de onderstaande demo:
De puck wordt vertegenwoordigd door de klas puck
. De belangrijkste delen zijn de bijwerken()
methode en de mOwner
eigendom:
public class Puck public var velocity: Vector3D; openbare positie var: Vector3D; privé var mOwner: Athlete; // de atleet die momenteel de puck draagt. public function setOwner (theOwner: Athlete): void if (mOwner! = theOwner) mOwner = theOwner; velocity = null; public function update (): void public function get owner (): Athlete return mOwner;
Volgens dezelfde logica van de atleet, de puck's bijwerken()
methode wordt telkens opgeroepen wanneer de game wordt bijgewerkt. De mOwner
eigenschap bepaalt of de puck in het bezit is van een atleet. Als mOwner
is nul
, het betekent dat de puck "vrij" is en zich zal verplaatsen en uiteindelijk zal stuiteren op de ijsbaanwandelingen.
Als mOwner
is niet nul
, het betekent dat de puck wordt gedragen door een atleet. In dit geval negeert het eventuele botsingscontroles en wordt het krachtig voor de atleet geplaatst. Dit kan worden bereikt met behulp van de atleet snelheid
vector, die ook overeenkomt met de richting van de atleet:
De verder
vector is een kopie van de atleet snelheid
vector, dus wijzen ze in dezelfde richting. Na verder
is genormaliseerd, het kan worden geschaald met elke waarde, zeg maar, 30
-om te bepalen hoe ver de puck voor de atleet zal worden geplaatst.
Eindelijk de puck's positie
ontvangt de atleet positie
toegevoegd aan verder
, de puck op de gewenste positie plaatsen.
Hieronder staat de code voor dat alles:
public class Puck // (...) private function placeAheadOfOwner (): void var ahead: Vector3D = mOwner.boid.velocity.clone (); vooruit = normaliseren (vooruit) * 30; positie = mOwner.boid.position + ahead; override public function update (): void if (mOwner! = null) placeAheadOfOwner (); // (...)
In de PlayState
klasse, er is een botstest om te controleren of de puck een atleet overlapt. Als dat zo is, wordt de atleet die net de puck heeft geraakt, de nieuwe eigenaar. Het resultaat is een puck die "vasthoudt" aan de atleet. Leid in de onderstaande demo de atleet om de puck in het midden van de ijsbaan aan te raken om dit in actie te zien:
Het is tijd om de puck te laten bewegen als gevolg van geraakt te zijn door de stick. Ongeacht de atleet die de puck draagt, is alles wat nodig is om een treffer van de stick te simuleren het berekenen van een nieuwe snelheidsvector. Die nieuwe snelheid verplaatst de puck naar de gewenste bestemming.
Een snelheidsvector kan worden gegenereerd door de ene positievector van een andere; de nieuw gegenereerde vector zal dan van de ene positie naar de andere gaan. Dat is precies wat nodig is om de nieuwe snelheidsvector van de puck na een treffer te berekenen:
Berekening van de nieuwe snelheid van de puck na een slag van de stick.In de bovenstaande afbeelding is het bestemmingspunt de muiscursor. De huidige positie van de puck kan als beginpunt worden gebruikt, terwijl het punt waarop de puck zich zou moeten bevinden nadat deze door de stick is geraakt, als eindpunt kan worden gebruikt.
De pseudo-code hieronder toont de implementatie van goFromStickHit ()
, een methode in de puck
klasse die de logica implementeert die wordt geïllustreerd in de bovenstaande afbeelding:
public class Puck // (...) publieke functie goFromStickHit (theAthlete: Athlete, theDestination: Vector3D, theSpeed: Number = 160): void // Plaats de puck voor op de eigenaar om onverwachte trajecten te voorkomen // (bijv. puck botst tegen atleet die het net heeft geraakt) placeAheadOfOwner (); // Markeer de puck als gratis (geen eigenaar) setOwner (null); // Bereken de nieuwe velocity van de puck var new_velocity: Vector3D = theDestination - position; velocity = normalize (new_velocity) * theSpeed;
De new_velocity
vector gaat van de huidige positie van de puck naar het doel (de bestemming
). Daarna wordt het genormaliseerd en geschaald door de snelheid
, die de magnitude (lengte) van definieert new_velocity
. Die bewerking definieert met andere woorden hoe snel de puck van zijn huidige positie naar de bestemming zal bewegen. Eindelijk de puck's snelheid
vector wordt vervangen door new_velocity
.
In de PlayState
klasse, de goFromStichHit ()
methode wordt aangeroepen telkens wanneer de speler op het scherm klikt. Wanneer dit gebeurt, wordt de muiscursor gebruikt als de bestemming voor de treffer. Het resultaat is te zien in deze demo:
Tot nu toe hebben we slechts een enkele atleet rond de ijsbaan laten bewegen. Naarmate er meer atleten aan toegevoegd worden, moet de AI geïmplementeerd worden om al deze atleten eruit te laten zien alsof ze in leven en denken zijn.
Om dat te bereiken, gebruiken we een op stack gebaseerde eindige toestandsmachine (stack-gebaseerde FSM, kortweg). Zoals eerder beschreven, zijn FSM's veelzijdig en handig voor het implementeren van AI in games.
Voor ons hockeyspel, een eigenschap met de naam mBrain
zal worden toegevoegd aan de Atleet
klasse:
openbare klas Atleet // (...) private var mBrain: StackFSM; // bestuurt het AI-materiaal public function Athlete (thePosX: Number, thePosY: Number, theTotalMass: Number) // (...) mBrain = new StackFSM (); // (...)
Deze eigenschap is een instantie van StackFSM
, een klasse die eerder is gebruikt in de FSM-zelfstudie. Het gebruikt een stack om de AI-status van een entiteit te regelen. Elke staat wordt beschreven als een methode; wanneer een toestand in de stapel wordt gedrukt, wordt deze de actief methode en wordt aangeroepen tijdens elke game-update.
Elke staat voert een specifieke taak uit, zoals het verplaatsen van de atleet naar de puck. Elke staat is verantwoordelijk voor het beëindigen van zichzelf, wat betekent dat het verantwoordelijk is voor het zichzelf uit de stapel knallen.
De atleet kan nu worden bestuurd door de speler of door de AI, dus de bijwerken()
methode in de Atleet
klasse moet worden aangepast om die situatie te controleren:
public class Athlete // (...) update openbare functie (): void // Wis alle stuurkrachten mBoid.steering = null; if (mControlledByAI) // De atleet wordt bestuurd door de AI. Werk de hersenen bij (FSM) en // blijf uit de buurt van ijsbaanwanden. mBrain.update (); else // De atleet wordt bestuurd door de speler, dus volg // de muiscursor. followMouseCursor (); // Update alle stuurspullen mBoid.update ();
Als de AI actief is, mBrain
is bijgewerkt, die de momenteel actieve statusmethode oproept, waardoor de atleet zich overeenkomstig gedraagt. Als de speler de controle heeft, mBrain
wordt helemaal genegeerd en de atleet beweegt zoals geleid door de speler.
Wat betreft de toestanden om in de hersenen te duwen: voorlopig laten we er slechts twee van implementeren. De ene staat laat een atleet zich voorbereiden op een wedstrijd; bij het voorbereiden van de wedstrijd, zal een atleet zich verplaatsen naar zijn positie op de ijsbaan en stilstaan, starend naar de puck. De andere staat zorgt ervoor dat de atleet eenvoudigweg stilstaat en naar de puck staart.
In de volgende secties zullen we deze toestanden implementeren.
Als de atleet zich in de nutteloos
staat, hij zal stoppen met bewegen en naar de puck staren. Deze status wordt gebruikt wanneer de atleet al op de baan staat en wacht tot er iets gebeurt, zoals het begin van de wedstrijd.
De staat wordt gecodeerd in de Atleet
klas, onder de inactief ()
methode:
openbare klasse Atleet // (...) openbare functie Atleet (thePosX: Number, thePosY: Number, theTotalMass: Number, theTeam: FlxGroup) // (...) // Vertel het brein dat de huidige status 'inactief' is mBrain.pushState (idle); private function idle (): void var aPuck: Puck = getPuck (); stopAndlookAt (aPuck.position); private function stopAndlookAt (thePoint: Vector3D): void mBoid.velocity = thePoint - mBoid.position; mBoid.velocity = normaliseren (mBoid.velocity) * 0,01;
Aangezien deze methode niet vanzelf uit de stapel komt, blijft deze voor altijd actief. In de toekomst zal deze toestand vanzelf opduiken om ruimte te maken voor andere staten, zoals aanval, maar voor nu doet het de slag.
De stopAndStareAt ()
methode volgt hetzelfde principe dat wordt gebruikt om de snelheid van de puck na een slag te berekenen. Een vector van de positie van de atleet tot de positie van de puck wordt berekend door thePoint - mBoid.position
en gebruikt als de nieuwe snelheidsvector van de atleet.
Die nieuwe snelheidsvector zal de atleet naar de puck brengen. Om ervoor te zorgen dat de atleet niet beweegt, wordt de vector geschaald door 0.01
, "krimpend" zijn lengte tot bijna nul. Het zorgt ervoor dat de atleet stopt met bewegen, maar zorgt ervoor dat hij naar de puck staart.
Als de atleet zich in de prepareForMatch
staat, zal hij zich naar zijn oorspronkelijke positie bewegen, soepel stoppen daar. De beginpositie is waar de atleet moet zijn vlak voordat de wedstrijd begint. Aangezien de atleet moet stoppen op de bestemming, kan het aankomstgedrag opnieuw worden gebruikt:
public class Athlete // (...) private var mInitialPosition: Vector3D; // de positie op de baan waarop de sporter openbare functie moet krijgen Atleet (thePosX: Number, thePosY: Number, theTotalMass: Number, theTeam: FlxGroup) // (...) mInitialPosition = new Vector3D (thePosX, thePosY); // Vertel de hersenen dat de huidige status 'inactief' is mBrain.pushState (idle); private function prepareForMatch (): void mBoid.steering = mBoid.steering + mBoid.arrive (mInitialPosition, 80); // Zit ik op de beginpositie? if (afstand (mBoid.position, mInitialPosition) <= 5) // I'm in position, time to stare at the puck. mBrain.popState(); mBrain.pushState(idle); // (… )
De staat gebruikt het aankomstgedrag om de atleet naar de beginpositie te brengen. Als de afstand tussen de atleet en zijn beginpositie kleiner is dan 5
, het betekent dat de sporter op de gewenste plaats is aangekomen. Wanneer dit gebeurt, prepareForMatch
springt zichzelf uit de stapel en duwt nutteloos
, waardoor het de nieuwe actieve staat wordt.
Hieronder is het resultaat van het gebruik van een stack-gebaseerde FSM om meerdere atleten te besturen. druk op G
om ze op willekeurige posities op de baan te plaatsen, druk op de prepareForMatch
staat:
Deze tutorial presenteerde de basis voor het implementeren van een hockeywedstrijd met behulp van stuurgedrag en stack-gebaseerde eindige-toestandsmachines. Door een combinatie van deze concepten te gebruiken, kan een atleet zich op de ijsbaan bewegen en de muiscursor volgen. De atleet kan ook de puck naar een bestemming slaan.
Met behulp van twee staten en een stack-gebaseerde FSM kunnen de atleten zich opnieuw organiseren en naar hun positie op de ijsbaan gaan, zich voorbereiden op de wedstrijd.
In de volgende tutorial leer je hoe je de atleten kunt laten aanvallen door de puck naar het doel te dragen terwijl je tegenstanders vermijdt.