Fizica platformei de bază 2D, Partea 6 Obiectivul vs. răspunsul la coliziune a obiectelor

În tranșa anterioară a seriei, am implementat un mecanism de detectare a coliziunii între obiectele de joc. În această parte, vom folosi mecanismul de detectare a coliziunilor pentru a construi un răspuns fizic simplu dar robust între obiecte.

Demo-ul arată rezultatul final al acestui tutorial. Utilizați WASD pentru a muta caracterul. Butonul mijlociu al mouse-ului creează o platformă unidirecțională, butonul din dreapta al mouse-ului creează o țiglă solidă, iar bara spațială creează o clonă de caractere. Glisoarele modifică dimensiunea personajului jucătorului. 

Demo-ul a fost publicat sub unitatea 5.4.0f3, iar codul sursă este, de asemenea, compatibil cu această versiune de Unity.

Rezolvarea coliziunilor

Acum, că avem toate datele de coliziune din activitatea pe care am făcut-o în partea anterioară, putem adăuga un răspuns simplu la coliziunea obiectelor. Obiectivul nostru aici este acela de a face posibil ca obiectele să nu treacă unul peste celălalt ca și cum ar fi într-un alt plan - vrem să fie solide și să acționeze ca un obstacol sau o platformă pentru alte obiecte. Pentru aceasta, trebuie să facem un singur lucru: mutați obiectul dintr-o suprapunere dacă se întâmplă.

Acoperă datele suplimentare

Vom avea nevoie de date suplimentare pentru MovingObject clasă pentru a gestiona răspunsul obiect vs. obiect. Mai întâi de toate, este frumos să ai un boolean pentru a marca un obiect ca fiind cinematic - adică, acest obiect nu va fi împins de alt obiect. 

Aceste obiecte vor funcționa bine ca platforme și pot fi și platforme mobile. Ele ar trebui să fie cele mai grele lucruri în jur, deci poziția lor nu va fi corectată în nici un fel - alte obiecte vor trebui să se îndepărteze pentru a face loc pentru ei.

bool public mIsKinematic = fals;

Celelalte date pe care îmi place să le am sunt informații despre faptul că stăm pe un obiect sau pe partea stângă sau dreaptă etc. Până acum am putut interacționa doar cu dale, dar acum putem interacționa și cu alte obiecte. 

Pentru a aduce o anumită armonie în acest sens, vom avea nevoie de un nou set de variabile care să descrie dacă caracterul împinge ceva în stânga, în dreapta, în partea de sus sau în partea de jos.

bool public mPushesRight = fals; bool public mPushesLeft = false; bool public mPushesBottom = false; bool public mPushesTop = false; bool public mPushedTop = fals; bool public mPushedBottom = false; bool public mPushedRight = fals; bool public mPushedLeft = false; bool public mPushesLeftObject = false; bool public mPushesRightObject = false; bool public mPushesBottomObject = false; bool public mPushesTopObject = false; bool public mPushedLeftObject = false; bool public mPushedRightObject = false; bool public mPushedBottomObject = false; bool public mPushedTopObject = false; bool public mPushesRightTile = false; bool public mPushesLeftTile = false; bool public mPushesBottomTile = false; bool public mPushesTopTile = false; bool public mPushedTopTile = false; bool public mPushedBottomTile = false; bool public mPushedRightTile = false; bool public mPushedLeftTile = false;

Acum sunt multe variabile. Într-un context de producție, ar merita să le transformăm în steaguri și să avem doar un număr întreg în loc de toate aceste booleani, dar din motive de simplitate ne vom ocupa de a lăsa aceștia așa cum sunt. 

Așa cum ați putea observa, aici avem date destul de fine. Știm dacă caracterul împinge sau împinge un obstacol într-o anumită direcție în general, dar putem, de asemenea, ușor să ne întrebăm dacă suntem lângă o țiglă sau un obiect.

Deplasați-vă din suprapunere

Să creăm UpdatePhysicsResponse funcția în care vom rezolva răspunsul obiect vs. obiect.

private void UpdatePhysicsResponse () 

