Imaginați-vă o scenă de joc în care o cameră este aglomerată cu entități controlate de AI. Din anumite motive, trebuie să părăsească camera și să treacă printr-o ușă. În loc să-i faci să se plimbe unul pe celălalt într-un flux haotic, învață-i cum să plece politicos în timp ce stau pe linie. Acest tutorial prezintă coadă comportament de conducere cu abordări diferite pentru a face o mulțime să se miște în timp ce formează rânduri de entități.
Notă: Deși acest tutorial este scris folosind AS3 și Flash, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului. Trebuie să aveți o înțelegere de bază a vectorilor de matematică.
Cozile, în contextul acestui tutorial, este procesul de a sta în linie, formând un rând de personaje care așteaptă cu răbdare să ajungă undeva. Pe măsură ce primul se mișcă, restul urmează, creând un model care arată ca un tren care trage vagoane. Când așteptați, un personaj nu trebuie să părăsească linia.
Pentru a ilustra comportamentul coada de așteptare și pentru a arăta diferitele implementări, demo-ul cu o "scenă de așteptare" este cel mai bun mod de a merge. Un exemplu bun este o cameră aglomerată cu entități controlate de AI, toate încercând să părăsească camera și să treacă prin ușă:
Această scenă a fost făcută folosind două comportamente descrise anterior: evitarea căutării și coliziunii.
Usa este formata din doua obstacole dreptunghiulare pozitionate unul langa altul, cu un gol intre ele (usa). Personajele caută un punct situat în spatele lor. Când sunt acolo, caracterele sunt repoziționate în partea de jos a ecranului.
Chiar acum, fără comportamentul la coadă, scena arată ca o hoardă de sălbatici care se deplasează pe capetele celorlalți pentru a ajunge la destinație. Când terminăm, mulțimea va părăsi fără probleme locul, formând rânduri.
Prima abilitate pe care un personaj trebuie să o obțină pentru a rămâne în linie este să afle dacă există cineva înaintea lor. Pe baza acestor informații, poate decide dacă să continue sau să înceteze mișcarea.
În ciuda existenței unor modalități mai sofisticate de a verifica vecinii înainte, voi folosi o metodă simplificată bazată pe distanța dintre un punct și un caracter. Această abordare a fost utilizată în comportamentul de evitare a coliziunii pentru a verifica obstacolele care urmează înainte:
Un punct numit înainte
este proiectat în fața personajului. Dacă distanța dintre acel punct și un caracter vecin este mai mică sau egală cu MAX_QUEUE_RADIUS
, înseamnă că există cineva înainte și caracterul trebuie să se oprească în mișcare.
înainte
punct se calculează după cum urmează (pseudo-cod):
// Atât qa cât și înainte sunt vectorii de matematică qa = normalizează (viteza) * MAX_QUEUE_AHEAD; înainte = qa + poziție;
Viteza, care dă și direcția personajului, este normalizată și mărită MAX_QUEUE_AHEAD
pentru a produce un nou vector numit qa
. Cand qa
este adăugat la poziţie
vector, rezultatul este un punct înaintea caracterului și o distanță de MAX_QUEUE_AHEAD
unități distanță de acesta.
Toate acestea pot fi înfășurate în getNeighborAhead ()
metodă:
funcția privată getNeighborAhead (): Boid var i: int; var ret: Boid = null; var qa: Vector3D = viteza.clone (); qa.normalize (); qa.scaleBy (MAX_QUEUE_AHEAD); înainte = poziția.clone (). adăugați (qa); pentru (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;
Metoda verifică distanța dintre înainte
punct și toate celelalte caractere, returnând primul personaj a cărui distanță este mai mică sau egală cu MAX_QUEUE_AHEAD
. Dacă nu se găsește niciun caracter, metoda se întoarce nul
.
Ca și în cazul tuturor celorlalte comportamente, coada de forță se calculează printr-o metodă numită coadă()
:
coadă de funcții private (): Vector3D var vecin: Boid = getNeighborAhead (); dacă (vecin! = null) // TODO: ia măsuri deoarece vecinul este în față returnează Vector3D nou (0, 0);
Rezultatul getNeighborAhead ()
în stocate în variabilă vecin
. Dacă vecin! = nul
înseamnă că există cineva înainte; în caz contrar, calea este clară.
coadă()
, ca toate celelalte metode de comportament, trebuie să returneze o forță care este forța de direcție legată de metoda în sine. coadă()
va reveni pentru moment o forță fără magnitudine, deci nu va produce efecte.
Actualizați()
metoda tuturor caracterelor pe scena ușii, până acum, este (pseudo-cod):
funcția publică (): void var doorway: Vector3D = getDoorwayPosition (); direcție = căuta (ușă); // căutați direcția de intrare a ușii = direcție + coliziuneAvoidance (); // evita obstacolele direcție = direcție + coadă (); // coadă de-a lungul direcției = trunchiu (direcție, MAX_FORCE); direcție = direcție / masă; velocity = truncate (viteza + direcție, MAX_SPEED); pozitie = pozitie + viteza;
De cand coadă()
returnează o forță nulă, personajele vor continua să se miște fără să formeze rânduri. Este timpul sa-i facem sa actioneze atunci cand un vecin este detectat chiar inainte.
Condițiile de conducere se bazează pe forțe care se schimbă constant, astfel încât întregul sistem devine foarte dinamic. În funcție de implementare, cu cât mai multe forțe sunt implicate, cu atât mai greu devine identificarea și anularea unui vector de forță specific.
Implementarea utilizată în această serie de comportament de direcție adaugă toate forțele împreună. În consecință, pentru a anula o forță, aceasta trebuie recalculată, inversată și adăugată din nou la vectorul forței de direcție curent.
Exact asta se întâmplă în comportamentul de sosire, unde viteza este anulată pentru a opri mișcarea personajului. Dar ceea ce se întâmplă atunci când acționează mai multe forțe, cum ar fi evitarea coliziunilor, fugi și multe altele?
Următoarele secțiuni prezintă două idei pentru a face un caracter să se oprească. Primul utilizează o abordare "oprire tare" care acționează direct asupra vectorului de viteză, ignorând toate celelalte forțe de direcție. Cel de-al doilea utilizează un vector de forță, numit frână
, pentru a anula grație toate celelalte forțe de direcție, eventual făcând caracterul să se oprească în mișcare.
Mai multe forțe de direcție se bazează pe vectorul vitezei caracterului. Dacă acel vector se modifică, toate celelalte forțe vor fi afectate atunci când sunt recalculate. Ideea de "oprire tare" este destul de simplă: dacă există un caracter înainte, noi "strângem" vectorul de viteză:
coadă de funcții private (): Vector3D var vecin: Boid = getNeighborAhead (); dacă (vecin! = null) velocity.scaleBy (0.3); returnați Vector3D (0, 0);
În codul de mai sus, viteză
vectorul este redus la 30%
din magnitudinea sa actuală (lungime) în timp ce un personaj este înaintea lui. În consecință, mișcarea este redusă drastic, dar în cele din urmă va reveni la magnitudinea normală când caracterul care blochează modul în care se mișcă.
Acest lucru este mai ușor de înțeles prin analizarea modului în care se calculează mișcarea fiecare actualizare:
velocity = truncate (viteza + direcție, MAX_SPEED); pozitie = pozitie + viteza;
În cazul în care viteză
forța continuă să se micșoreze, la fel direcție
vigoare, deoarece se bazează pe viteză
forta. Creează un ciclu vicios care va ajunge la o valoare extrem de scăzută viteză
. Atunci caracterul se oprește în mișcare.
Când procesul de micșorare se încheie, fiecare actualizare de joc va crește viteză
vector un pic, care afectează direcție
și forța. În cele din urmă mai multe actualizări după ce vor aduce ambele viteză
și direcție
vector înapoi la magnitudinea lor normală.
Abordarea "hard stop" produce următorul rezultat:
Chiar dacă acest rezultat este destul de convingător, se simte ca un rezultat "robotic". O mulțime adevărată nu are de obicei spații goale între membrii lor.
A doua abordare pentru oprirea mișcării încearcă să creeze un rezultat mai puțin "robotic" prin anularea tuturor forțelor de direcție active folosind a frână
forta:
funcția privată (): Vector3D var v: Vector3D = viteza.clone (); var frână: Vector3D = Vector3D () nou; var vecin: Boid = getNeighborAhead (); dacă (vecin! = null) brake.x = -steering.x * 0.8; brake.y = -steering.y * 0,8; v.scaleBy (-1); frâna = frâna.add (v); frână de întoarcere;
În loc de a crea frână
prin recalcularea și inversarea fiecărei forțe de direcție active, frână
se calculează pe baza curentului direcție
vector, care deține toate forțele de direcție adăugate la moment:
frână
forța primește ambele sale X
și y
componente din direcție
forță, dar inversată și cu o scară de 0,8
. Înseamnă că frână
are 80% din magnitudinea lui direcție
și puncte în direcția opusă.
Bacsis: Utilizarea direcție
forța directă este periculoasă. Dacă coadă()
este primul comportament care trebuie aplicat unui personaj direcție
Forța va fi "goală". În consecință, coadă()
trebuie să fie invocată după toate celelalte metode de direcție, astfel încât să poată accesa lista completă și finală direcție
forta.
frână
Forța trebuie, de asemenea, să anuleze viteza personajului. Asta se face prin adăugarea -viteză
la frână
forta. După aceasta, metoda coadă()
poate returna finala frână
forta.
Rezultatul utilizării forței de frânare este următorul:
Abordarea de frânare produce un rezultat mai natural comparativ cu cel vechi "robotic", deoarece toate personajele încearcă să umple spațiile goale. Cu toate acestea, introduce o nouă problemă: caracterele se suprapun.
Pentru a remedia această situație, abordarea la frânare poate fi îmbunătățită printr-o versiune puțin modificată a abordării "oprire tare":
funcția privată (): Vector3D var v: Vector3D = viteza.clone (); var frână: Vector3D = Vector3D () nou; var vecin: Boid = getNeighborAhead (); dacă (vecin! = null) brake.x = -steering.x * 0.8; brake.y = -steering.y * 0,8; v.scaleBy (-1); frâna = frâna.add (v); dacă (distanța (poziția, vec <= MAX_QUEUE_RADIUS) velocity.scaleBy(0.3); return brake;
Un nou test este folosit pentru a verifica vecinii din apropiere. De data aceasta, în loc să utilizați înainte
pentru a măsura distanța, noul test verifică distanța dintre caractere poziţie
vector:
Acest nou test verifică dacă există caractere în apropiere în interiorul MAX_QUEUE_RADIUS
raza, dar acum este centrat pe poziţie
vector. Dacă cineva se află în raza de acțiune, înseamnă că zona înconjurătoare devine prea aglomerată, iar personajele probabil încep să se suprapună.
Suprapunerea este atenuată prin scalarea viteză
vector la 30% din magnitudinea sa actuală la fiecare actualizare. La fel ca în abordarea "opririi tari", înrăutățirea viteză
vector drastic reduce mișcarea.
Rezultatul pare mai puțin "robotic", dar nu este ideal, deoarece personajele se suprapun încă la ușă:
Chiar dacă personajele încearcă să ajungă la ușă într-un mod convingător, umplând toate spațiile goale atunci când calea devine îngustă, se apropie prea mult una de alta la ușă.
Acest lucru poate fi rezolvat prin adăugarea unei forțe de separare:
funcția privată (): Vector3D var v: Vector3D = viteza.clone (); var frână: Vector3D = Vector3D () nou; var vecin: Boid = getNeighborAhead (); dacă (vecin! = null) brake.x = -steering.x * 0.8; brake.y = -steering.y * 0,8; v.scaleBy (-1); frâna = frâna.add (v); frâna = frâna.add (separare ()); dacă (distanța (poziția, vec <= MAX_QUEUE_RADIUS) velocity.scaleBy(0.3); return brake;
Anterior folosit în conducător după comportament, forța de separare a adăugat la frână
Forța va face ca personajele să se oprească în același timp în timp ce încearcă să stea departe unul de celălalt.
Rezultatul este o mulțime convingătoare care încearcă să ajungă la ușă:
Comportamentul coada de așteptare permite ca personajele să stea în linie și așteaptă cu răbdare să ajungă la destinație. Odată în linie, un personaj nu va încerca să "trișeze" prin poziții de sărituri; se va muta numai atunci când personajul din fața sa se mișcă.
Scena de intrare folosită în acest tutorial a prezentat cât de versatil și tweakable acest comportament poate fi. Câteva modificări produc rezultate diferite, care pot fi ajustate într-o mare varietate de situații. Comportamentul poate fi, de asemenea, combinat cu alții, cum ar fi evitarea coliziunilor.
Sper că ți-a plăcut acest nou comportament și ai început să îl folosești pentru a adăuga mulțimi în mișcare în jocul tău!