Tänk dig en spelplats där ett rum är trångt med AI-kontrollerade enheter. Av någon anledning måste de lämna rummet och passera genom en dörröppning. Istället för att få dem att gå över varandra i ett kaotiskt flöde, lär dem hur man vänligen lämnar sig medan de står i kö. Denna handledning presenterar kö styrning beteende med olika metoder för att göra en folkmassa rör sig medan de bildar rader av enheter.
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.
Köa, i samband med denna handledning är processen att stå i linje och bilda en rad tecken som tålmodigt väntar på att komma någonstans. Som den första i raden rör sig, följer resten, skapar ett mönster som ser ut som ett tåg som drar vagnar. När du väntar ska en karaktär aldrig lämna linjen.
För att illustrera köbeteendet och visa de olika implementeringarna är en demo med en "queueing scene" det bästa sättet att gå. Ett bra exempel är ett rum som är fullt med AI-kontrollerade enheter, alla försöker lämna rummet och passera genom dörröppningen:
Denna scen gjordes med hjälp av två tidigare beskrivna beteenden: sök och kollisionsundvikande.
Dörröppningen är gjord av två rektangulära hinder placerade sida vid sida med ett gap mellan dem (dörröppningen). Tecknen söker en punkt som ligger bakom det. När det är, placeras karaktärerna längst ner på skärmen.
Just nu, utan köbeteendet, ser scenen ut som en skara av vildar som kliver på varandras huvuden för att komma fram till destinationen. När vi är färdiga, lämnar publiken smidigt platsen och bildar rader.
Den första förmågan en karaktär måste få för att stå i linje är att ta reda på om det finns någon framför dem. Baserat på den informationen kan den bestämma huruvida man ska fortsätta eller sluta flytta.
Trots förekomsten av mer sofistikerade sätt att kontrollera grannar framåt använder jag en förenklad metod baserad på avståndet mellan en punkt och ett tecken. Detta tillvägagångssätt användes vid kollisionsundvikande beteende för att kontrollera före hinder:
En punkt som heter ett huvud
projiceras framför tecknet. Om avståndet mellan den punkten och ett grannskapskaraktär är mindre än eller lika med MAX_QUEUE_RADIUS
, det betyder att det finns någon framför och karaktären måste sluta röra sig.
De ett huvud
poäng beräknas enligt följande (pseudokod):
// Både qa och framåt är matrisvektorer qa = normalisera (hastighet) * MAX_QUEUE_AHEAD; framåt = qa + position;
Hastigheten, som också ger karaktärens riktning, normaliseras och skalas av MAX_QUEUE_AHEAD
att producera en ny vektor som heter qa
. När qa
läggs till i placera
vektor, resultatet är en punkt framför karaktären och ett avstånd av MAX_QUEUE_AHEAD
enheter bort från det.
Allt detta kan vara inslaget i getNeighborAhead ()
metod:
privat funktion getNeighborAhead (): Boid var i: int; var rätt: Boid = null; var qa: Vector3D = hastighet.klon (); qa.normalize (); qa.scaleBy (MAX_QUEUE_AHEAD); framåt = position.clone (). add (qa); för (i = 0; i < Game.instance.boids.length; i++) var neighbor :Boid = Game.instance.boids[i]; var d :Number = distance(ahead, neighbor.position); if (neighbour != this && d <= MAX_QUEUE_RADIUS) ret = neighbor; break; return ret;
Metoden kontrollerar avståndet mellan ett huvud
punkt och alla andra tecken, återvänder det första tecknet vars avstånd är mindre eller lika med MAX_QUEUE_AHEAD
. Om ingen karaktär hittas returnerar metoden null
.
Som med alla andra beteenden, queueing force beräknas med en metod som heter kö()
:
privat funktionskö (): Vector3D var granne: Boid = getNeighborAhead (); om (grann! = null) // TODO: vidta åtgärder eftersom grann är framåt returnera ny Vector3D (0, 0);
Resultatet av getNeighborAhead ()
i lagrad i variabeln granne
. Om grann! = null
det betyder att det finns någon framför annars är banan klar.
De kö()
, som alla andra beteendemetoder, måste återge en kraft som är styrkraften relaterad till själva metoden. kö()
kommer att returnera en kraft utan storlek för nu, så det kommer inte att ge några effekter.
De uppdatering()
Metoden för alla tecken i dörrplatsen, tills nu, är (pseudokod):
public function update (): void var dörröppning: Vector3D = getDoorwayPosition (); styrning = söka (dörröppning); // söka dörröppning styrning = styrning + kollisionAvoidance (); // undvika hinder styrning = styrning + kö (); // kö längs vägen styrning = trunkate (styrning, MAX_FORCE); styrning = styrning / massa; hastighet = trunkera (hastighet + styrning, MAX_SPEED); position = position + hastighet;
Eftersom kö()
returnerar en null kraft, karaktärerna fortsätter att röra sig utan att bilda rader. Det är dags att få dem att vidta några åtgärder när en granne upptäcks rätt framåt.
Styrningsbeteenden är baserade på krafter som ständigt förändras, så hela systemet blir mycket dynamiskt. Beroende på genomförandet, desto svårare blir det, desto hårdare blir det att bestämma och avbryta en specifik kraftvektor.
Genomförandet som används i denna styrningsbeteendeserie kompletterar alla krafter tillsammans. Som ett resultat av att avbryta en kraft måste den omräknas, inverteras och läggas till den nuvarande styrkraftsvektorn igen.
Det är ganska mycket vad som händer i ankomstbeteendet, där hastigheten avbryts för att göra tecknet slutar röra sig. Men vad händer när fler styrkor verkar tillsammans, som kollisionsundvikande, fly, och mer?
Följande avsnitt presenterar två idéer för att få ett teckenstopp att flytta. Den första använder en "hard stop" -metod som fungerar direkt på hastighetsvektorn, och ignorerar alla andra styrkor. Den andra använder en kraftvektor som heter broms
, att graciöst avbryta alla andra styrkor, så småningom att karaktären slutar röra sig.
Flera styrkor är baserade på karaktärens hastighetsvektor. Om den vektorn ändras kommer alla andra krafter att påverkas när de omberäknas. "Hard stop" -idén är ganska enkel: Om det finns ett tecken framåt, "krympar vi" hastighetsvektorn:
privat funktionskö (): Vector3D var granne: Boid = getNeighborAhead (); om (grann! = null) hastighet.scaleBy (0,3); returnera ny Vector3D (0, 0);
I koden ovan är hastighet
vektorn är skalad till 30%
av dess nuvarande storlek (längd) medan ett tecken är framåt. Till följd av detta reduceras rörelsen drastiskt, men kommer så småningom att återgå till sin normala storlek när karaktären som blockerar vägen rör sig.
Det är lättare att förstå genom att analysera hur rörelsen beräknas varje uppdatering:
hastighet = trunkera (hastighet + styrning, MAX_SPEED); position = position + hastighet;
Om hastighet
kraften fortsätter att krympa, det gör också styrning
tvinga, för det är baserat på hastighet
tvinga. Det skapar en ond cirkel som kommer att hamna med ett extremt lågt värde för hastighet
. Det är då karaktären slutar att röra sig.
När krympningsprocessen slutar, kommer varje speluppdatering att öka hastighet
vektor lite, som påverkar styrning
tvinga också. Så småningom kommer flera uppdateringar efter att ta med båda hastighet
och styrning
vektor tillbaka till sina normala storheter.
"Hard stop" -metoden ger följande resultat:
Även om detta resultat är ganska övertygande, känns det som ett "robot" resultat. En riktig folkmassa har vanligtvis inga tomma utrymmen mellan sina medlemmar.
Det andra sättet att stoppa rörelsen försöker skapa ett mindre "robot" resultat genom att avbryta alla aktiva styrkrafter med hjälp av a broms
tvinga:
privat funktionskö (): Vector3D var v: Vector3D = hastighet.klon (); var broms: Vector3D = ny Vector3D (); var granne: Boid = getNeighborAhead (); om (grann! = null) brake.x = -steering.x * 0.8; brake.y = -steering.y * 0,8; v.scaleBy (-1); broms = brake.add (v); returbroms
I stället för att skapa broms
kraft genom att beräkna och invertera var och en av de aktiva styrkrafterna, broms
beräknas baserat på strömmen styrning
vektor som rymmer alla styrkor som läggs till för tillfället:
De broms
kraften mottar båda dess x
och y
komponenter från styrning
kraft, men inverterad och med en skala av 0,8
. Det betyder att broms
har 80% av storleken på styrning
och pekar i motsatt riktning.
Tips: Använda styrning
tvinga direkt är farligt. Om kö()
är det första beteendet som ska tillämpas på en karaktär, den styrning
kraften kommer att vara "tom". Som en konsekvens, kö()
måste åberopas efter alla andra styrmetoder, så att den kan komma åt hela och sista styrning
tvinga.
De broms
kraft måste också avbryta karaktärens hastighet. Det är gjort genom att lägga till -hastighet
till broms
tvinga. Därefter är metoden kö()
kan returnera finalen broms
tvinga.
Resultatet av att använda bromskraften är följande:
Bromsningsläget ger ett mer naturligt resultat jämfört med den "robotic" gamla, eftersom alla tecken försöker fylla de tomma utrymmena. Det introducerar dock ett nytt problem: tecken överlappar varandra.
För att fixa det, kan bromsansättningen förbättras med en något modifierad version av "hard stop" -metoden:
privat funktionskö (): Vector3D var v: Vector3D = hastighet.klon (); var broms: Vector3D = ny Vector3D (); var granne: Boid = getNeighborAhead (); om (grann! = null) brake.x = -steering.x * 0.8; brake.y = -steering.y * 0,8; v.scaleBy (-1); broms = brake.add (v); om (avstånd (position, neighbor.position) <= MAX_QUEUE_RADIUS) velocity.scaleBy(0.3); return brake;
Ett nytt test används för att kontrollera närliggande grannar. Den här gången istället för att använda ett huvud
peka på att mäta avståndet, kontrollerar det nya testet avståndet mellan tecknen placera
vektor:
Det här nya testet kontrollerar om det finns några närliggande tecken inom MAX_QUEUE_RADIUS
radie, men nu är det centrerad vid placera
vektor. Om någon är inom räckhåll betyder det att omgivningen blir alltför trångt och karaktärerna börjar förmodligen överlappa varandra.
Överlappningen mildras genom att skala hastighet
vektor till 30% av dess nuvarande storlek varje uppdatering. Precis som i "hard stop" -metoden, krymper hastighet
vektor minskar rörelsen drastiskt.
Resultatet verkar mindre "robotiskt", men det är inte idealiskt, eftersom karaktärerna fortfarande överlappar vid dörröppningen:
Även om tecknen försöker nå dörren på ett övertygande sätt, fyller alla tomma utrymmen när banan blir smal, de kommer för nära varandra vid dörröppningen.
Detta kan lösas genom att lägga till en separationskraft:
privat funktionskö (): Vector3D var v: Vector3D = hastighet.klon (); var broms: Vector3D = ny Vector3D (); var granne: Boid = getNeighborAhead (); om (grann! = null) brake.x = -steering.x * 0.8; brake.y = -steering.y * 0,8; v.scaleBy (-1); broms = brake.add (v); broms = brake.add (separation ()); om (avstånd (position, neighbor.position) <= MAX_QUEUE_RADIUS) velocity.scaleBy(0.3); return brake;
Tidigare använd i ledarens följande beteende, adderade separeringskraften till broms
kraft kommer att göra karaktärer sluta röra sig samtidigt som de försöker hålla sig borta från varandra.
Resultatet är en övertygande publik som försöker nå dörren:
Kön beteende låter tecken stå i linje och vänta tålmodigt för att komma fram till destinationen. En gång i rad försöker ett tecken inte "fuska" av hopppositioner; Den kommer bara att flytta när tecknet precis framför det rör sig.
Dörrplatsen som användes i denna handledning presenterade hur mångsidigt och anpassningsbart detta beteende kan vara. Några förändringar ger olika resultat, vilket kan anpassas till en mängd olika situationer. Beteendet kan också kombineras med andra, till exempel kollisionsundvikande.
Jag hoppas att du gillade det här nya beteendet och börja använda det för att lägga till rörliga folkmassor till ditt spel!