Mai întâi, dacă obiectul este marcat ca cinematic, ne întoarcem pur și simplu. Nu rezolvăm răspunsul deoarece obiectul cinematic nu trebuie să răspundă la orice alt obiect - celelalte obiecte trebuie să răspundă la acesta.

dacă (mIsKinematic) retur;

Acum, presupunem că nu vom avea nevoie de un obiect cinematic pentru a avea datele corecte cu privire la faptul că împinge un obiect pe partea stângă etc. Dacă nu este cazul, atunci acest lucru ar trebui să fie modificat un pic, pe care eu O să ating mai târziu linia.

Acum, să începem să abordăm variabilele pe care tocmai le-am declarat recent.

mPushedBottomObject = mPushesBottomObject; mPushedRightObject = mPushesRightObject; mPushedLeftObject = mPushesLeftObject; mPushedTopObject = mPushesTopObject; mPushesBottomObject = false; mPushesRightObject = false; mPushesLeftObject = false; mPushesTopObject = false;

Salvăm rezultatele cadrului precedent la variabilele corespunzătoare și acum presupunem că nu atingem nici un alt obiect.

Să începem să iterăm acum prin toate datele de coliziune acum.

pentru (int i = 0; i < mAllCollidingObjects.Count; ++i)  var other = mAllCollidingObjects[i].other; var data = mAllCollidingObjects[i]; var overlap = data.overlap; 

În primul rând, să abordăm cazurile în care obiectele abia se ating, reciproc, care nu se suprapun. În acest caz, știm că nu trebuie să mișcăm nimic, ci doar să setăm variabilele. 

Așa cum am menționat anterior, indicatorul pe care obiectele atinge este că suprapunerea pe una dintre axe este egală cu 0. Să începem prin verificarea axei x.

dacă (overlap.x == 0.0f) 

Dacă condiția este adevărată, trebuie să vedem dacă celălalt obiect se află pe partea stângă sau dreaptă a AABB.

dacă (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x)  altceva 

În cele din urmă, dacă este în dreapta, apoi setați mPushesRightObject pentru a fi adevărat și pentru a seta viteza astfel încât să nu fie mai mare de 0, deoarece obiectul nostru nu mai poate trece la dreapta ca calea este blocată.

dacă (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f);  altceva 

Să ne ocupăm și de partea stângă în același mod.

dacă (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f);  altfel mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f); 

În cele din urmă, știm că nu va trebui să facem nimic altceva aici, așa că continuăm cu iterația următoarei buclă.

dacă (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f);  altfel mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f);  continua; 

Să abordăm și axa y în același fel.

dacă (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f);  altfel mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f);  continua;  altfel dacă (overlap.y == 0.0f) if (other.mAABB.center.y> mAABB.center.y) mPushesTopObject = true; mSpeed.y = Mathf.Min (mSpeed.y, 0.0f);  altfel mPushesBottomObject = true; mSpeed.y = Mathf.Max (mSpeed.y, 0.0f);  continua; 

Acesta este, de asemenea, un loc bun pentru a stabili variabilele pentru un corp cinematic, dacă trebuie să facem acest lucru. Nu ne-ar păsa dacă suprapunerea este egală cu zero sau nu, pentru că nu vom muta oricum un obiect cinematic. De asemenea, trebuia să ignorăm ajustarea vitezei, deoarece nu am vrea să oprim un obiect cinematic. Vom trece peste toate aceste lucruri pentru demo, deși, deoarece nu vom folosi variabilele de ajutor pentru obiectele cinematice.

Acum că acest lucru este acoperit, putem gestiona obiectele care s-au suprapus în mod corespunzător cu AABB-ul nostru. Înainte de a face acest lucru, totuși, permiteți-mi să explic abordarea pe care am luat-o pentru a răspunde la coliziune în demo.

Mai întâi, dacă obiectul nu se mișcă și ne batem în el, celălalt obiect ar trebui să rămână nemișcat. Noi îl tratăm ca pe un corp cinematic. Am decis să merg în acest fel pentru că simt că este mai generic și comportamentul de împingere poate fi întotdeauna tratat mai jos pe linie în actualizarea personalizată a unui anumit obiect.

