Fizica platformei de bază 2D, Partea 2

În această parte a seriei de platforme fizice 2D, vom crea o mapă tile și vom implementa parțial detectarea și răspunsul coliziunii obiectelor tilemap.

Nivel Geometrie

Există două abordări de bază pentru construirea nivelelor platformerului. Unul dintre ele este de a utiliza o grilă și așezați plăcile corespunzătoare în celule, iar cealaltă este o formă mai liberă, în care puteți plasa liber geometria nivelului oricum și oriunde doriți. 

Există argumente pro-contra pentru ambele abordări. Vom folosi grila, așa că să vedem ce fel de profesioniști are față de cealaltă metodă:

  • O mai bună detectare a coliziunii de performanță împotriva rețelei este mai ieftină decât în ​​cazul obiectelor plasate în cele mai multe cazuri.
  • Face mult mai ușor să se ocupe de trasarea pe traseu.
  • Placile sunt mai precise si mai previzibile decat obiectele asezate slab, mai ales cand se iau in considerare lucruri precum terenul distrubuitor.

Construirea unei clase de hartă

Să începem prin crearea unei clase Map. Va păstra toate datele specifice hărții.

hartă publică a clasei 

Acum trebuie să definim toate plăcile pe care harta conține, dar înainte de a face acest lucru, trebuie să știm ce tipuri de plăci există în jocul nostru. Pentru moment, noi planificăm doar trei: o țiglă goală, o faianță solidă și o platformă cu sens unic.

public enum TileType Gol, Blocare, OneWay 

În demo, tipurile de plăci corespund direct tipului de coliziune pe care am dori să o avem cu o țiglă, dar într-un joc real care nu este neapărat așa. Pe măsură ce aveți mai multe plăci diferite, ar fi mai bine să adăugați noi tipuri, cum ar fi GrassBlock, GrassOneWay și așa mai departe, pentru a permite modelului TileType să definească nu numai tipul coliziunii, ci și aspectul plăcii.

Acum, în clasa de hărți putem adăuga o serie de plăci.

clasa publica hartă private TileType [,] mTiles; 

Bineînțeles, o tabelă pe care nu o putem vedea nu este foarte folositoare pentru noi, așa că avem nevoie și de sprites pentru a face copii de rezervă ale datelor. În mod normal, în Unitate, este extrem de ineficient ca fiecare piesă să fie un obiect separat, dar din moment ce folosim doar acest lucru pentru a testa fizica noastră, este bine să o facem astfel în demo.

privat SpriteRenderer [,] mTilesSprites;

Harta are nevoie, de asemenea, de o poziție în spațiul mondial, astfel încât, dacă trebuie să avem mai mult decât o singură, putem să-i mutăm.

public Vector3 mPoziția;

Lățime și înălțime, în plăci.

public int mWidth = 80; public int mHeight = 60;

Și mărimea dalelor: în demonstrație vom lucra cu o dimensiune destul de mică a dalelor, care este de 16 x 16 pixeli.

public const int cTileSize = 16;

Asta ar fi. Acum avem nevoie de câteva funcții ajutoare pentru a ne permite să accesăm cu ușurință datele hărții. Să începem prin realizarea unei funcții care va transforma coordonatele lumii în coordonatele țiglelor.

public Vector2i GetMapTileAtPoint (Vector2 punct) 

După cum puteți vedea, această funcție durează a Vector2 ca parametru și returnează a Vector2i, care este practic un vector 2D care operează pe numere întregi în loc de plutitoare.

Transformarea poziției lumii în poziția hărții este foarte simplă - pur și simplu trebuie să schimbăm poziția punct de mPosition așa că vom întoarce țigla în raport cu poziția hărții și apoi vom împărți rezultatul cu mărimea dalelor.

