Dinamica dinamică a corpului vizează simularea obiectelor deformabile realiste. Îl vom folosi aici pentru a simula o perdea de pânză și un set de ragdoll cu care puteți interacționa și arunca în jurul ecranului. Va fi rapid, stabil și suficient de simplu pentru a face cu matematica la nivel de liceu.
Notă: Deși acest tutorial este scris în Procesarea și compilat cu Java, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.
În această demonstrație, puteți vedea o perdea mare (prezentând simulația țesăturii) și un număr de păpușari (care demonstrează simularea ragdoll):
Puteți încerca și tu demo-ul. Faceți clic și trageți pentru a interacționa, apăsați pe 'R' pentru resetare și apăsați pe 'G' pentru a comuta gravitatea.
Blocurile de bază ale jocului nostru sunt Point. Pentru a evita ambiguitatea, o vom numi PointMass
. Detaliile sunt în numele: este un punct în spațiu și reprezintă o cantitate de masă.
Cea mai de bază mod de a implementa fizica pentru acest punct este de a "transmite" viteza sa într-un fel.
x = x + velx y = y + velY
Nu putem să presupunem că jocul nostru se va desfășura cu aceeași viteză tot timpul. S-ar putea să ruleze la 15 cadre pe secundă pentru unii utilizatori, dar la 60 pentru alții. Cel mai bine este să țineți cont de ratele de cadre ale tuturor intervalelor, care pot fi făcute utilizând un interval de timp.
x = x + velX * timeExpusă y = y + velY * timeElapsed
În acest fel, dacă un cadru va dura mai mult să scadă pentru o persoană decât pentru alta, jocul va continua să ruleze la aceeași viteză. Pentru un motor fizic, cu toate acestea, acest lucru este incredibil de instabil.
Imaginați-vă dacă jocul dvs. se blochează pentru o secundă sau două. Motorul ar compensa prea mult pentru asta și va mișca PointMass
dincolo de mai multe pereți și obiecte pe care altfel le-ar fi detectat o coliziune. Astfel, nu numai că detecția coliziunilor ar fi afectată, ci și metoda de rezolvare a constrângerilor pe care o vom folosi.
Cum putem avea stabilitatea primei ecuații, x = x + velx
, cu consistența celei de-a doua ecuații, x = x + velX * timeElapsed
? Dacă, poate, am putea combina cele două?
Exact asta vom face. Imaginează-ne timpul scurs
a fost 30
. Am putea face exact acelasi lucru ca si cea de-a doua ecuatie, dar cu o acuratete si rezolutie mai mare, prin chemare x = x + (velx * 5)
de sase ori.
elapsedTime = lastTime - curentTime lastTime = currentTime // reinițializează lastTime // adaugă timpul care nu a putut fi utilizat ultimul cadru elapsedTime + = leftOverTime // diviza-l în bucăți de 16 ms timesteps = floor (elapsedTime / 16) // timpul de stocare nu am putut folosi pentru următorul cadru. leftOverTime = elapsedTime - timesteps * 16 pentru (i = 0; i < timesteps; i++) x = x + velX * 16 y = y + velY * 16 // solve constraints, look for collisions, etc.
Algoritmul utilizează aici un timestep fixat mai mare decât unul. Acesta găsește timpul scurs, îl împarte în "bucăți" de dimensiuni fixe și împinge timpul rămas până la următorul cadru. Rulam simulația puțin câte puțin pentru fiecare bucată pe care a trecut timpul nostru scurs.
Am selectat 16 pentru mărimea temporală, pentru a simula fizica ca și cum ar fi rulat la aproximativ 60 de cadre pe secundă. Conversie de la timpul scurs
la cadre pe secundă se poate face cu unele matematici: 1 secundă / eșuatTimeInSecunde
.
1s / (16ms / 1000s) = 62,5fps
, astfel încât un timestep de 16 ms este echivalent cu 62,5 cadre pe secundă.
Constrângerile sunt restricții și reguli adăugate la simulare, ghidând unde PointMasses poate și nu poate merge.
Ele pot fi simple ca această constrângere de frontieră, pentru a împiedica PointMasses să se deplaseze de pe marginea din stânga a ecranului:
dacă (x < 0) x = 0 if (velX < 0) velX = velX * -1
Adăugarea constrângerii pentru marginea dreaptă a ecranului se face în mod similar:
dacă (x> lățimea) x = lățimea dacă (velX> 0) velX = velX * -1
A face acest lucru pentru axa y este o chestiune de schimbare de la x la y.
Având dreptul de constrângeri poate duce la interacțiuni foarte frumoase și captivante. Constrângerile pot deveni, de asemenea, extrem de complexe. Încercați să vă imaginați simularea unui coș vibrator de cereale, care nu se intersectează cu niciun fel de boabe sau un braț robotic cu 100 de articulații sau chiar ceva simplu ca o stivă de cutii. Procesul tipic implică găsirea de puncte de coliziune, găsirea momentului exact al coliziunii și apoi găsirea forței sau a impulsului potrivit pentru a se aplica fiecărui organism pentru a preveni coliziunea.
Înțelegerea complexității unui set de constrângeri poate fi dificilă, iar apoi rezolvarea acelor constrângeri, in timp real este și mai dificilă. Ce vom face este să simplificăm în mod semnificativ rezolvarea constrângerilor.
Un matematician și programator numit Thomas Jakobsen a explorat câteva moduri de simulare a fizicii personajelor pentru jocuri. El a sugerat că acuratețea nu este la fel de importantă ca credibilitatea și performanța. Inima întregului său algoritm a fost o metodă folosită încă din anii '60 pentru a modela dinamica moleculară, numită Verlet Integration. S-ar putea să fiți familiarizați cu jocul Hitman: Codename 47. Acesta a fost unul dintre primele jocuri care utilizează fizica ragdoll și utilizează algoritmii dezvoltați de Jakobsen.
Verlet Integration este metoda pe care o vom folosi pentru a transmite poziția PointMass. Ce am făcut înainte, x = x + velx
, este o metodă numită Integrare Euler (pe care am folosit-o și în Coding Destructible Pixel Terrain).
Diferența majoră dintre integrarea Euler și Verlet este modul în care este implementată viteza. Folosind Euler, o viteză este stocată cu obiectul și este adăugată la poziția obiectului în fiecare cadru. Cu toate acestea, folosind Verlet, se aplică inerția utilizând poziția anterioară și cea actuală. Luați diferența în cele două poziții și adăugați-o la ultima poziție pentru a aplica inerția.
// Inerția: obiectele în mișcare rămân în mișcare. ulX = x - ultimaX velY = y - ultimaYxtX = x + velX + accX * timestepSq nextY = y + velY + accY * timestepSq lastX = x lastY = y x = nextX y = nextY
Am adăugat accelerația acolo pentru gravitație. În afară de asta, accX
și Accy
nu vor fi necesare pentru rezolvarea coliziunilor. Folosind integrarea Verlet, nu mai trebuie să facem nici un fel de impuls sau rezolvarea forțelor pentru coliziuni. Modificarea poziției va fi suficientă pentru a avea o simulare stabilă, realistă și rapidă. Ceea ce a dezvoltat Jakobsen este un substitut liniar pentru ceva care altfel este neliniar.
Beneficiile integrării Verlet pot fi cele mai bune prin exemplu. Într-un motor de țesături, nu numai că vom avea puncte PointMasses, dar și legături între ele. "Link-urile" noastre vor fi o constrângere la distanță între două PointMasses. În mod ideal, vrem ca două PointMasses cu această constrângere să fie întotdeauna la o anumită distanță.
Ori de câte ori rezolvăm această constrângere, integrarea Verlet ar trebui să țină în mișcare aceste puncte. De exemplu, dacă un capăt ar trebui să fie mutat rapid în jos, celălalt capăt ar trebui să-l urmeze ca un bici prin inerție.
Vom avea nevoie de un singur link pentru fiecare pereche de PointMasses atașate una de cealaltă. Toate datele pe care le veți avea nevoie în link sunt PointMasses și distanțele de odihnă. Opțional puteți avea rigiditate, pentru mai mult de o constrângere de primăvară. În demo-ul nostru avem de asemenea o "sensibilitate la rupere", care este distanța la care link-ul va fi eliminat.
Voi explic doar restingDistance
aici, dar distanța la rupere și rigiditatea sunt implementate în demo și în codul sursă.
Legătură restingDistance tearDistance stiffness PointMass O rezolvare PointMass B () matematică pentru rezolvarea distanței
Puteți utiliza algebra liniară pentru a rezolva problema pentru constrângere. Găsiți distanțele dintre cele două, determinați cât de departe de-a lungul restingDistance
ele sunt, apoi le traduc pe baza acestora și a diferențelor lor.
// calcula distanța diffX = p1.x - p2.x diffY = p1.y - p2.yd = sqrt (diffX * diffX + diffY * diffY) // diferență diferență scalară = (restingDistance - d) / d // translation pentru fiecare PointMass. Acestea vor fi împinse la o distanță de 1/2 la distanța necesară pentru a se potrivi cu distanțele de odihnă. translateX = diffX * 0.5 * diferență translateY = diffY * 0.5 * diferență p1.x + = translateX p1.y + = translateY p2.x - = translateX p2.y - = translateY
În demo, avem de calcul și masa și rigiditatea. Există unele probleme în rezolvarea acestei constrângeri. Atunci când există mai mult de două sau trei PointMasses legate unul de altul, rezolvarea unora dintre aceste constrângeri poate încălca alte constrângeri rezolvate anterior.
Thomas Jakobsen a întâmpinat și această problemă. La început, se poate crea un sistem de ecuații și se rezolvă simultan toate constrângerile. Acest lucru ar crește rapid în complexitate, totuși, și ar fi dificil de adăugat mai mult decât chiar doar câteva legături în sistem.
Jakobsen a dezvoltat o metodă care la început ar putea părea prostie și naivă. El a creat o metodă numită "relaxare", unde, în loc de a rezolva o dată constrângerea, o rezolvăm de mai multe ori. De fiecare dată când repetăm și rezolvăm legăturile, setul de legături devine din ce în ce mai aproape de tot ceea ce este rezolvat.
Pentru a revedea, iată cum funcționează motorul nostru în pseudocod. Pentru un exemplu mai specific, verificați codul sursă al demo-ului.
animationLoop numPhysicsUpdates = oricum ne putem potrivi in intervalul de timp scurs (pentru fiecare numPhysicsUpdates) // (cu constraintSolve fiind orice numar 1 sau mai mare.Am folosit de obicei 3 pentru (fiecare constraintSolve) pentru (fiecare constrângere Link) rezolva constrângere // rezolvarea constrângerilor legate de final // constrângeri de final actualizare fizică // (folosiți verlet!) // finalizare actualizare fizică tragere puncte și linkuri
Acum putem construi țesătura în sine. Crearea legăturilor ar trebui să fie destul de simplă: conectați-le la stânga atunci când PointMass nu este primul pe rândul său și conectați-l când nu este primul în coloana sa.
Demo-ul utilizează o listă unidimensională pentru a stoca funcțiile PointMasses și găsește puncte la care se face trimitere la utilizarea x + y * lățime
.
// dorim ca bucla y să fie afară, așa că scanează rând în rând în loc de coloană cu coloană pentru (fiecare y de la 0 la înălțime) pentru (fiecare x de la 0 la lățime) nou PointMass la x, y // atașați la stânga dacă (x! = 0) atașați PM la ultimul PM din listă // atașați la dreapta dacă (y! = 0) atașați PM la PM @ ((y - 1) lățimea + 1) + x) în listă dacă (y == 0) pin PM adăugați PM la listă
S-ar putea să observați în cod că avem, de asemenea, "PM PM". Dacă nu vrem ca cortina noastră să cadă, putem bloca rândul de sus al PointMasses la pozițiile lor de plecare. Pentru a programa o constrângere cu pini, adăugați câteva variabile pentru a urmări locația pinului și apoi deplasați PointMass în acea poziție după fiecare rezolvare a constrângerilor.
Ragdolls au fost intențiile originale ale lui Jakobsen în urma folosirii lui de integrare Verlet. Mai întâi vom începe cu capetele. Vom crea o constrângere a cercului care va interacționa numai cu limita.
Cercul PointMass rază rezolva () if (y < radius) y = 2*(radius) - y; if (y > înălțime-rază) y = 2 * (înălțime - rază) - y; dacă (x> raza de lățime) x = 2 * (lățimea - raza) - x; dacă (x < radius) x = 2*radius - x;
Apoi putem crea corpul. Am adăugat fiecare parte a corpului pentru a se potrivi exact cu proporțiile de masă și lungime ale unui corp uman obișnuit. Verifică Body.pde
în fișierele sursă pentru detalii complete. Făcând acest lucru ne va conduce la o altă problemă: corpul va contori cu ușurință în forme incomode și ar arăta foarte nerealist.
Există o serie de modalități de remediere a acestei situații. În demo, folosim legături invizibile și foarte instabile de la picioare la umăr și la nivelul pelvisului până la cap, pentru a împinge în mod natural corpul într-o poziție mai puțin incomodă de odihnă.
De asemenea, puteți crea constrângeri în unghi fals prin utilizarea legăturilor. Să spunem că avem trei PointMasses, cu două legate de unul în mijloc. Puteți găsi o lungime între capete pentru a satisface orice unghi ales. Pentru a găsi această lungime, puteți folosi Legea cosinelor.
A = distanța de odihnă de la capăt PointMass la centru PointMass B = distanța de odihnă de la alta PointMass la centru PointMass lungime = sqrt (A * A + B * B - 2 * A * B * cos (unghi) ca distanță de odihnă
Modificați legătura astfel încât această constrângere să se aplice numai când distanța este mai mică decât distanța de odihnă sau, dacă este mai mare decât distanța de odihnă. Acest lucru va păstra unghiul în centrul punctului de la a fi prea apropiat sau prea departe, în funcție de ceea ce aveți nevoie.
Unul dintre lucrurile minunate cu un motor complet fizic liniar este faptul că poate fi orice dimensiune doriți. Tot ceea ce sa făcut cu x a fost de asemenea făcut cu o valoare y, și acolo pot fi extenuate la trei sau chiar patru dimensiuni (nu sunt sigur cum ai face asta, totuși!)
De exemplu, iată o constrângere a legăturii pentru simulare în 3D:
// calcula distanța diffX = p1.x - p2.x diffY = p1.y - p2.y diffZ = p1.z - p2.zd = sqrt (diffX * diffX + diffY * diffY + diffZ * diffZ) diferența scalară = (restingDistance - d) / d // traducere pentru fiecare PointMass. Acestea vor fi împinse la o distanță de 1/2 la distanța necesară pentru a se potrivi cu distanțele de odihnă. translateX = diffX * 0.5 * diferență translateY = diffY * 0.5 * diferență translateZ = diffZ * 0.5 * diferență p1.x + = translateX p1.y + = translateY p1.z + = translateZ p2.x - = translateX p2.y - = translateY p2.z - = translateZ
Vă mulțumim pentru lectură! O mare parte din simulare se bazează în mare măsură pe articolul lui Thomas Jakobsen despre caracterele avansate de caractere din GDC 2001. Am făcut tot ce mi-a făcut pentru a elimina majoritatea lucrurilor complicate și pentru a simplifica până la punctul pe care majoritatea programatorilor o vor înțelege. Dacă aveți nevoie de ajutor sau aveți comentarii, nu ezitați să postați mai jos.