Dacă ambele obiecte s-au mișcat în timpul coliziunii, am împărțit suprapunerea între ele pe baza vitezei lor. Cu cât se îndreptau mai repede, cu atât mai mare parte din valoarea suprapunerii vor fi mutate înapoi.

Ultimul punct este, similar cu abordarea răspunsului tilemap, dacă un obiect este în scădere și în timp ce merge în jos zgârieturi un alt obiect, chiar cu un pixel orizontal, obiectul nu va aluneca și continua să meargă în jos, dar va sta pe acel pixel.

Cred că aceasta este abordarea cea mai maleabilă și modificarea acesteia nu ar trebui să fie foarte dificilă dacă doriți să rezolvați diferit un răspuns.

Să continuăm implementarea prin calcularea vectorului de viteză absolută pentru ambele obiecte în timpul coliziunii. De asemenea, vom avea nevoie de suma vitezelor, deci știm ce procent din suprapunerea obiectului nostru ar trebui mutat.

Vector2 absSpeed1 = Vector2 nou (Mathf.Abs (data.pos1.x - data.oldPos1.x), Mathf.Abs (date.pos1.y - data.oldPos1.y)); Vector2 absSpeed2 = Vector2 nou (Mathf.Abs (data.pos2.x - data.oldPos2.x), Mathf.Abs (data.pos2.y - data.oldPos2.y)); Vector2 speedSum = absSpeed1 + absSpeed2;

Rețineți că, în loc să folosim viteza salvată în datele de coliziune, folosim decalajul dintre poziția în momentul coliziunii și cadranul anterior acesteia. Acest lucru va fi mai precis în acest caz, deoarece viteza reprezintă vectorul de mișcare înainte de corecția fizică. Pozițiile însăși sunt corectate dacă obiectul a lovit o placă solidă, de exemplu, dacă doriți să obțineți un vector de viteză corectat, ar trebui să calculați așa.

Acum, să începem să calculam raportul de viteză pentru obiectul nostru. Dacă celălalt obiect este cinematic, vom seta raportul de viteză la unul, pentru a ne asigura că mutăm întregul vector de suprapunere, respectând regula că obiectul cinematic nu trebuie să fie mutat.