publica Vector2i GetMapTileAtPoint (Vector2 punct) returnează vectorul Vector2i ((int) ((point.x - mPosition.x + cTileSize / 2.0f) / (float) (cTileSize)), (int) ((point.y - mPosition. y + cTileSize / 2.0f) / (float) (cTileSize)); 

Rețineți că trebuia să schimbăm punct suplimentar prin cTileSize / 2.0f, deoarece pivotul plăcilor este în centrul său. Să facem, de asemenea, două funcții suplimentare care vor returna numai componentele X și Y ale poziției în spațiul hărții. Va fi util mai târziu.

public int int GetMapTileYAtPoint (float y) retur (int) ((y - mPosition.y + cTileSize / 2.0f) / (float) (cTileSize));  int public GetMapTileXAtPoint (float x) întoarcere (int) ((x - mPosition.x + cTileSize / 2.0f) / (float) (cTileSize)); 

De asemenea, ar trebui să creăm o funcție complementară care, dată fiind o țiglă, își va reveni poziția în spațiul mondial.

publicul Vector2 GetMapTilePosition (int tileIndexX, int tileIndexY) returnează vectorul Vector2 ((float) (tileIndexX * cTileSize) + mPosition.x, (float) (tileIndexY * cTileSize) + mPosition.y);  publicul Vector2 GetMapTilePosition (Vector2i tileCoords) returnează vectorul Vector2 ((float) (tileCoords.x * cTileSize) + mPosition.x, (float) (tileCoords.y * cTileSize) + mPosition.y); 

În afară de pozițiile de traducere, trebuie să avem și câteva funcții pentru a vedea dacă o țiglă dintr-o anumită poziție este goală, este o faianță solidă sau este o platformă unidirecțională. Să începem cu o funcție GetTile foarte generică, care va returna un tip de țiglă specifică.

public TileType GetTile (int x, int y) dacă (x < 0 || x >= mWidth || y < 0 || y >= mHeight) returnați TileType.Block; retur mTiles [x, y]; 

După cum puteți vedea, înainte de a reveni la tipul de țiglă, verificăm dacă poziția dată nu este limitată. Dacă este, atunci vrem să o tratăm ca un bloc solid, altfel vom întoarce un tip adevărat.

Următorul în coadă este o funcție pentru a verifica dacă o piesă este un obstacol. 

bootul public IsObstacle (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) return true; întoarcere (mTiles [x, y] == TileType.Block); 

In acelasi mod ca si inainte, verificam daca placa este in afara limitelor, si daca este atunci vom reveni la adevarat, asa ca orice piesa din afara limitelor este tratata ca un obstacol.

Acum să verificăm dacă placa este o faianță. Putem sta pe un bloc și pe o platformă unidirecțională, așa că trebuie să revenim la adevărat dacă țigla este una dintre aceste două.

boolul public IsGround (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) return false; întoarcere (mTiles [x, y] == TileType.OneWay || mTiles [x, y] == TileType.Block); 

În cele din urmă, să adăugăm IsOneWayPlatform și Este gol funcționează în același mod.

boolul public IsOneWayPlatform (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) return false; retur (mTiles [x, y] == TileType.OneWay);  boolean public IsEmpty (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) return false; întoarcere (mTiles [x, y] == TileType.Empty); 

Asta e tot ce avem nevoie de clasa noastră de hărți. Acum putem trece mai departe și putem implementa coliziunea de caracter împotriva lui.

Coliziune de caractere

Să ne întoarcem la MovingObject clasă. Trebuie să creăm câteva funcții care să detecteze dacă personajul se ciocnește cu tabloul tilelor.

Metoda prin care vom ști dacă caracterul se ciocnește cu o țiglă sau nu este foarte simplu. Vom verifica toate plăcile care există chiar în fața AABB al obiectului în mișcare.


Caseta galbenă reprezintă AABB-ul personajului și vom verifica dalele de-a lungul liniilor roșii. Dacă oricare dintre cele care se suprapun cu o țiglă, am setat o variabilă de coliziune corespunzătoare la adevărat (cum ar fi mOnGround, mPushesLeftWall, mAtCeiling sau mPushesRightWall).

Să începem prin crearea unei funcții HasGround, care va verifica dacă personajul se ciocnește cu o piesă de sol. 

public bool HasGround (Vector2 oldPosition, Vector2 pozitie, Vector2 viteza, out float groundY) 

Această funcție revine adevărat dacă caracterul se suprapune cu oricare dintre plăcile de jos. Aceasta ia poziția veche, poziția curentă și viteza actuală ca parametri și, de asemenea, returnează poziția Y a vârfului plăcii cu care ne confruntăm și dacă țigla colizată este o platformă unică sau nu.

Primul lucru pe care dorim să-l facem este să calculam centrul AABB.

boolul public HasGround (Vector2 oldPosition, pozitia Vector2, viteza Vector2, terenul plutitor Y) var center = position + mAABBOffset; 

Acum că avem asta, pentru verificarea de coliziune de jos va trebui să calculam începutul și sfârșitul liniei senzorului de fund. Linia senzorului este la doar un pixel sub conturul fundului AABB.

boolul public HasGround (Vector2 oldPosition, pozitia Vector2, viteza Vector2, terenul plutitor Y) var center = position + mAABBOffset; var bottomLeft = centru - mAABB.halfSize - Vector2.up + Vector2.right; var bottomRight = nou Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y); 

