În această parte a seriei despre fizica platformerului 2D, vom adăuga o apucare de cornișe, o mecanică de clemență pentru sari și o capacitate de a scala obiectul.
Acum, că putem să sară, să coborâm de pe platformele cu sens unic și să fugim în jurul nostru, putem implementa, de asemenea, o apucare de cornișe. Mecanica cu capcane mecanice nu este cu siguranță o necesitate în fiecare joc, dar este o metodă foarte populară de extindere a gamei posibile de mișcare a unui jucător în timp ce încă nu faceți ceva extrem ca un salt dublu.
Să ne uităm la modul în care determinăm dacă se poate apuca o margine. Pentru a determina dacă personajul poate apuca o muchie, vom verifica în mod constant partea în care personajul se îndreaptă. Dacă găsim o țiglă goală în partea de sus a AABB și apoi o piesă solidă sub ea, atunci partea de sus a acelei plăci solide este marginea pe care personajul nostru o poate apuca.
Să mergem la noi Caracter
de clasă, unde vom implementa o apucare de corzi. Nu are rost să faci asta în MovingObject
deoarece majoritatea obiectelor nu vor avea opțiunea de a apuca o muchie, deci ar fi o risipă de a face orice prelucrare în acea direcție acolo.
În primul rând, trebuie să adăugăm câteva constante. Să începem prin crearea constantelor offset ale senzorului.
public const float cGrabLedgeStartY = 0.0f; public const float cGrabLedgeEndY = 2.0f;
cGrabLedgeStartY
și cGrabLedgeEndY
sunt offseturi din partea de sus a AABB; primul este primul punct senzor, iar cel de-al doilea este punctul senzorului final. După cum puteți vedea, caracterul va trebui să găsească o muchie în limita a 2 pixeli.
Avem de asemenea nevoie de o constanta suplimentara pentru a alinia caracterul la placile pe care tocmai le-a luat. Pentru caracterul nostru, acesta va fi setat la -4.
public const float cGrabLedgeTileOffsetY = -4.0f;
În afară de asta, vom dori să ne amintim coordonatele plăcii pe care am apucat-o. Să le salvăm ca pe o variabilă de membru a unui personaj.
public Vector2i mLedgeTile;
Va trebui să vedem dacă putem să luăm pervazul din starea de salt, așa că hai să mergem acolo. Imediat ce verificăm dacă personajul a aterizat pe teren, să vedem dacă sunt îndeplinite condițiile de a apuca o muchie. Condițiile primare sunt următoarele:
dacă (mOnGround) // dacă nu există nici o stare de schimbare a mișcării în stare dacă (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Stand; mSpeed = Vector2.zero; mAudioSource.PlayOneShot (mHitWallSfx, 0.5f); else // fie mergeți la dreapta, fie mergeți la stânga sunt apăsate pentru a schimba starea pentru a merge mCurrentState = CharacterState.Walk; mSpeed.y = 0.0f; mAudioSource.PlayOneShot (mHitWallSfx, 0.5f); altfel dacă (mSpeed.y <= 0.0f && !mAtCeiling && ((mPushesRightWall && KeyState(KeyInput.GoRight)) || (mPushesLeftWall && KeyState(KeyInput.GoLeft))))
Dacă aceste trei condiții sunt îndeplinite, atunci trebuie să căutăm o margine de apucat. Să începem prin calcularea poziției de sus a senzorului, care va fi fie colțul din stânga sus sau colțul din dreapta sus al AABB.
Vector2 aabbCornerOffset; dacă (mPushesRightWall && mInputs [(int) KeyInput.GoRight]) aabbCornerOffset = mAABB.halfSize; altfel aabbCornerOffset = Vector2 nou (-mAABB.halfSize.x - 1.0f, mAABB.halfSize.y);
Acum, după cum vă puteți imagina, aici vom întâlni o problemă similară cu cea pe care am găsit-o la punerea în aplicare a controalelor de coliziune - în cazul în care caracterul se încadrează foarte rapid, este foarte probabil să pierdeți hotspot-ul la care poate apuca pervazul . De aceea va trebui să verificăm dacă țigla pe care trebuie să o luăm nu pornește de la colțul actual al cadrului, ci de cea anterioară - așa cum este ilustrat aici:
Imaginea de sus a unui personaj este poziția sa în cadrul anterior. În această situație, trebuie să începem să căutăm oportunități de a apuca o muchie din colțul din dreapta sus al AABB al cadrului precedent și să ne oprim la poziția actuală a cadrului.
Să obținem coordonatele plăcilor de care avem nevoie pentru a verifica, începând cu declararea variabilelor. Vom verifica dale într-o singură coloană, deci tot ce ne trebuie este coordonatele X ale coloanei, precum și coordonatele Y de sus și de jos.
int tileX, topY, bottomY;
Să obținem coordonatele X din colțul AABB.
int tileX, topY, bottomY; tileX = mMap.GetMapTileXAtPoint (mAABB.center.x + aabbCornerOffset.x);
Vrem să începem să căutăm o margine din poziția anterioară a cadrului numai dacă de fapt am fost deja în mișcare spre peretele împins în acel moment - deci poziția X a personajului nostru nu sa schimbat.
dacă ((mPushedLeftWall && mPushesLeftWall) || (mPushedRightWall && mPushesRightWall)) topY = mMap.GetMapTileYAtPoint (mOldPosition.y + mAABBOffset.y + aabbCornerOffset.y - Constants.cGrabLedgeStartY); bottomY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeEndY);
După cum puteți vedea, în acest caz, calculăm topY folosind poziția anterioară a cadrului, iar cea de jos utilizând cea a cadrului curent. Dacă nu eram lângă perete, atunci vom vedea pur și simplu dacă putem lua o muchie folosind doar poziția obiectului în cadrul actual.
dacă ((mPushedLeftWall && mPushesLeftWall) || (mPushedRightWall && mPushesRightWall)) topY = mMap.GetMapTileYAtPoint (mOldPosition.y + mAABBOffset.y + aabbCornerOffset.y - Constants.cGrabLedgeStartY); bottomY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeEndY); altceva topY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeStartY); bottomY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeEndY);
În regulă, acum că știm ce dale de verificat, putem începe să iterăm prin ele. Vom merge dinspre partea de sus spre partea de jos, pentru că această ordine are cel mai mult sens atunci când permitem scoaterea de la margine doar atunci când caracterul se încadrează.
pentru (int y = topY; y> = bottomY; --y)
Acum, să verificăm dacă tigla pe care o interpretăm îndeplinește condițiile care permit personajului să apucă o muchie. Condițiile, după cum sa explicat mai sus, sunt următoarele:
pentru (int y = topY; y> = bottomY; --y) if (! mMap.IsObstacle (tileX, y) && mMap.IsObstacle (tileX, y-1))
Următorul pas este să calculați poziția colțului plăcilor pe care vrem să le luăm. Acest lucru este destul de simplu - trebuie doar să luăm poziția plăcilor și apoi să o compensăm cu dimensiunea plăcilor.
dacă ! mMap.IsObstacle (tileX, y) && mMap.IsObstacle (tileX, y-1)) var tileCorner = mMap.GetMapTilePosition (tileX, y-1); tileCorner.x - = Mathf.Sign (aabbCornerOffset.x) * map.cTileSize / 2; tileCorner.y + = map.cTileSize / 2;
Acum, că știm acest lucru, ar trebui să verificăm dacă colțul se află între punctele noastre de senzori. Bineînțeles că vrem să facem acest lucru numai dacă verificăm țigla în ceea ce privește poziția actuală a cadrului, care este tigla cu coordonate Y egală cu cea de jos. Dacă nu este cazul, atunci putem presupune în siguranță că am trecut pervazul dintre cadrul anterior și cel actual - așa că dorim oricum să luăm pervazul.
dacă ! mMap.IsObstacle (tileX, y) && mMap.IsObstacle (tileX, y-1)) var tileCorner = mMap.GetMapTilePosition (tileX, y-1); tileCorner.x - = Mathf.Sign (aabbCornerOffset.x) * map.cTileSize / 2; tileCorner.y + = map.cTileSize / 2; dacă (y> bottomY || ((mAABB.center.y + aabbCornerOffset.y) - tileCorner.y <= Constants.cGrabLedgeEndY && tileCorner.y - (mAABB.center.y + aabbCornerOffset.y) >= Constants.cGrabLedgeStartY))
Acum, suntem acasă, am găsit muchia pe care vrem să o luăm. Mai întâi, să salvăm poziția plăcilor pervazului.
dacă (y> bottomY || ((mAABB.center.y + aabbCornerOffset.y) - tileCorner.y <= Constants.cGrabLedgeEndY && tileCorner.y - (mAABB.center.y + aabbCornerOffset.y) >= Constants.cGrabLedgeStartY)) mLedgeTile = nou Vector2i (tileX, y-1);
De asemenea, trebuie să aliniem caracterul cu marginea. Ceea ce vrem să facem este să aliniem partea superioară a senzorului de margine a personajului cu partea superioară a țiglei și apoi să o compensăm prin cGrabLedgeTileOffsetY
.
mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY;
În afară de aceasta, trebuie să facem lucruri precum setarea vitezei la zero și schimbarea stării CharacterState.GrabLedge
. După aceasta, putem rupe din bucla, pentru că nu mai are nici un rost să iterăm prin restul plăcilor.
mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY; mSpeed = Vector2.zero; mCurrentState = CharacterState.GrabLedge; pauză;
Asta va fi! Pernele pot fi acum detectate și apucate, deci acum trebuie doar să punem în aplicare GrabLedge
stat, pe care l-am omorât mai devreme.
Odată ce personajul apucă o margine, jucătorul are două opțiuni: poate să sară sau să coboare. Jumping funcționează normal; jucătorul apasă tasta de salt, iar forța saltului este identică cu forța aplicată la sărituri de la sol. Scăderea se face prin apăsarea butonului în jos sau a tastei direcționale care se îndreaptă spre margine.
Primul lucru pe care trebuie să-l faceți este să aflați dacă marginea este la stânga sau la dreapta personajului. Putem face acest lucru pentru că am salvat coordonatele marginii pe care personajul o apucă.
bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft;
Putem folosi acele informații pentru a determina dacă caracterul ar trebui să scadă de pe margine. Pentru a renunța, jucătorul trebuie să:
bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft; if (mInputs[(int)KeyInput.GoDown] || (mInputs[(int)KeyInput.GoLeft] && ledgeOnRight) || (mInputs[(int)KeyInput.GoRight] && ledgeOnLeft))
Există o mică avertizare aici. Luați în considerare o situație când țineți butonul de jos și butonul drept, când personajul se află pe o muchie în dreapta. Va duce la următoarea situație:
Problema aici este că personajul apucă marginea imediat după ce îl lasă.
O soluție simplă la aceasta este de a bloca mișcarea spre marginea unui cuplu de cadre după ce am scăpat de pe margine. Pentru aceasta, trebuie să adăugăm două variabile noi; să le sunăm mCannotGoLeftFrames
și mCannotGoRightFrames
.
public int mCannotGoLeftFrames = 0; public int mCannotGoRightFrames = 0;
Când personajul scade de pe muchie, trebuie să setăm acele variabile și să schimbăm starea pentru a sari.
bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft; if (mInputs[(int)KeyInput.GoDown] || (mInputs[(int)KeyInput.GoLeft] && ledgeOnRight) || (mInputs[(int)KeyInput.GoRight] && ledgeOnLeft)) if (ledgeOnLeft) mCannotGoLeftFrames = 3; else mCannotGoRightFrames = 3; mCurrentState = CharacterState.Jump;
Acum, să ne întoarcem puțin A sari
stat și să ne asigurăm că respectă interdicția noastră de a vă deplasa la stânga sau la dreapta după ce ați părăsit pervazul. Să resetați intrările imediat înainte de a verifica dacă ar trebui să căutăm o margine pentru a apuca.
dacă (mCannotGoLeftFrames> 0) --mCannotGoLeftFrames; mInputs [(int) KeyInput.GoLeft] = false; dacă (mCannotGoRightFrames> 0) --mCannotGoRightFrames; mInputs [(int) KeyInput.GoRight] = false; dacă (mSpeed.y <= 0.0f && !mAtCeiling && ((mPushesRightWall && mInputs[(int)KeyInput.GoRight]) || (mPushesLeftWall && mInputs[(int)KeyInput.GoLeft])))
Dupa cum vedeti, in acest fel nu vom indeplini conditiile necesare pentru a apuca o muchie atata timp cat directia blocata este aceeasi cu directia marginea pe care personajul ar putea incerca sa o apuca. De fiecare dată când refuzăm o anumită intrare, descrescăm din cadrele de blocare rămase, așa că în cele din urmă vom putea să ne mișcăm din nou - în cazul nostru, după 3 cadre.
Acum să continuăm să lucrăm la GrabLedge
stat. Din moment ce ne-am ocupat de abandonul de pe margine, acum trebuie să facem posibil să sarăm din poziția de apucare.
Dacă personajul nu a căzut de pe margine, trebuie să verificăm dacă tasta de salt a fost apăsată; dacă este așa, trebuie să setăm viteza verticală a saltului și să schimbăm starea:
dacă (mInputs [(int) KeyInput.GoDown] || (mInputs [(int) KeyInput.GoLeft] && ledgeOnRight) || (mInputs [(int) KeyInput.GoRight] && ledgeOnLeft)) if (ledgeOnLeft) mCannotGoLeftFrames = ; altfel mCannotGoRightFrames = 3; mCurrentState = CharacterState.Jump; altfel dacă (mInputs [(int) KeyInput.Jump]) mSpeed.y = mJumpSpeed; mCurrentState = CharacterState.Jump;
Cam asta e tot! Acum, grabbing-ul ar trebui să funcționeze corect în toate tipurile de situații.
Adesea, pentru a face sariturile mai ușoare în jocurile cu platformer, personajul este permis să sară dacă tocmai a ieșit de pe marginea unei platforme și nu mai este pe teren. Aceasta este o metodă populară pentru a diminua o iluzie că jucătorul a apăsat butonul de salt, dar personajul nu a sărit, ceea ce ar fi putut apărea din cauza întârzierii de intrare sau a playerului apăsând butonul de salt imediat după ce personajul sa mutat de pe platformă.
Să implementăm un astfel de mecanic acum. Mai întâi de toate, trebuie să adăugăm o constantă a numărului de cadre după ce caracterul se oprește de pe platformă, dar poate să efectueze un salt.
public const int cJumpFramesThreshold = 4;
Avem de asemenea nevoie de un contor de cadre în Caracter
clasa, astfel încât știm cât de multe cadre caracterul este în aer deja.
protejate int mFramesFromJumpStart = 0;
Acum, să lăsăm setul mFramesFromJumpStart
la 0 de fiecare dată când tocmai am părăsit terenul. Să facem asta imediat după ce sunăm UpdatePhysics
.
UpdatePhysics (); dacă (mWasOnGround &&! mOnGround) mFramesFromJumpStart = 0;
Și să creștem în fiecare cadru pe care suntem în starea de salt.
caz CharacterState.Jump: ++ mFramesFromJumpStart;
Dacă suntem în stare de salt, nu putem permite un salt în aer dacă suntem fie la tavan, fie avem o viteză verticală pozitivă. Viteza verticală pozitivă ar însemna că personajul nu a ratat un salt.
++mFramesFromJumpStart; dacă (mFramesFromJumpStart <= Constants.cJumpFramesThreshold) if (mAtCeiling || mSpeed.y > 0.0f) mFramesFromJumpStart = Constants.cJumpFramesThreshold + 1;
Dacă nu este cazul și tasta de salt este apăsată, tot ce trebuie să facem este să setăm viteza verticală la valoarea saltului, ca și cum am sărit în mod normal, chiar dacă personajul se află deja în starea de salt.
dacă (mFramesFromJumpStart <= Constants.cJumpFramesThreshold) if (mAtCeiling || mSpeed.y > 0.0f) mFramesFromJumpStart = Constants.cJumpFramesThreshold + 1; altfel dacă (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed;
Si asta e! Putem seta cJumpFramesThreshold
la o valoare mare ca 10 cadre pentru a vă asigura că funcționează.
Efectul de aici este destul de exagerat. Nu este foarte remarcabil dacă permitem personajului să sară doar 1-4 cadre, după ce nu mai este pe teren, dar în general acest lucru ne permite să modificăm cât de indulcit vrem să sară să fie.
Să facem posibilă ajustarea obiectelor. Avem deja mScale
în MovingObject
clasa, deci tot ce trebuie să facem este să ne asigurăm că afectează corect AABB și AABB.
Mai întâi, să editați clasa AABB astfel încât să aibă o componentă de scară.
public struct AABB scară publică Vector2; centrul public Vector2; public Vector2 halfSize; public AABB (Centrul Vector2, Vector2 halfSize) scale = Vector2.one; this.center = center; this.halfSize = halfSize;
Acum, să editați jumătate de măsură
, astfel încât atunci când le accesăm, obținem de fapt o dimensiune scalată în locul celei nesemnificative.
scară publică Vector2; centrul public Vector2; privat Vector2 halfSize; public Vector2 HalfSize set jumătateSize = valoare; get returnați vectorul Vector2 (halfSize.x * scale.x, halfSize.y * scale.y);
Vom dori, de asemenea, să obținem sau să setăm doar o valoare X sau Y de jumătate de mărime, deci trebuie să facem aceștia să obțină separatori și setteri separați.
float public HalfSizeX set halfSize.x = valoare; primi întoarcere halfSize.x * scale.x; public float HalfSizeY set halfSize.y = valoare; a obține return halfSize.y * scale.y;
Pe lângă diminuarea AABB-ului în sine, va trebui, de asemenea, să scarăm mAABBOffset
, astfel încât după scalarea obiectului, sprite-ul său se va potrivi încă cu AABB la fel cum a procedat atunci când obiectul a fost necorespunzător. Să ne întoarcem la MovingObject
clasa pentru ao edita.
privat vector2 mAABBOffset; public Vector2 AABBOffset set mAABBOffset = valoare; get returnați Vector2 nou (mAABBOffset.x * mScale.x, mAABBOffset.y * mScale.y);
La fel ca înainte, vom dori să avem acces separat la componentele X și Y.
plutarul public AABBOffsetX set mAABBOffset.x = valoare; primi return mAABBOffset.x * mScale.x; float public AABBOffsetY set mAABBOffset.y = valoare; get retur mAABBOffset.y * mScale.y;
În cele din urmă, trebuie, de asemenea, să ne asigurăm că atunci când scara este modificată în MovingObject
, acesta este, de asemenea, modificat în AABB. Scara obiectului poate fi negativă, însă AABB nu ar trebui să aibă o scală negativă deoarece ne bazăm pe jumătate pentru a fi întotdeauna pozitivă. De aceea, în loc de a trece pur și simplu scara la AABB, vom trece o scară care are toate componentele pozitive.
vector2 privat mScale; spectrul public Vector2 set mScale = valoare; mAABB.scale = Vector2 nou (Mathf.Abs (value.x), Mathf.Abs (valoare.y)); get return mScale; float public ScaleX set mScale.x = valoare; mAABB.scale.x = Mathf.Abs (valoare); get retur mScale.x; Scale public float set mScale.y = valoare; mAABB.scale.y = Mathf.Abs (valoare); get return mScale.y;
Tot ce trebuie să facem acum este să ne asigurăm că ori de câte ori am folosit variabilele în mod direct, le folosim acum prin intermediul getters și setters. Oriunde am folosit halfSize.x
, Vom folosi HalfSizeX
, oriunde am folosit halfSize.y
, Vom folosi HalfSizeY
, si asa mai departe. Câteva utilizări ale unei funcții de găsire și înlocuire ar trebui să se ocupe de acest lucru.
Scalarea ar trebui să funcționeze bine acum și, din cauza modului în care ne-am construit funcțiile de detectare a coliziunilor, nu contează dacă caracterul este gigantic sau mic - ar trebui să interacționeze cu harta.
Această parte concluzionează munca noastră cu harta țiglelor. În următoarele părți, vom seta lucrurile pentru a detecta coliziunile dintre obiecte.
A fost nevoie de ceva timp și efort, dar sistemul, în general, ar trebui să fie foarte robust. Un lucru care poate lipseste acum este suportul pentru pante. Multe jocuri nu se bazează pe ele, dar multe dintre ele fac, deci este cel mai mare obiectiv de îmbunătățire a acestui sistem. Vă mulțumim că ați citit până acum, vă vom vedea în partea următoare!