float speedRatioX, speedRatioY; dacă (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; altceva 

Acum, să începem cu un caz ciudat în care ambele obiecte se suprapun reciproc, dar ambele nu au nici o viteză. Acest lucru nu ar trebui să se întâmple cu adevărat, dar dacă un obiect se produce prin suprapunerea unui alt obiect, ne-ar plăcea să se deplaseze în mod natural. În acest caz, am dori ca ambii să se deplaseze cu 50% din vectorul suprapus.

dacă (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; altceva if (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f; 

Un alt caz este atunci când speedSum pe axa x este egală cu zero. În acest caz, calculați raportul corespunzător pentru axa y și setați că trebuie să deplasăm 50% din suprapunere pentru axa x.

dacă (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f;  altfel dacă (speedSum.x == 0.0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y; 

În mod similar, ne ocupăm de cazul în care speedSum este zero numai pe axa y, iar pentru ultimul caz se calculează ambele rapoarte în mod corespunzător.

dacă (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; altceva if (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f;  altfel dacă (speedSum.x == 0.0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y;  altfel dacă (speedSum.y == 0.0f) speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = 0.5f;  altceva speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = absSpeed1.y / speedSum.y; 

Acum, când rapoartele sunt calculate, putem vedea cât de mult trebuie să ne compensăm obiectul.

float offsetX = suprapus.x * speedRatioX; float offsetY = suprapunere.y * speedRatioY;

Acum, înainte de a ne decide dacă ar trebui să mutăm obiectul din coliziune pe axa x sau pe axa y, să calculăm direcția de la care s-a produs suprapunerea. Există trei posibilități: fie că am intrat într-un alt obiect orizontal, vertical sau diagonal. 

În primul caz, dorim să ieșim din suprapunere pe axa x, în al doilea caz dorim să ieșim din suprapunerea pe axa y și, în ultimul caz, dorim să ieșim din suprapunere pe oricare dintre ele axa a avut cea mai mică suprapunere.

Amintiți-vă că pentru a suprapune cu un alt obiect avem nevoie de AABBs să se suprapună reciproc atât pe axele x și y. Pentru a verifica dacă am intrat într-un obiect pe orizontală, vom vedea dacă cadrul anterior anterior suprapusem obiectul de pe axa y. Dacă acesta este cazul și nu s-au suprapus pe axa x, suprapunerea trebuie să se fi întâmplat deoarece în cadrul actual AABB-urile au început să se suprapună pe axa x și, prin urmare, deducem că am lovit un alt obiect orizontal.

În primul rând, să calculăm dacă s-au suprapus cu celelalte AABB din cadrul anterior.

bool suprapusLastFrameX = Mathf.Abs (date.oldPos1.x - date.oldPos2.x) < mAABB.HalfSizeX + other.mAABB.HalfSizeX; bool overlappedLastFrameY = Mathf.Abs(data.oldPos1.y - data.oldPos2.y) < mAABB.HalfSizeY + other.mAABB.HalfSizeY;

Acum, să stabilim condiția pentru a ieși din suprapunere orizontal. Așa cum am explicat mai sus, trebuia să avem o suprapunere pe axa y și să nu se suprapună peste axa x din cadrul anterior.

dacă (! suprapusLastFrameX && suprapusLastFrameY) 

Dacă nu este cazul, atunci vom trece din suprapunere pe axa y.

dacă (! se suprapuneLastFrameX && se suprapuneLastFrameY)  altceva 

După cum sa menționat mai sus, trebuie să acoperim și scenariul de lovire în obiect în diagonală. Am intrat în obiect în diagonală dacă AABB-urile noastre nu se suprapun în cadrul anterior pe oricare dintre axe, deoarece știm că în cadrul actual se suprapun pe ambele, astfel încât coliziunea trebuie să se fi întâmplat pe ambele axe simultan.

dacă ((! suprapusLastFrameX && suprapusLastFrameY) || (! suprapusLastFrameX && suprapusLastFrameY))  altceva 

Dar vrem să ieșim din suprapunere pe axă în cazul unei ciocni diagonale numai dacă suprapunerea pe axa x este mai mică decât suprapunerea pe axa y.

dacă ((! suprapusLastFrameX && suprapusLastFrameY) || (! suprapusLastFrameX && suprapusLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y)))   else  

Toate cazurile au fost rezolvate. Acum, de fapt, trebuie să mutăm obiectul din suprapunere.

dacă ((! suprapusLastFrameX && suprapusLastFrameY) || (! suprapusLastFrameX && suprapusLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y)))  mPosition.x += offsetX; if (overlap.x < 0.0f)  mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);  else  mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);   else  

După cum puteți vedea, o rezolvăm foarte asemănător cu cazul în care abia atingem un alt AABB, dar, de asemenea, ne mutăm obiectul prin compensarea calculată.

Corecția verticală se face în același mod.

dacă ((! suprapusLastFrameX && suprapusLastFrameY) || (! suprapusLastFrameX &&! suprapusLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y)))  mPosition.x += offsetX; if (overlap.x < 0.0f)  mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);  else  mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);   else  mPosition.y += offsetY; if (overlap.y < 0.0f)  mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);  else  mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);  

Asta e aproape; există doar un singur avertisment pentru a acoperi. Imaginați-vă scenariul în care aterizăm simultan pe două obiecte. Avem două instanțe de date de coliziune aproape identice. Pe măsură ce repetăm ​​prin toate coliziunile, corectăm poziția coliziunii cu primul obiect, ne mutându-ne puțin. 

Apoi, ne ocupăm de coliziunea celui de-al doilea obiect. Suprapunerea salvată în momentul coliziunii nu mai este actualizată, deoarece ne-am mutat deja din poziția inițială și dacă am fi avut de a face față celei de-a doua ciocniri, același lucru ne-am ocupat de primul, ne-am mișca din nou puțin , făcând obiectul nostru corectat de două ori distanța pe care trebuia să o facă.

Pentru a remedia această problemă, vom urmări cât de mult am corectat deja obiectul. Să declare vectorul offsetSum chiar înainte de a începe să iterăm prin toate coliziunile.

