În primele două tutoriale din această serie, am abordat subiecte de Rezoluție Impuls și Arhitectură Core. Acum este momentul să adăugăm câteva dintre atingerile finale ale motorului fizic 2D, bazat pe impulsuri.
Subiectele pe care le vom examina în acest articol sunt:
Am recomandat să citiți articolele precedente din serie înainte de a încerca să abordați această problemă. Unele informații cheie din articolele anterioare se bazează pe acest articol.
Notă: Deși acest tutorial este scris folosind C ++, ar trebui să puteți folosi aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.
Iată un demo rapid al a ceea ce facem noi în această parte:
Frecarea este o parte a rezoluției de coliziune. Fricțiunea aplică întotdeauna o forță asupra obiectelor în direcția opusă mișcării în care urmează să se deplaseze.
În viața reală, fricțiunea este o interacțiune incredibil de complexă între diferite substanțe și, pentru ao modela, se fac presupuneri și aproximări uriașe. Aceste presupuneri sunt implicate în matematică și sunt, de obicei, ceva de genul "fricțiunea poate fi aproximată de un singur vector" - în mod similar cu modul în care dinamica rigidă a corpului simulează interacțiunile din viața reală prin asimilarea corpurilor cu o densitate uniformă care nu se poate deforma.
Uitați-vă rapid la demo-ul video din primul articol din această serie:
Interacțiunile dintre cadavre sunt destul de interesante, iar bouncingul în timpul coliziunilor se simte realist. Cu toate acestea, odată ce obiectele pătrund pe platforma solidă, ele tocmai se împrăștie și se îndepărtează de marginile ecranului. Acest lucru se datorează lipsei simulării de frecare.
După cum ar trebui să reamintiți din primul articol din această serie, o anumită valoare, j
, reprezintă magnitudinea unui impuls necesar pentru a separa două elemente de penetrare în timpul unei coliziuni. Această magnitudine poate fi referită ca jnormal
sau jN
deoarece este folosit pentru a modifica viteza de-a lungul coliziunii normale.
Incorporarea unui răspuns la frecare presupune calcularea unei alte magnitudine, denumită în continuare jtangent
sau Jt
. Frecarea va fi modelată ca un impuls. Această magnitudine va modifica viteza unui obiect de-a lungul vectorului negativ tangent al coliziunii, sau cu alte cuvinte de-a lungul vectorului de frecare. În două dimensiuni, rezolvarea pentru acest vector de frecare este o problemă de rezolvat, dar în 3D problema devine mult mai complexă.
Frecarea este destul de simplă și putem folosi ecuația anterioară pentru j
, cu excepția faptului că vom înlocui toate cazurile normale n
cu un vector tangent T
.
\ [Ecuația 1: \\
j = \ frac - (1 + e) (V ^ B -V ^ A) \ cdot n)
\ frac 1 masa ^ A + \ frac 1 masa ^ B]
A inlocui n
cu T
:
\ [Ecuația 2: \\
j = \ frac - (1 + e) ((V ^ B -V ^ A) \ cdot t)
\ frac 1 masa ^ A + \ frac 1 masa ^ B]
Deși doar o singură instanță de n
a fost înlocuit cu T
în această ecuație, odată ce rotațiile sunt introduse, mai multe situații trebuie înlocuite în afară de cea singulară din numărul de numerar al ecuației 2.
Acum, problema de a calcula T
apare. Vectorul tangent este un vector perpendicular la coliziunea normală care se confruntă mai mult cu normalul. Acest lucru ar putea părea confuz - nu vă faceți griji, am o diagramă!
Mai jos puteți vedea vectorul tangent perpendicular pe cel normal. Vectorul tangent poate orienta spre stânga sau spre dreapta. La stânga ar fi "mai departe" de viteza relativă. Cu toate acestea, este definită ca perpendiculară pe cea normală care indică "mai mult spre" viteza relativă.
Așa cum am menționat mai înainte, fricțiunea va fi un vector orientat opus vectorului tangent. Aceasta înseamnă că direcția în care se poate aplica frecarea poate fi calculată direct, deoarece vectorul normal a fost găsit în timpul detecției coliziunii.
Cunoscând acest lucru, vectorul tangent este (unde n
este coliziunea normală):
\ [V ^ R = V ^ B -V ^ A \\
t = V ^ R - (V ^ R \ cdot n) * n \]
Tot ce este lăsat pentru a rezolva jt
, magnitudinea fricțiunii, este de a calcula valoarea direct utilizând ecuațiile de mai sus. Există câteva piese foarte dificile după ce se calculează această valoare care va fi acoperită în curând, deci nu este ultimul lucru necesar în rezolvarea coliziunilor noastre:
// se recalculează viteza relativă după impulsul normal // se aplică (impulsul din primul articol, acest cod vine // direct după aceea în aceeași funcție de rezolvare) Vec2 rv = VB - VA // Solvează pentru vectorul tangent Vec2 tangent = rv - Dot (rv, normal) * normal tangent.Normalize () // Rezolvați pentru mărimea de aplicat de-a lungul flotorului vectorului de frecare jt = -Dot (rv, t) jt = jt / (1 / MassA +
Codul de mai sus urmează în mod direct ecuația 2. Din nou, este important să constatăm că vectorul de fricțiune indică direcția opusă vectorului tangent și ca atare trebuie să aplicăm un semn negativ atunci când punctăm viteza relativă de-a lungul tangentei pentru a rezolva viteza relativă de-a lungul vectorului tangent. Acest semn negativ răstoarnă viteza tangentă și indică brusc în direcția în care frecarea trebuie aproximată ca.
Legea lui Coulomb este partea de simulare a fricțiunii cu care majoritatea programatorilor au probleme. Eu însumi trebuia să fac destul de mult să studiez pentru a descoperi modul corect de modelare a acesteia. Trucul este că legea lui Coulomb este o inegalitate.
Starea de frecare Coulomb:
\ [Ecuația 3: \\
F_f <= \mu F_n \]
Cu alte cuvinte, forța de frecare este întotdeauna mai mică sau egală cu forța normală înmulțită cu o anumită constantă μ
(a căror valoare depinde de materialele obiectelor).
Forța normală este doar vechea noastră j
magnitudinea înmulțită cu coliziunea normală. Deci dacă ne-am rezolvat jt
(reprezentând forța de frecare) este mai mică decât μ
ori forța normală, atunci ne putem folosi jt
magnitudinea ca frecare. Dacă nu, atunci trebuie să ne folosim timpul normal de forță μ
in schimb. Acest caz "altceva" este o formă de strângere a fricțiunii noastre sub o valoare maximă, maximul fiind timpul normal de forță μ
.
Întregul punct al legii lui Coulomb este de a efectua această procedură de strângere. Această prindere se dovedește a fi cea mai dificilă parte a simulării prin frecare pentru rezoluția bazată pe impuls pentru a găsi documentația oriunde - până acum, cel puțin! Cele mai multe hârtii albe pe care le-am putut găsi pe această temă fie au omis fricțiunea în întregime, fie au încetat scurt și au implementat proceduri de strângere necorespunzătoare (sau inexistente). Sperăm că până acum aveți o apreciere pentru înțelegerea faptului că obținerea acestei părți este importantă.
Înainte de a explica ceva, trebuie doar să scoateți clema de prindere într-o singură mișcare. Acest bloc de cod următor este exemplul de cod anterior cu procedeul de strângere terminat și aplicarea impulsului de frecare împreună:
// se recalculează viteza relativă după impulsul normal // se aplică (impulsul din primul articol, acest cod vine // direct după aceea în aceeași funcție de rezolvare) Vec2 rv = VB - VA // Solvează pentru vectorul tangent Vec2 tangent = rv - Dot (rv, normal) * normal tangent.Normalize () // Rezolvați ca mărimea să se aplice de-a lungul flotorului vectorului de frecare jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB) PythagoreanSolve = A ^ 2 + B ^ 2 = C ^ 2, rezolvarea pentru C dat A și B // Folosiți pentru aproximarea coeficienților de frecare ai fiecărui corp float mu = PythagoreanSolve (A-> staticFriction, B-> Valorile magnetului de frecare și crearea vectorului de impuls Vec2 frictionImpulse if (abs (jt) < j * mu) frictionImpulse = jt * t else dynamicFriction = PythagoreanSolve( A->(1 / A-> masa) * frictionImpulse B-> viteza + = (1 / B-> masa) * frictionImpulse
Am decis să folosesc această formulă pentru a rezolva coeficienții de frecare între două corpuri, date fiind un coeficient pentru fiecare organism:
\ [Ecuația 4: \\
Fricțiune = \ sqrt [] Fricțiune ^ 2_A + Fricțiune ^ 2_B \]
Am vazut ca altcineva a facut asta in propriul motor de fizica si mi-a placut rezultatul. O medie a celor două valori ar funcționa foarte bine pentru a scăpa de utilizarea rădăcinii pătrate. Într-adevăr, orice formă de alegere a coeficientului de frecare va funcționa; aceasta este exact ceea ce prefer. O altă opțiune ar fi folosirea unui tabel de căutare în care tipul fiecărui corp este folosit ca index într-o tabelă 2D.
Este important ca valoarea absolută a lui jt
este utilizată în comparație, deoarece comparația constă, în mod teoretic, în strângerea magnitudiilor brute sub un anumit prag. De cand j
este întotdeauna pozitivă, trebuie să fie inversată pentru a reprezenta un vector adecvat de frecare, în cazul în care se folosește frecare dinamică.
În ultimul fragment de cod au fost introduse fricțiuni statice și dinamice fără explicații! Voi dedica întreaga secțiune explicând diferența și necesitatea acestor două tipuri de valori.
Ceva interesant se întâmplă cu frecare: necesită o "energie de activare" pentru ca obiectele să înceapă să se miște când se odihnesc complet. Atunci când două obiecte se odihnesc unul pe celălalt în viața reală, este nevoie de o cantitate suficientă de energie pentru a împinge una și pentru ao face să se miște. Cu toate acestea, odată ce obțineți ceva de alunecare, este adesea mai ușor să îl mențineți alunecând de atunci.
Acest lucru se datorează modului în care funcționează frecarea la nivel microscopic. O altă imagine ajută aici:
După cum puteți vedea, micile deformări dintre suprafețe sunt într-adevăr principalul vinovat care creează frecarea în primul rând. Atunci când un obiect este în repaus pe altul, deformările microscopice se află între obiecte, interconectându-se. Acestea trebuie să fie rupte sau separate, pentru ca obiectele să alunece unul față de celălalt.
Avem nevoie de o modalitate de a modela acest lucru în cadrul motorului nostru. O soluție simplă este de a furniza fiecărui tip de material două valori de frecare: una pentru statică și una pentru dinamică.
Fricțiunea statică este utilizată pentru a ne fixa jt
magnitudine. Dacă rezolvată jt
magnitudinea este destul de scăzută (sub pragul nostru), atunci putem presupune că obiectul se află în repaus, sau aproape ca odihnă și să folosească întreaga jt
ca un impuls.
Pe flipside, dacă ne-am rezolvat jt
este peste prag, se poate presupune că obiectul a rupt deja "energia activării", iar într-o astfel de situație este folosit un impuls de frecare mai mic, care este reprezentat de un coeficient de frecare mai mic și de un calcul ușor de impuls diferit.
Presupunând că nu ați săriți nici o porțiune a secțiunii de frecare, bine făcută! Ați încheiat cea mai grea parte din întreaga serie (după părerea mea).
Scenă
clasa acționează ca un container pentru tot ceea ce implică un scenariu de simulare a fizicii. El apelează și utilizează rezultatele oricărei faze largi, conține toate corpurile rigide, execută verificări de coliziune și rezolvă apelurile. De asemenea, integrează toate obiectele live. Scena interfețează și cu utilizatorul (ca și în programatorul care utilizează motorul de fizică).
Iată un exemplu despre cum poate arăta o structură de scenă:
clasa Scene public: Scene (Vec2 gravitate, real dt); ~ Scene (); void SetGravity (Vec2 gravitate) void SetDT (real dt) Body * CreateBody (ShapeInterface * shape, BodyDef def) // Introduce un corp în scenă și inițializează corpul (calculează masa). // void InsertBody (Corpul corpului) // Sterge un corp din scena void RemoveBody (Corpul * corp) // Actualizeaza scena cu un singur timestep void Pasul (void) float GetDT (void) LinkedList * GetBodyList (void) Vec2 GetGravity (void) void QueryAABB (CallBackQuery cb, const AABB & aabb) void QueryPoint (CallBackQuery cb, const Point2 & punct) privat: float dt // Timestep în secunde float inv_dt // Inverse timestep în sceonds LinkedList corp_list uint32 body_count Vec2 gravitate bool debug_draw BroadPhase broadphase;
Nu este nimic deosebit de complex în ceea ce privește Scenă
clasă. Ideea este de a permite utilizatorului să adauge și să îndepărteze ușor corpurile rigide. BodyDef
este o structură care deține toate informațiile despre un corp rigid și poate fi utilizată pentru a permite utilizatorului să introducă valori ca un fel de structură de configurare.
Cealaltă funcție importantă este Etapa()
. Această funcție efectuează o singură rundă de verificări, rezoluție și integrare a coliziunilor. Acest lucru ar trebui să fie chemat din cadrul bucla de timestepping prezentată în al doilea articol din această serie.
Interogarea unui punct sau AABB presupune verificarea pentru a vedea care obiecte se ciocnesc cu un pointer sau AABB în interiorul scenei. Acest lucru facilitează logica legată de joc, pentru a vedea cum sunt plasate lucrurile în lume.
Avem nevoie de o modalitate ușoară de a alege ce funcție de coliziune ar trebui să fie numită, pe baza tipului a două obiecte diferite.
În C ++ există două modalități majore pe care le cunosc: dublu expediere și o masă de salt 2D. În propriile teste personale am găsit masa de 2D de salt la superioare, așa că voi intra în detaliu despre modul în care să implementez asta. Dacă intenționați să folosiți o altă limbă decât C sau C ++, sunt sigur că o serie de funcții sau obiecte functor pot fi construite în mod similar cu o tabelă de indicatori de funcții (ceea ce este un alt motiv pentru care am ales să vorbesc despre tabelele de salt mai degrabă decât alte opțiuni care sunt mai specifice pentru C ++).
O tabelă de salt în C sau C ++ este o tabelă cu indicatori de funcție. Indicii reprezentând nume sau constante arbitrare sunt folosite pentru a indexa în tabel și pentru a apela o funcție specifică. Utilizarea ar putea arăta cam așa pentru o masă de salt 1D:
enum Animal Iepurasul de rață Leu; const void (* vorbesc) (void) [] = RabbitTalk, DuckTalk, LionTalk,; // Apelați o funcție din tabel cu discuție de expediere virtuală 1D [Rabbit] () // apelează funcția RabbitTalk
Codul de mai sus imită de fapt ceea ce implementează limba C ++ apeluri funcționale virtuale și moștenire
. Cu toate acestea, C ++ implementează numai apeluri virtuale unice. O masă 2D poate fi construită manual.
Iată câteva psuedocode pentru o masă de salt 2D pentru a apela rutinele de coliziune:
collisionCallbackArray = AABBvsAABB AABBvsCircle CirclevsAABB CirclevsCircle // Apelați o rutină de coliziune pentru detectarea coliziunii între A și B // două colizoare fără a cunoaște tipul lor de coliziune exactă // tipul poate fi de tip AABB sau Circle collisionCallbackArray [A-> type] [B -> tip] (A, B)
Și acolo avem! Tipurile reale ale fiecărui colizor pot fi folosite pentru a indexa într-o matrice 2D și a alege o funcție pentru a rezolva coliziunea.
Rețineți, totuși AABBvsCircle
și CirclevsAABB
sunt aproape duplicate. Acest lucru este necesar! Normalul trebuie să fie răsturnat pentru una dintre aceste două funcții, și aceasta este singura diferență dintre ele. Acest lucru permite o rezoluție consecventă a coliziunii, indiferent de combinația de obiecte care trebuie rezolvată.
Până acum am acoperit o sumă imensă de subiecte în ceea ce privește înființarea unui motor rigid de fizică corporală, în întregime de la zero! Rezoluția de coliziune, fricțiunea și arhitectura motorului sunt toate subiectele care au fost acoperite până acum. Un motor de fizică complet reușit, potrivit pentru multe jocuri de dimensiuni de nivel de producție, poate fi construit cu cunoștințele prezentate în această serie până acum.
Privind spre viitor, intenționez să scriu un articol dedicat în întregime unei trăsături foarte dorite: rotație și orientare. Obiectele orientate sunt extrem de atractive pentru a viziona interacționa unul cu celălalt și sunt piesa finală pe care motorul nostru fizic personalizat o cere.
Rezoluția de rotație se dovedește a fi destul de simplă, deși detecția coliziunilor are un impact în complexitate. Mult noroc până data viitoare și vă rog să puneți întrebări sau să postați comentarii mai jos!