stânga jos și dreapta-jos reprezintă cele două capete ale senzorului. Acum, când avem aceste informații, putem calcula ce dale trebuie să verificăm. Să începem prin crearea unei bucla în care vom trece prin gresie de la stânga la dreapta.

pentru (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) 

Rețineți că nu există nicio condiție pentru a ieși din bucla aici - o vom face la sfârșitul bucla. 

Primul lucru pe care ar trebui să-l facem în buclă este să ne asigurăm că checkedTile.x nu este mai mare decât capătul drept al senzorului. Este posibil să se întâmple acest lucru deoarece deplasăm punctul verificat prin multiplii dimensiunii plăcii, de exemplu, dacă caracterul are o lățime de 1,5 tigle, trebuie să verificăm țigla de pe marginea din stânga a senzorului, apoi o piesă la dreapta , iar apoi 1,5 dale spre dreapta în loc de 2.

pentru (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkTile.x, bottomRight.x); 

Acum trebuie să obținem coordonatele țiglelor în spațiul hărții pentru a putea verifica tipul plăcilor.

int tileIndexX, tileIndexY; pentru (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkTile.x); tileIndexY = mMap.GetMapTileYAtPoint (checkedTile.y); 

Mai întâi, să calculam poziția de top a plăcilor.

int tileIndexX, tileIndexY; pentru (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkTile.x); tileIndexY = mMap.GetMapTileYAtPoint (checkedTile.y); groundY = (float) tileIndexY * map.cTileSize + map.cTileSize / 2.0f + mMap.mPosition.y; 

Acum, dacă plăcile verificate în prezent reprezintă un obstacol, putem reveni cu adevărat la adevărat.

int tileIndexX, tileIndexY; pentru (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkTile.x); tileIndexY = mMap.GetMapTileYAtPoint (checkedTile.y); groundY = (float) tileIndexY * map.cTileSize + map.cTileSize / 2.0f + mMap.mPosition.y; dacă (mMap.IsObstacle (tileIndexX, tileIndexY)) returnează adevărat; 

În cele din urmă, să verificăm dacă am analizat deja toate plăcile care se intersectează cu senzorul. Dacă este cazul, atunci putem ieși din siguranță în bucla. După ce ieșim din buclă și nu găsim o plăcuță cu care ne-am ciocnit, trebuie să ne întoarcem fals pentru a lăsa apelantul să știe că nu există un teren sub obiect.

int tileIndexX, tileIndexY; pentru (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkTile.x); tileIndexY = mMap.GetMapTileYAtPoint (checkedTile.y); groundY = (float) tileIndexY * map.cTileSize + map.cTileSize / 2.0f + mMap.mPosition.y; dacă (mMap.IsObstacle (tileIndexX, tileIndexY)) returnează adevărat; dacă (checkedTile.x> = bottomRight.x) pauză;  return false; 

Aceasta este versiunea cea mai de bază a cecului. Să încercăm să mergem la lucru acum. Înapoi în UpdatePhysics funcția noastră, verificarea veche a solului arată așa.

dacă (mPosition.y <= 0.0f)  mPosition.y = 0.0f; mOnGround = true;  else mOnGround = false;

Să o înlocuim folosind metoda nou creată. Dacă personajul se încadrează și am găsit un obstacol pe calea noastră, atunci trebuie să-l mutăm din coliziune și să stabilim și mOnGround la adevărat. Să începem cu condiția.

float groundY = 0; dacă (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY))  

Dacă condiția este îndeplinită, atunci trebuie să mutăm caracterul de pe partea superioară a plăcii cu care s-au ciocnit.

float groundY = 0; dacă (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY))  mPosition.y = groundY + mAABB.halfSize.y - mAABBOffset.y; 

După cum puteți vedea, este foarte simplu deoarece funcția readuce nivelul la sol la care ar trebui să aliniem obiectul. După aceasta, trebuie doar să setăm viteza verticală la zero și să setăm mOnGround la adevărat.

