Styrningsbeteenden är bra för att skapa realistiska rörelsemönster, men de är ännu större om du enkelt kan styra, använda och kombinera dem. I denna handledning diskuterar jag och täcker genomförandet av en rörelsearkitekt för alla våra tidigare diskuterade beteenden.
Notera: Även om denna handledning skrivs med AS3 och Flash, borde du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst. Du måste ha en grundläggande förståelse för matematiska vektorer.
Som tidigare diskuterat ger varje styrande beteende en resulterande kraft (kallad "styrkraft") som läggs till hastighetsvektorn. Riktningen och storleken på den kraften kommer att driva karaktären, så att den rör sig enligt ett mönster (sök, fly, vandra osv.). Den allmänna beräkningen är:
styrning = sök (); // detta kan vara vilket beteende som helst styrning = stympa (styrning, max_force) styrning = styrning / masshastighet = trunkera (hastighet + styrning, max_speed) position = läge + hastighet
Eftersom styrkraften är en vektor kan den läggas till någon annan vektor (precis som hastigheten). Den verkliga "magiken" ligger emellertid i det faktum att du kan lägga till flera styrkor tillsammans - det är lika enkelt som:
styrning = inget (); // nollvektorn, som betyder "nollstyrka" styrning = styrning + sök (); styrning = styrning + flykt (); (...) styrning = stympa (styrning, max_force) styrning = styrning / masshastighet = trunkera (hastighet + styrning, max_hastighet) position = läge + hastighet
De kombinerade styrkrafterna kommer att resultera i en vektor som representerar Allt dessa krafter. I kodstycket ovan kommer den resulterande styrkraften att göra tecknet söka något medan på samma gång det kommer att fly någonting annan.
Kolla nedan några exempel på styrstyrkor kombinerade för att skapa en enda styrkraft:
Kombinationen av styrkrafterna kommer att skapa extremt komplexa rörelsemönster enkelt. Föreställ dig hur svårt det är att skriva kod för att få en karaktär att söka något, men samtidigt undvika ett specifikt område utan att använda vektorer och krafter?
Det skulle kräva beräkning av avstånd, områden, vägar, grafer och liknande. Om sakerna rör sig, måste alla dessa beräkningar upprepas då och då, eftersom miljön förändras ständigt.
Med styrningsbeteenden är alla krafter dynamisk. De är avsedda att beräknas varje speluppdatering, så de kommer naturligt och smidigt att reagera på miljöförändringar.
Demon nedan visar fartyg som kommer att söka muspekaren, men kommer att fly i mitten av skärmen, båda samtidigt:
För att kunna använda flera styrningsbeteenden samtidigt på ett enkelt och enkelt sätt, a rörelsechef kommer till hands. Tanken är att skapa en "svart låda" som kan anslutas till en befintlig enhet, vilket gör det möjligt att utföra dessa beteenden.
Chefen har en hänvisning till den enhet som den är ansluten till ("värden"). Chefen kommer att tillhandahålla värden med en uppsättning metoder, till exempel söka()
och fly()
. Varje gång sådana metoder åberopas uppdaterar chefen sina interna egenskaper för att producera en styrkraftsvektor.
När chefen behandlar alla inbjudningar lägger den resulterande styrkraften till värdens hastighetsvektor. Det kommer att ändra värdens hastighetsvektorstyrka och riktning i enlighet med det aktiva beteendet.
Figuren nedan visar arkitekturen:
Chefen har en uppsättning metoder, var och en representerar ett distinkt beteende. Varje beteende måste levereras med olika bitar av extern information för att kunna fungera.
Sökbeteendet behöver till exempel en punkt i det utrymme som används för att beräkna styrkraften mot den platsen. eftersträva behöver flera bitar av information från sitt mål, såsom nuvarande position och hastighet. En punkt i rymden kan uttryckas som en förekomst av Punkt
eller Vector2D
, Bägge ganska vanliga klasser i alla ramar.
Målet som används i förföljande beteende kan dock vara någonting. För att göra rörelsehanteraren generisk nog måste den få ett mål som oberoende av sin typ kan svara på några "frågor", som "Vad är din nuvarande hastighet?". Med vissa principer för objektorienterad programmering kan det uppnås med gränssnitt.
Antag gränssnittet IBoid
beskriver en enhet som kan hanteras av rörelsehanteraren, vilken klass i spelet som helst kan använda styrningsbeteenden, så länge det implementerar IBoid
. Det gränssnittet har följande struktur:
offentligt gränssnitt IBoid funktion getVelocity (): Vector3D; funktion getMaxVelocity (): Number; funktion getPosition (): Vector3D; funktion getMass (): Number;
Nu när chefen kan interagera med alla spel enheter på ett generiskt sätt kan dess grundläggande struktur skapas. Chefen består av två egenskaper (den resulterande styrkraften och värdreferensen) och en uppsättning offentliga metoder, en för varje beteende:
offentlig klass SteeringManager offentlig varning: Vector3D; allmän värd: IBoid; // Konstruktörens offentliga funktion SteeringManager (värd: IBoid) this.host = host; this.steering = ny Vector3D (0, 0); // Public API (en metod för varje beteende) public function seek (mål: Vector3D, slowingRadius: Number = 20): void public function fly (mål: Vector3D): void public function wander (): void Offentlig funktion undviker (mål: IBoid): void public function pursuit (mål: IBoid): void // Uppdateringsmetoden. // Bör kallas efter att alla beteenden har åberopats public function update (): void // Återställ den interna styrkraften. public function reset (): void // Den interna API-funktionen doSeek (mål: Vector3D, slowingRadius: Number = 0): Vector3D privatfunktion doFlee (mål: Vector3D): Vector3D privatfunktion doWander () Vector3D privat funktion doEvade (mål: IBoid): Vector3D privatfunktion doPursuit (mål: IBoid): Vector3D
När chefen är instanserad måste den få en hänvisning till den värd som den är ansluten till. Det kommer att tillåta chefen att ändra värdhastighetsvektorn enligt de aktiva beteenden.
Varje beteende är representerat av två metoder, en offentlig och en privat. Använda sök som ett exempel:
public function seek (mål: Vector3D, slowingRadius: Number = 20): void privat funktion doSeek (mål: Vector3D, slowingRadius: Number = 0): Vector3D
Allmänheten söka()
kommer att åberopas för att berätta för chefen att tillämpa det specifika beteendet. Metoden har inget returvärde och dess parametrar är relaterade till beteendet i sig, till exempel en punkt i rymden. Under huven är den privata metoden doSeek ()
kommer att åberopas och dess returvärde, den beräknade styrkraften för det specifika beteendet, läggs till chefen styrning
fast egendom.
Följande kod visar genomförandet av sökningen:
// Publiceringsmetoden. // Mottar ett mål att söka och en slowingRadius (används för att utföra ankomsten). offentliga funktionssök (mål: Vector3D, slowingRadius: Number = 20): void steering.incrementBy (doSeek (target, slowingRadius)); // Den verkliga implementeringen av sökningen (med ankomstkod ingår) privat funktion doSeek (mål: Vector3D, slowingRadius: Number = 0): Vector3D var force: Vector3D; var avstånd: Number; önskad = target.subtract (host.getPosition ()); avstånd = önskad längd; desired.normalize (); om (avstånd <= slowingRadius) desired.scaleBy(host.getMaxVelocity() * distance/slowingRadius); else desired.scaleBy(host.getMaxVelocity()); force = desired.subtract(host.getVelocity()); return force;
Alla andra beteendemetoder implementeras på ett mycket liknande sätt. De jakt()
Metoden kommer till exempel att se ut så här:
Offentlig funktion strävan (mål: IBoid): void steering.incrementBy (doPursuit (mål)); privat funktion doPursuit (mål: IBoid): Vector3D distance = target.getPosition (). subtrahera (host.getPosition ()); var updatesNeeded: Number = distance.length / host.getMaxVelocity (); var tv: Vector3D = target.getVelocity (). klon (); tv.scaleBy (updatesNeeded); targetFuturePosition = target.getPosition (). klon (). lägg till (tv); returnera doSeek (targetFuturePosition);
Med hjälp av koden från tidigare tutorials är allt du behöver göra för att anpassa dem i form av beteende()
och doBehavior ()
, så att de kan läggas till rörelsehanteraren.
Varje gång ett beteende sätts på, läggs den resulterande kraft som den producerar till chefen styrning
fast egendom. Till följd av detta kommer egendomen att ackumulera alla styrkor.
När alla beteenden har åberopats måste chefen tillämpa den nuvarande styrkraften på värdens hastighet, så den kommer att röra sig enligt aktiva beteenden. Det utförs i uppdatering()
Metoden för rörelsehanteraren:
public function update (): void varhastighet: Vector3D = host.getVelocity (); var position: Vector3D = host.getPosition (); trunkera (styrning, MAX_FORCE); steering.scaleBy (1 / host.getMass ()); velocity.incrementBy (styrning); trunkera (hastighet, värd.getMaxVelocity ()); position.incrementBy (hastighet);
Metoden ovan måste anropas av värden (eller någon annan spelhet) efter det att alla beteenden har åberopats, annars kommer värden aldrig att ändra sin hastighetsvektor för att matcha aktiva beteenden.
Låt oss anta en klass som heter Byte
bör flyttas med hjälp av styrningsbeteende, men för närvarande har det ingen styrningskod eller rörelsehanteraren. Dess struktur kommer att se ut så här:
public class Prey public var position: Vector3D; allmän varhastighet: Vector3D; allmän var massa: Nummer; allmän funktion Prey (posX: Nummer, posY: Nummer, totalMassa: Nummer) position = ny Vector3D (posX, posY); hastighet = ny vektor3D (-1, -2); massa = totalMass; x = position.x; y = position.y; public function update (): void hastighet.normalize (); velocity.scaleBy (MAX_VELOCITY); hastighet.scaleBy (1 / massa); trunkera (hastighet, MAX_VELOCITY); position = position.add (hastighet); x = position.x; y = position.y;
Med hjälp av den här strukturen kan klassförekomsterna flytta med hjälp av Euler-integrationen, precis som den första demoen i sökhandledningen. För att göra det möjligt att använda chefen behöver den en egenskap som refererar till rörelsebehandlaren och den måste genomföra IBoid
gränssnitt:
public class Prey implementerar IBoid public var position: Vector3D; allmän varhastighet: Vector3D; allmän var massa: Nummer; allmän styrning: styrman allmän funktion Prey (posX: Nummer, posY: Nummer, totalMassa: Nummer) position = ny Vector3D (posX, posY); hastighet = ny vektor3D (-1, -2); massa = totalMass; styrning = ny styrmanager (detta); x = position.x; y = position.y; public function update (): void hastighet.normalize (); velocity.scaleBy (MAX_VELOCITY); hastighet.scaleBy (1 / massa); trunkera (hastighet, MAX_VELOCITY); position = position.add (hastighet); x = position.x; y = position.y; // Nedan är de metoder gränssnittet IBoid kräver. allmän funktion getVelocity (): Vector3D returhastighet; allmän funktion getMaxVelocity (): Number return 3; allmän funktion getPosition (): Vector3D returposition; offentlig funktion getMass (): Number return mass;
De uppdatering()
Metoden måste ändras så att chefen också kan uppdateras:
public function update (): void // Gör bytet vandra runt ... steering.wander (); // Uppdatera chefen så att den kommer att ändra rovhastighetsvektorn. // Chefen kommer också att utföra Euler-intergration, ändra // positioneringsvektorn. steering.update (); // Efter att chefen har uppdaterat sina interna strukturer, är allt vi måste // göra uppdatera vår position enligt "position" -vektorn. x = position.x; y = position.y;
Alla beteenden kan användas samtidigt, så länge som alla metodsamtal görs före chefen uppdatering()
invokation, vilken tillämpar den ackumulerade styrkraften på värdens hastighetsvektor.
Koden nedan visar en annan version av Prey's uppdatering()
metod, men den här gången kommer den att söka en position i kartan och undviker ett annat tecken (båda samtidigt):
public function update (): void var destination: Vector3D = getDestination (); // platsen att söka varjare: IBoid = getHunter (); // få den enhet som jagar oss // Sök destinationen och undvik jägaren (samtidigt!) steering.seek (destination); steering.evade (jägare); // Uppdatera chefen så att den kommer att ändra rovhastighetsvektorn. // Chefen kommer också att utföra Euler-intergration, ändra // positioneringsvektorn. steering.update (); // Efter att chefen har uppdaterat sina interna strukturer, är allt vi måste // göra uppdatera vår position enligt "position" -vektorn. x = position.x; y = position.y;
Demon nedan visar ett komplext rörelsemönster där flera beteenden kombineras. Det finns två typer av tecken på scenen: Jägare och den Byte.
Jägaren vill bedriva ett byte om det blir tillräckligt nära det kommer att fortsätta så länge som uthållighetstillförseln varar; När det går ut ur uthållighet avbryts jakten och jägaren kommer vandra tills det återhämtar sina uthållighetsnivåer.
Här är jägaren uppdatering()
metod:
public function update (): void if (vila && stamina ++> = MAX_STAMINA) vila = false; om (byte! = null &&! vila) steering.pursuit (byte); uthållighet - = 2; om (uthållighet <= 0) prey = null; resting = true; else steering.wander(); prey = getClosestPrey(position); steering.update(); x = position.x; y = position.y;
Bytet kommer vandra obegränsat. Om jägaren blir för nära, kommer det att undgå. Om muspekaren är nära och det finns ingen jägare runt, kommer bytet söka muspekaren.
Här är Prey's uppdatering()
metod:
public function update (): void varavstånd: Number = Vector3D.distance (position, Game.mouse); hunter = getHunterWithinRange (position); om (jägare! = null) steering.evade (jägare); om (avstånd <= 300 && hunter == null) steering.seek(Game.mouse, 30); else if(hunter == null) steering.wander(); steering.update(); x = position.x; y = position.y;
Det slutliga resultatet (grå är att vandra, grönt är söka, orange är förföljelse, rött är bortfallet):
En rörelsehanterare är mycket användbar för att styra flera styrningsbeteenden samtidigt. Kombinationen av sådana beteenden kan producera väldigt komplexa rörelsemönster, så att en spelande enhet kan söka en sak samtidigt som den undviker en annan.
Jag hoppas att du gillade ledningssystemet som diskuterats och implementeras i den här handledningen och använder den i dina spel. Tack för att du läste! Glöm inte att hålla dig uppdaterad genom att följa oss på Twitter, Facebook eller Google+.