Tenk deg en spillscene hvor et rom er overfylt med AI-kontrollerte enheter. Av en eller annen grunn må de forlate rommet og passere gjennom en døråpning. I stedet for å få dem til å gå over hverandre i en kaotisk strøm, lær dem hvordan de høflig skal gå mens de står i kø. Denne opplæringen presenterer kø styringsadferd med ulike tilnærminger for å få folkemengden til å bevege seg mens de danner rader av enheter.
Merk: Selv om denne opplæringen er skrevet med AS3 og Flash, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø. Du må ha en grunnleggende forståelse av matematiske vektorer.
kø, i sammenheng med denne opplæringen, er prosessen med å stå i kø, og danner en rekke tegn som tålmodig venter på å komme et sted. Som den første i linjen beveger seg, følger resten, og skaper et mønster som ser ut som et tog som trekker vogner. Når du venter, bør et tegn aldri forlate linjen.
For å illustrere køenes adferd og vise de forskjellige implementasjonene, er en demonstrasjon med en "køer scene" den beste måten å gå. Et godt eksempel er et rom som er overfylt med AI-kontrollerte enheter, alle prøver å forlate rommet og passere gjennom døråpningen:
Denne scenen ble laget ved hjelp av to tidligere beskrevne atferd: søk og kollisjonssvikt.
Døråpningen er laget av to rektangulære hindringer plassert side ved side med et gap mellom dem (døråpningen). Tegnene søker et poeng bak det. Når det er, plasseres tegnene nederst på skjermen.
Akkurat nå, uten køenes adferd, ser scenen ut som en horde av vildmenn som trapper på hverandres hoder for å komme fram til destinasjonen. Når vi er ferdige, går publikum jevnt ut av plassen og danner rader.
Den første evnen en karakter må oppnå for å stå i kø er å finne ut om det er noen foran dem. Basert på denne informasjonen, kan den bestemme om å fortsette eller å slutte å flytte.
Til tross for eksistensen av mer sofistikerte måter å sjekke naboer fremover, bruker jeg en forenklet metode basert på avstanden mellom et punkt og et tegn. Denne tilnærmingen ble brukt i kollisjonssvindingsadferdigheten for å sjekke for hindringer fremover:
Et poeng som heter fremover
er projisert foran tegnet. Hvis avstanden mellom det punktet og et nabostatus er mindre enn eller lik MAX_QUEUE_RADIUS
, det betyr at det er noen foran og tegnet må slutte å bevege seg.
De fremover
punkt beregnes som følger (pseudo-kode):
// Både qa og fremover er mattevektorer qa = normaliser (hastighet) * MAX_QUEUE_AHEAD; fremover = qa + posisjon;
Hastigheten, som også gir karakterens retning, blir normalisert og skalert av MAX_QUEUE_AHEAD
å produsere en ny vektor kalt qa
. Når qa
er lagt til i stilling
vektor, resultatet er et punkt foran karakteren, og en avstand på MAX_QUEUE_AHEAD
enheter vekk fra det.
Alt dette kan pakkes inn i getNeighborAhead ()
metode:
privat funksjon getNeighborAhead (): Boid var jeg: int; var rett: Boid = null; var qa: Vector3D = hastighet.klone (); qa.normalize (); qa.scaleBy (MAX_QUEUE_AHEAD); fremover = position.clone (). add (qa); for (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 kontrollerer avstanden mellom fremover
punkt og alle andre tegn, returnerer det første tegnet hvis avstand er mindre eller lik MAX_QUEUE_AHEAD
. Hvis ingen tegn er funnet, returnerer metoden null
.
Som med alle andre oppføringer, kjørekraft beregnes ved hjelp av en metode som heter kø()
:
privatfunksjonskø (): Vector3D var nabo: Boid = getNeighborAhead (); hvis (nabo! = null) // TODO: ta tiltak fordi naboen er foran returnere ny Vector3D (0, 0);
Resultatet av getNeighborAhead ()
i lagret i variabelen nabo
. Hvis nabo! = null
det betyr at det er noen foran Ellers er banen klar.
De kø()
, som alle andre atferdsmetoder, må returnere en kraft som er styrkraften knyttet til metoden selv. kø()
vil returnere en kraft uten styrke for nå, så det vil ikke gi noen effekt.
De Oppdater()
Metode for alle tegn i døråpningsscenen, til nå, er (pseudokode):
offentlig funksjon oppdatering (): void var døråpning: Vector3D = getDoorwayPosition (); styring = søk (døråpning); // Søk dørstyring = styring + kollisjonAvoidance (); // unngå hindringer styring = styring + kø (); // kø langs veien styring = avkorte (styring, MAX_FORCE); styring = styring / masse; hastighet = trunkere (hastighet + styring, MAX_SPEED); posisjon = posisjon + hastighet;
Siden kø()
Returnerer null null, tegnene fortsetter å bevege seg uten å danne rader. Det er på tide å få dem til å gjøre noe når en nabo blir oppdaget rett fram.
Styringsadferdene er basert på krefter som hele tiden endres, slik at hele systemet blir veldig dynamisk. Avhengig av implementeringen, jo flere krefter som er involvert, jo vanskeligere blir det å finne og avbryte en bestemt kraftvektor.
Implementeringen som brukes i denne styringsadferanseserien, legger til alle krefter sammen. Som en konsekvens, for å avbryte en kraft, må den beregnes, omvendt og legges til den gjeldende styrkraftsvektoren igjen.
Det er ganske mye som skjer i ankomstadferansen, hvor hastigheten avbrytes for å få tegnet til å stoppe. Men hva skjer når flere krefter opptrer sammen, som for eksempel kollisjon, unngå, flee og mer?
Følgende avsnitt presenterer to ideer for å få et tegnstopp å flytte. Den første bruker en "hard stop" tilnærming som virker direkte på hastighetsvektoren, og ignorerer alle andre styringskrefter. Den andre bruker en kraftvektor som heter brems
, for å fornøyd avbryte alle andre styringskrefter, slik at karakteren slutter å bevege seg.
Flere styringskrefter er basert på karakterens hastighetsvektor. Hvis den vektoren endres, vil alle andre krefter bli påvirket når de omberegnes. Den "harde stopp" -ideen er ganske enkel: Hvis det er et tegn foran, "krymper vi" hastighetsvektoren:
privatfunksjonskø (): Vector3D var nabo: Boid = getNeighborAhead (); hvis (nabo! = null) hastighet.scaleBy (0.3); returner ny Vector3D (0, 0);
I koden ovenfor er hastighet
vektoren er skalert til 30%
av dens nåværende størrelse (lengde) mens et tegn er foran. Som følge av dette er bevegelsen drastisk redusert, men det vil etter hvert komme tilbake til sin normale størrelse når tegnet som blokkerer veien beveger seg.
Det er lettere å forstå ved å analysere hvordan bevegelsen beregnes hver oppdatering:
hastighet = trunkere (hastighet + styring, MAX_SPEED); posisjon = posisjon + hastighet;
Hvis hastighet
kraften fortsetter å krympe, det gjør også styring
tvinge, fordi den er basert på hastighet
makt. Det skaper en ond syklus som vil ende opp med en ekstremt lav verdi for hastighet
. Det er da karakteren slutter å bevege seg.
Når krympeprosessen slutter, vil hver spilloppdatering øke hastighet
vektor litt, påvirker styring
tvinge også. Til slutt flere oppdateringer etter vil bringe begge hastighet
og styring
vektor tilbake til sine normale størrelser.
Den "harde stopp" -tilnærmingen gir følgende resultat:
Selv om dette resultatet er ganske overbevisende, føles det som et "robot" utfall. En ekte mengde har vanligvis ingen tomme mellomrom mellom medlemmene sine.
Den andre tilnærmingen for å stoppe bevegelsen forsøker å skape et mindre "robot" resultat ved å kansellere alle aktive styrkrefter ved hjelp av a brems
makt:
privatfunksjonskø (): Vector3D var v: Vector3D = hastighet.klone (); var brems: Vector3D = ny Vector3D (); var nabo: Boid = getNeighborAhead (); hvis (nabo! = null) brake.x = -steering.x * 0,8; brake.y = -steering.y * 0,8; v.scaleBy (-1); bremse = brake.add (v); returbrems;
Istedenfor å skape brems
tvinge ved å regne ut og reversere hver enkelt av de aktive styrkrefter, brems
beregnes basert på gjeldende styring
vektor, som holder alle styringskrefter lagt til i øyeblikket:
De brems
kraft mottar begge sine x
og y
komponenter fra styring
tvinge, men invertert og med en skala av 0.8
. Det betyr at brems
har 80% av størrelsen på styring
og peker i motsatt retning.
Tips: Bruker styring
tvinge direkte er farlig. Hvis kø()
er den første oppførselen som skal brukes på et tegn, den styring
kraft vil være "tom". Som en konsekvens, kø()
må påberopes etter alle andre styringsmetoder, slik at den kan få tilgang til det komplette og endelige styring
makt.
De brems
kraft må også avbryte karakterens hastighet. Det er gjort ved å legge til -hastighet
til brems
makt. Etter det, metoden kø()
kan returnere finalen brems
makt.
Resultatet av å bruke bremsekraften er følgende:
Bremsemetoden gir et mer naturlig resultat sammenlignet med den "robotic" gamle, fordi alle tegn prøver å fylle de tomme mellomrom. Det introduserer imidlertid et nytt problem: tegn overlapper.
For å fikse det, kan bremsemetoden forbedres med en litt modifisert versjon av "hard stop" -tilnærmingen:
privatfunksjonskø (): Vector3D var v: Vector3D = hastighet.klone (); var brems: Vector3D = ny Vector3D (); var nabo: Boid = getNeighborAhead (); hvis (nabo! = null) brake.x = -steering.x * 0,8; brake.y = -steering.y * 0,8; v.scaleBy (-1); bremse = brake.add (v); hvis (avstand (posisjon, naboposisjon) <= MAX_QUEUE_RADIUS) velocity.scaleBy(0.3); return brake;
En ny test brukes til å sjekke nærliggende naboer. Denne gangen i stedet for å bruke fremover
pek på å måle avstanden, den nye testen kontrollerer avstanden mellom tegnene stilling
vektor:
Denne nye testen kontrollerer om det er noen nærliggende tegn innenfor MAX_QUEUE_RADIUS
radius, men nå er det sentrert på stilling
vektor. Hvis noen er innenfor rekkevidde, betyr det at omgivelsene blir for overfylte, og tegn begynner sannsynligvis å overlappe.
Overlappingen reduseres ved å skalere hastighet
vektor til 30% av sin nåværende størrelse hver oppdatering. Akkurat som i "hard stop" -tilnærmingen, krymper hastighet
vektor reduserer bevegelsen drastisk.
Resultatet virker mindre "robotic", men det er ikke ideelt, siden tegnene fortsatt overlapper ved døråpningen:
Selv om tegnene prøver å nå døråpningen på en overbevisende måte, fyller alle tomme mellomrom når banen blir smal, de kommer for nær hverandre ved døråpningen.
Dette kan løses ved å legge til en separasjonskraft:
privatfunksjonskø (): Vector3D var v: Vector3D = hastighet.klone (); var brems: Vector3D = ny Vector3D (); var nabo: Boid = getNeighborAhead (); hvis (nabo! = null) brake.x = -steering.x * 0,8; brake.y = -steering.y * 0,8; v.scaleBy (-1); bremse = brake.add (v); bremse = brake.add (separasjon ()); hvis (avstand (posisjon, naboposisjon) <= MAX_QUEUE_RADIUS) velocity.scaleBy(0.3); return brake;
Tidligere brukt i lederens oppførsel, separasjonskraften ble lagt til brems
kraft vil få tegnene til å stoppe samtidig som de prøver å holde seg vekk fra hverandre.
Resultatet er en overbevisende mengde som prøver å nå døråpningen:
Køenes adferd tillater at tegn står i kø og venter tålmodig for å komme fram til destinasjonen. En gang i kø, vil et tegn ikke forsøke å "jukse" ved hoppeposisjoner; Den beveger seg bare når tegnet rett foran det beveger seg.
Døråpningsscenen som ble brukt i denne opplæringen, presenterte hvor allsidig og tilpassbar denne oppførselen kan være. Noen få endringer gir forskjellige resultater, som kan finjusteres til en rekke situasjoner. Oppførselen kan også kombineres med andre, for eksempel kollisjonssvikt.
Jeg håper du likte denne nye oppførselen og begynner å bruke den til å legge til bevegelige folkemengder i spillet ditt!