float groundY = 0; dacă (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY))  mPosition.y = groundY + mAABB.halfSize.y - mAABBOffset.y; mSpeed.y = 0.0f; mOnGround = true; 

Dacă viteza noastră verticală este mai mare decât zero sau nu atingem niciun punct, trebuie să setăm mOnGround la fals.

float groundY = 0; dacă (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY))  mPosition.y = groundY + mAABB.halfSize.y - mAABBOffset.y; mSpeed.y = 0.0f; mOnGround = true;  else mOnGround = false;

Acum, să vedem cum funcționează acest lucru.

După cum puteți vedea, funcționează bine! Detectarea coliziunii pentru pereți de pe ambele părți și în partea de sus a personajului nu este încă acolo, dar caracterul se oprește de fiecare dată când se întâlnește cu solul. Încă mai trebuie să punem un pic mai mult în funcția de verificare a coliziunilor pentru ao face robustă.

Una dintre problemele pe care trebuie să le rezolvăm este vizibilă dacă decalajul caracterului de la un cadru la altul este prea mare pentru a detecta corect coliziunea. Acest lucru este ilustrat în următoarea imagine.

Această situație nu se întâmplă acum pentru că am blocat viteza maximă care se încadrează la o valoare rezonabilă și actualizăm fizica cu o frecvență de 60 FPS, astfel încât diferențele dintre pozițiile dintre cadre sunt destul de mici. Să vedem ce se întâmplă dacă actualizăm fizica doar de 30 de ori pe secundă. 

După cum puteți vedea, în acest scenariu, controlul nostru de coliziune la sol ne-a eșuat. Pentru a rezolva acest lucru, nu putem verifica pur și simplu dacă personajul se află sub el în poziția actuală, dar mai degrabă trebuie să vedem dacă există obstacole de-a lungul drumului de la poziția cadrului anterior.

Să ne întoarcem la noi HasGround funcţie. Aici, pe lângă calculul centrului, vom dori, de asemenea, să calculam centrul anterioare al cadrului.

public bool HasGround (Vector2 oldPosition, Vector2 pozitie, Vector2 viteza, out float groundY) var oldCenter = oldPosition + mAABBOffset; var centru = poziție + mAABBOffset;

De asemenea, trebuie să obținem poziția senzorului anterior a cadrului.

public bool HasGround (Vector2 oldPosition, Vector2 pozitie, Vector2 viteza, out float groundY) var oldCenter = oldPosition + mAABBOffset; var centru = poziție + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var bottomLeft = centru - mAABB.halfSize - Vector2.up + Vector2.right; var bottomRight = nou Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y);

Acum trebuie să calculam la ce tiglă pe verticală vom începe să verificăm dacă există sau nu o coliziune și la care ne oprim.

public bool HasGround (Vector2 oldPosition, Vector2 pozitie, Vector2 viteza, out float groundY) var oldCenter = oldPosition + mAABBOffset; var centru = poziție + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var bottomLeft = centru - mAABB.halfSize - Vector2.up + Vector2.right; var bottomRight = nou Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y); int endY = mMap.GetMapTileYAtPoint (bottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY);

Începem căutarea de pe placă la poziția senzorului anterior a cadrului și o terminăm la poziția senzorului cadrului curent. Desigur, pentru că atunci când verificăm o coliziune la sol, presupunem că cadem, ceea ce înseamnă că ne mișcăm de la poziția superioară la cel inferior.

În cele din urmă, trebuie să avem o altă buclă de iterație. Acum, înainte de a umple codul pentru această bucla exterioară, să luăm în considerare următorul scenariu.

Aici puteți vedea o săgeată care se mișcă repede. Acest exemplu arată că este necesar nu numai să iteram prin toate plăcile pe care ar trebui să le trecem vertical, ci și să interpolam poziția obiectului pentru fiecare țiglă pe care o parcurgem pentru a aproxima calea de la poziția cadrului anterior la cea actuală. Dacă pur și simplu am continuat să folosim poziția obiectului curent, atunci în cazul de mai sus ar fi detectată o coliziune, chiar dacă nu ar trebui să fie.

Să redenumim stânga jos și dreapta-jos la fel de newBottomLeft și newBottomRight, astfel încât știm că acestea sunt pozițiile senzorului noului cadru.