Vector2 offsetSum = Vector2.zero;

Acum, asigurați-vă că adăugați toate compensările pe care le-am aplicat obiectului nostru în acest vector.

dacă ((! suprapusLastFrameX && suprapusLastFrameY) || (! suprapusLastFrameX &&! suprapusLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y)))  mPosition.x += offsetX; offsetSum.x += offsetX; if (overlap.x < 0.0f)  mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);  else  mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);   else  mPosition.y += offsetY; offsetSum.y += offsetY; if (overlap.y < 0.0f)  mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);  else  mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);  

În cele din urmă, să compensăm fiecare suprapunere consecutivă a coliziunii de vectorul cumulativ al corecțiilor pe care le-am făcut până acum.

var overlap = data.overlap - offsetSum;

Acum, dacă aterizăm pe două obiecte de aceeași înălțime în același timp, prima coliziune se va procesa corespunzător, iar suprapunerea celei de-a doua coliziuni va fi compensată la zero, ceea ce nu ne va mai mișca obiectul.

Acum că funcția noastră este gata, să ne asigurăm că o vom folosi. Un loc bun pentru a numi această funcție ar fi după CheckCollisions apel. Aceasta ne va cere să ne împărțim UpdatePhysics funcția în două părți, așa că hai să creăm a doua parte acum, în MovingObject clasă.

public void UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; 

În partea a doua ne numim proaspăt finisate UpdatePhysicsResponse funcția și actualizarea generală împinge variabilele stânga, dreapta, de jos și de sus. După aceasta, trebuie doar să aplicăm poziția.

public void UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; // actualizați aabb mAABB.center = mPosition; // aplicați modificările la transform transform.position = Vector3 noi (Mathf.Round (mPosition.x), Mathf.Round (mPosition.y), mSpriteDepth); transform.localScale = Vector3 nou (ScaleX, ScaleY, 1.0f); 

Acum, în bucla principală de actualizare a jocului, să numim a doua parte a actualizării fizicii după CheckCollisions apel.

void FixedUpdate () pentru (int i = 0; i < mObjects.Count; ++i)  switch (mObjects[i].mType)  case ObjectType.Player: case ObjectType.NPC: ((Character)mObjects[i]).CustomUpdate(); mMap.UpdateAreas(mObjects[i]); mObjects[i].mAllCollidingObjects.Clear(); break;   mMap.CheckCollisions(); for (int i = 0; i < mObjects.Count; ++i) mObjects[i].UpdatePhysicsP2(); 

Terminat! Acum obiectele noastre nu se pot suprapune peste ele. Desigur, într-un set de joc, ar trebui să adăugăm câteva lucruri, cum ar fi grupurile de coliziuni etc., astfel încât nu este obligatoriu să detectați sau să răspundeți la coliziune cu fiecare obiect, dar acestea sunt lucruri care depind de modul în care doriți aveți lucruri înființate în jocul dvs., așa că nu vom merge în această privință.

rezumat

Asta e pentru o altă parte a seriei simple de fizică a platformerului 2D. Am folosit mecanismul de detectare a coliziunilor implementat în partea anterioară pentru a crea un răspuns fizic simplu între obiecte. 

Cu ajutorul acestor instrumente este posibil să creați obiecte standard, cum ar fi platforme în mișcare, blocuri de împingere, obstacole personalizate și multe alte tipuri de obiecte care nu pot fi într-adevăr parte din harta țiglelor, dar trebuie să facă parte dintr-un anumit fel de teren. Există o caracteristică mai populară pe care o lipsește încă implementarea fizicii noastre, iar acestea sunt versanții. 

Sperăm că în următoarea parte vom începe să extindem tilemap-ul nostru cu suportul pentru acestea, care ar completa setul de caracteristici de bază, o implementare simplă a fizicii pentru un platformer 2D care ar trebui să aibă și acest lucru ar termina seria. 

Desigur, există întotdeauna loc de îmbunătățire, deci dacă aveți o întrebare sau un sfat despre cum să faceți ceva mai bun sau dacă aveți doar o opinie despre acest tutorial, nu ezitați să utilizați secțiunea de comentarii pentru a vă anunța!