Förstå styrningsbeteenden kö

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 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.


Introduktion

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:


Boids lämnar rummet och passerar genom dörröppningen utan köbeteendet. Klicka för att visa styrkor.

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.


Ser framåt

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:


Test för grannar med hjälp av framåtriktningen.

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.


Skapa köningsmetoden

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.


Några ord om att stoppa rörelsen

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.


Stoppande rörelse: "Hard Stop"

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:


Köbeteenden med "hard stop" -metoden. Klicka för att visa styrkor.

Ä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.


Stoppande rörelse: bromsstyrka

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:


Representation av bromskraften.

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:


Köbeteende med bromskraften. Klicka för att visa styrkor.

Överlåtande karaktärers överlappning

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:


Kontrollera närliggande grannar inom MAX_QUEUE_RADIUS-radien centrerade vid positionen istället för framåtpunkten.

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:


Köbeteenden med "hårt stopp" och bromskraft kombinerad. Klicka för att visa styrkor.

Lägga till separering

Ä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öbeteenden med "hårt stopp", bromsstyrka och separation kombinerat. Klicka för att visa styrkor.

Slutsats

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!