public bool HasGround (Vector2 oldPosition, Vector2 pozitie, Vector2 viteza, out float groundY) var oldCenter = oldPosition + mAABBOffset; var centru = poziție + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomLeft = centru - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomRight = nou Vector2 (newBottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, newBottomLeft.y); int endY = mMap.GetMapTileYAtPoint (newBottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY); int tileIndexX; pentru (int tileIndexY = begY; tileIndexY> = endY; --tilIndexY)  return false; 

Acum, în această buclă nouă, să interpolam pozițiile senzorului, astfel încât la începutul buclei să presupunem că senzorul se află în poziția anterioară a cadrului și că la sfârșitul acestuia va fi în poziția cadrului curent.

public bool HasGround (Vector2 oldPosition, Vector2 pozitie, Vector2 viteza, out float groundY) var oldCenter = oldPosition + mAABBOffset; var centru = poziție + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomLeft = centru - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomRight = nou Vector2 (newBottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, newBottomLeft.y); int endY = mMap.GetMapTileYAtPoint (newBottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY); int dist = Mathf.Max (Mathf.Abs (endY-begY), 1); int tileIndexX; pentru (int tileIndexY = begY; tileIndexY> = endY; --IndIndexY) var bottomLeft = Vector2.Lerp (newBottomLeft, oldBottomLeft, (float) Mathf.Abs (endY-tileIndexY) / dist); var bottomRight = nou Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y);  return false; 

Rețineți că interpolam vectorii bazându-ne pe diferența de dale pe axa Y. Când pozițiile vechi și noi se află în aceeași placă, distanța verticală va fi zero, deci în acest caz nu am fi putut să împărțim distanța. Așadar, pentru a rezolva această problemă, dorim ca distanța să aibă o valoare minimă de 1, astfel încât, dacă se va întâmpla un astfel de scenariu (și se va întâmpla foarte des), pur și simplu vom folosi noua poziție pentru detectarea coliziunilor. 

În cele din urmă, pentru fiecare iterație, trebuie să executăm același cod pe care l-am făcut deja pentru verificarea coliziunii de-a lungul lățimii obiectului. 

public bool HasGround (Vector2 oldPosition, Vector2 pozitie, Vector2 viteza, out float groundY) var oldCenter = oldPosition + mAABBOffset; var centru = poziție + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomLeft = centru - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomRight = nou Vector2 (newBottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, newBottomLeft.y); int endY = mMap.GetMapTileYAtPoint (newBottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY); int dist = Mathf.Max (Mathf.Abs (endY-begY), 1); int tileIndexX; pentru (int tileIndexY = begY; tileIndexY> = endY; --IndIndexY) var bottomLeft = Vector2.Lerp (newBottomLeft, oldBottomLeft, (float) Mathf.Abs (endY-tileIndexY) / dist); var bottomRight = nou Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y); pentru (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkTile.x); groundY = (float) tileIndexY * map.cTileSize + map.cTileSize / 2.0f + mMap.mPosition.y; dacă (mMap.IsObstacle (tileIndexX, tileIndexY)) returnează adevărat; dacă (checkedTile.x> = bottomRight.x) pauză;  return false; 

Cam asta e tot. Așa cum vă puteți imagina, dacă obiectele jocului se mișcă foarte repede, acest mod de a verifica coliziunea poate fi destul de costisitor, dar ne asigură și că nu vor exista probleme ciudate cu obiecte care se deplasează prin ziduri solide.

rezumat

Phew, a fost mai mult decât am crezut că avem nevoie, nu-i așa? Dacă observați orice erori sau posibile comenzi rapide pe care le puteți lua, permiteți-mi și tuturor să știți în comentariile! Verificarea coliziunii ar trebui să fie suficient de robustă, astfel încât să nu ne temem de eventualele evenimente nefericite de obiecte alunecând prin blocurile tilemap. 

O mare parte a codului a fost scris pentru a vă asigura că nu există obiecte care trec prin plăci la viteze mari, dar dacă aceasta nu este o problemă pentru un anumit joc, am putea elimina în siguranță codul suplimentar pentru a crește performanța. S-ar putea chiar să fie o idee bună să ai un steag pentru anumite obiecte care se mișcă rapid, astfel încât numai cei care folosesc versiunile mai costisitoare ale verificărilor.

Avem încă multe lucruri de rezolvat, dar am reușit să facem o verificare fiabilă a coliziunii, care poate fi oglindită destul de clar în celelalte trei direcții. Vom face asta în următoarea parte.