In questa parte della serie sulla fisica dei platform 2D, aggiungeremo il grassetto della sporgenza, i meccanismi di clemenza del salto e la capacità di ridimensionare l'oggetto.
Ora che possiamo saltare, scendere dalle piattaforme a senso unico e correre, possiamo anche implementare l'accostamento alla sporgenza. La meccanica dei Ledge-grabbing non è assolutamente un must in ogni gioco, ma è un metodo molto popolare per estendere la gamma di movimenti possibili di un giocatore senza fare qualcosa di estremo come un doppio salto.
Diamo un'occhiata a come determiniamo se una sporgenza può essere afferrata. Per determinare se il personaggio può afferrare una sporgenza, controlleremo costantemente il lato verso il quale il personaggio si sta muovendo. Se troviamo una tessera vuota nella parte superiore di AABB, e quindi una tessera solida sotto di essa, la parte superiore di tale tessera solida è la sporgenza sulla quale il nostro personaggio può aggrapparsi.
Andiamo al nostro Personaggio
classe, dove implementeremo la sporgenza della sporgenza. Non ha senso farlo nel MovingObject
classe, dal momento che la maggior parte degli oggetti non ha la possibilità di afferrare una sporgenza, quindi sarebbe un rifiuto fare qualsiasi elaborazione in quella direzione lì.
Per prima cosa, dobbiamo aggiungere un paio di costanti. Iniziamo creando le costanti di offset del sensore.
public const float cGrabLedgeStartY = 0.0f; public const float cGrabLedgeEndY = 2.0f;
Il cGrabLedgeStartY
e cGrabLedgeEndY
sono offset dalla parte superiore della AABB; il primo è il primo punto del sensore, mentre il secondo è il punto del sensore finale. Come puoi vedere, il personaggio dovrà trovare una sporgenza entro 2 pixel.
Abbiamo anche bisogno di una costante aggiuntiva per allineare il personaggio alla tessera che ha appena afferrato. Per il nostro personaggio, questo sarà impostato a -4.
public const float cGrabLedgeTileOffsetY = -4.0f;
A parte questo, vorremmo ricordare le coordinate della tessera che abbiamo afferrato. Salviamo quelli come variabile membro del personaggio.
public Vector2i mLedgeTile;
Dovremo vedere se possiamo afferrare la sporgenza dallo stato di salto, quindi andiamo là. Subito dopo aver controllato se il personaggio è atterrato a terra, vediamo se le condizioni per afferrare una sporgenza sono soddisfatte. Le condizioni primarie sono le seguenti:
if (mOnGround) // se non vi è alcun movimento cambia stato in piedi se (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Stand; mSpeed = Vector2.zero; mAudioSource.PlayOneShot (mHitWallSfx, 0.5f); else // vengono premuti a destra o a sinistra, quindi cambiamo lo stato per camminare mCurrentState = CharacterState.Walk; mSpeed.y = 0.0f; mAudioSource.PlayOneShot (mHitWallSfx, 0.5f); else if (mSpeed.y <= 0.0f && !mAtCeiling && ((mPushesRightWall && KeyState(KeyInput.GoRight)) || (mPushesLeftWall && KeyState(KeyInput.GoLeft))))
Se queste tre condizioni sono soddisfatte, allora dobbiamo cercare la sporgenza da afferrare. Iniziamo calcolando la posizione superiore del sensore, che sarà l'angolo in alto a sinistra o in alto a destra dell'AABB.
Vector2 aabbCornerOffset; if (mPushesRightWall && mInputs [(int) KeyInput.GoRight]) aabbCornerOffset = mAABB.halfSize; else aabbCornerOffset = new Vector2 (-mAABB.halfSize.x - 1.0f, mAABB.halfSize.y);
Ora, come puoi immaginare, qui incontreremo un problema simile a quello che abbiamo trovato durante l'implementazione dei controlli di collisione: se il personaggio sta cadendo molto velocemente, è molto probabile che manchi l'hotspot in cui può afferrare la sporgenza . Ecco perché dovremo controllare la tessera che dobbiamo afferrare non partendo dall'angolo del fotogramma corrente, ma quello precedente, come illustrato qui:
L'immagine in alto di un personaggio è la sua posizione nel frame precedente. In questa situazione, dobbiamo iniziare a cercare le opportunità per afferrare una sporgenza dall'angolo in alto a destra della AABB del frame precedente e fermarsi alla posizione corrente del frame.
Prendiamo le coordinate delle tessere che dobbiamo controllare, iniziando dichiarando le variabili. Controlleremo le tessere in una singola colonna, quindi tutto ciò di cui abbiamo bisogno è la coordinata X della colonna e le sue coordinate Y superiore e inferiore.
int tileX, topY, bottomY;
Prendiamo la coordinata X dell'angolo di AABB.
int tileX, topY, bottomY; tileX = mMap.GetMapTileXAtPoint (mAABB.center.x + aabbCornerOffset.x);
Vogliamo iniziare a cercare una sporgenza dalla posizione del frame precedente solo se in quel momento ci stavamo già muovendo verso il muro spinto, quindi la posizione X del nostro personaggio non è cambiata.
if ((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);
Come puoi vedere, in questo caso stiamo calcolando il TOPY usando la posizione del frame precedente, e quello in basso usando quello del frame corrente. Se non fossimo accanto a nessun muro, allora vedremo se riusciamo ad afferrare una sporgenza usando solo la posizione dell'oggetto nel fotogramma corrente.
if ((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); else topY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeStartY); bottomY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeEndY);
Bene, ora che sappiamo quali tessere controllare, possiamo iniziare a scorrere attraverso di esse. Andremo dall'alto verso il basso, perché questo ordine ha più senso in quanto permettiamo che la sporgenza si impigli solo quando il personaggio sta cadendo.
per (int y = topY; y> = bottomY; --y)
Ora controlliamo se la tessera che stiamo iterando soddisfa le condizioni che consentono al personaggio di afferrare una sporgenza. Le condizioni, come spiegato prima, sono le seguenti:
for (int y = topY; y> = bottomY; --y) if (! mMap.IsObstacle (tileX, y) && mMap.IsObstacle (tileX, y - 1))
Il prossimo passo è calcolare la posizione dell'angolo della piastrella che vogliamo afferrare. Questo è abbastanza semplice: abbiamo solo bisogno di ottenere la posizione della tessera e quindi compensarla con le dimensioni della tessera.
if (! 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;
Ora che lo sappiamo, dovremmo controllare se l'angolo si trova tra i nostri punti sensore. Ovviamente lo vogliamo fare solo se stiamo controllando la tessera relativa alla posizione attuale del frame, che è la tessera con la coordinata Y uguale a quella inferioreY. Se questo non è il caso, allora possiamo tranquillamente supporre che abbiamo passato la sporgenza tra il frame precedente e quello attuale, quindi vogliamo comunque afferrare la sporgenza.
if (! 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; if (y> bottomY || ((mAABB.center.y + aabbCornerOffset.y) - tileCorner.y <= Constants.cGrabLedgeEndY && tileCorner.y - (mAABB.center.y + aabbCornerOffset.y) >= Constants.cGrabLedgeStartY))
Ora siamo a casa, abbiamo trovato la sporgenza che vogliamo afferrare. Per prima cosa, salviamo la posizione della piastrella della sporgenza afferrata.
if (y> bottomY || ((mAABB.center.y + aabbCornerOffset.y) - tileCorner.y <= Constants.cGrabLedgeEndY && tileCorner.y - (mAABB.center.y + aabbCornerOffset.y) >= Constants.cGrabLedgeStartY)) mLedgeTile = new Vector2i (tileX, y - 1);
Abbiamo anche bisogno di allineare il personaggio con la sporgenza. Quello che vogliamo fare è allineare la parte superiore del sensore di sporgenza del personaggio con la parte superiore della tessera, e quindi compensare quella posizione per cGrabLedgeTileOffsetY
.
mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY;
A parte questo, dobbiamo fare cose come impostare la velocità a zero e cambiare lo stato in CharacterState.GrabLedge
. Dopo questo, possiamo uscire dal ciclo perché non ha senso scorrere le altre tessere.
mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY; mSpeed = Vector2.zero; mCurrentState = CharacterState.GrabLedge; rompere;
Questo sarà! Le sporgenze ora possono essere rilevate e afferrate, quindi ora abbiamo solo bisogno di implementare il GrabLedge
stato, che abbiamo saltato in precedenza.
Una volta che il personaggio ha afferrato una sporgenza, il giocatore ha due opzioni: possono saltare o scendere. Saltare funziona normalmente; il giocatore preme il tasto di salto e la forza del salto è identica alla forza applicata quando salta da terra. L'abbandono viene effettuato premendo il pulsante giù, o il tasto direzionale che punta lontano dalla sporgenza.
La prima cosa da fare è rilevare se la sporgenza si trova a sinistra oa destra del personaggio. Possiamo farlo perché abbiamo salvato le coordinate della sporgenza che il personaggio sta afferrando.
bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft;
Possiamo usare queste informazioni per determinare se il personaggio debba lasciare la sporgenza. Per scendere, il giocatore deve:
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))
C'è un piccolo avvertimento qui. Considera una situazione in cui stiamo tenendo premuto il pulsante Giù e il pulsante destro, quando il personaggio si trova su una sporgenza a destra. Risulterà nella seguente situazione:
Il problema qui è che il personaggio afferra la sporgenza immediatamente dopo averla lasciata andare.
Una soluzione semplice a questo è bloccare il movimento verso la sporgenza per un paio di fotogrammi dopo che siamo caduti dalla sporgenza. Per questo abbiamo bisogno di aggiungere due nuove variabili; chiamiamoli mCannotGoLeftFrames
e mCannotGoRightFrames
.
public int mCannotGoLeftFrames = 0; public int mCannotGoRightFrames = 0;
Quando il personaggio scende dalla sporgenza, dobbiamo impostare quelle variabili e cambiare lo stato per saltare.
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;
Ora torniamo per un po 'al Saltare
state, e assicuriamoci che rispetti il nostro divieto di spostarsi a sinistra oa destra dopo aver lasciato la sporgenza. Riportiamo gli input subito prima di controllare se dovremmo cercare una sporgenza da afferrare.
if (mCannotGoLeftFrames> 0) --mCannotGoLeftFrames; mInputs [(int) KeyInput.GoLeft] = false; if (mCannotGoRightFrames> 0) --mCannotGoRightFrames; mInputs [(int) KeyInput.GoRight] = false; if (mSpeed.y <= 0.0f && !mAtCeiling && ((mPushesRightWall && mInputs[(int)KeyInput.GoRight]) || (mPushesLeftWall && mInputs[(int)KeyInput.GoLeft])))
Come puoi vedere, in questo modo non soddisferemo le condizioni necessarie per afferrare una sporgenza fintanto che la direzione bloccata è la stessa della direzione della sporgenza che il personaggio potrebbe provare ad afferrare. Ogni volta che neghiamo un input particolare, decrementiamo dai restanti frame di blocco, quindi alla fine saremo in grado di spostarci di nuovo, nel nostro caso, dopo 3 frame.
Ora continuiamo a lavorare su GrabLedge
stato. Dal momento che abbiamo gestito la discesa dalla sporgenza, ora dobbiamo rendere possibile il salto dalla posizione di presa.
Se il personaggio non è caduto dalla sporgenza, dobbiamo controllare se è stato premuto il tasto di salto; se è così, dobbiamo impostare la velocità verticale del salto e cambiare lo stato:
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; else if (mInputs [(int) KeyInput.Jump]) mSpeed.y = mJumpSpeed; mCurrentState = CharacterState.Jump;
Questo è praticamente tutto! Ora l'accaparramento dovrebbe funzionare correttamente in tutti i tipi di situazioni.
Spesso, per rendere più facili i salti nei giochi platform, al personaggio è consentito saltare se è appena uscito dal bordo di una piattaforma e non è più a terra. Questo è un metodo popolare per mitigare l'illusione che il giocatore abbia premuto il pulsante di salto, ma il personaggio non ha saltato, il che potrebbe essere apparso a causa del ritardo di input o il giocatore che preme il pulsante di salto subito dopo che il personaggio si è allontanato dalla piattaforma.
Implementiamo una meccanica del genere ora. Prima di tutto, dobbiamo aggiungere una costante di quanti fotogrammi dopo che il personaggio esce dalla piattaforma può ancora eseguire un salto.
public const int cJumpFramesThreshold = 4;
Avremo anche bisogno di un contatore di frame nel Personaggio
classe, quindi sappiamo quanti fotogrammi il personaggio è già nell'aria.
protected int mFramesFromJumpStart = 0;
Ora impostiamo il mFramesFromJumpStart
a 0 ogni volta che abbiamo appena lasciato il terreno. Facciamolo subito dopo aver chiamato UpdatePhysics
.
UpdatePhysics (); if (mWasOnGround &&! mOnGround) mFramesFromJumpStart = 0;
E incrementiamo ogni frame in cui ci troviamo nello stato di salto.
case CharacterState.Jump: ++ mFramesFromJumpStart;
Se siamo nello stato di salto, non possiamo permettere un salto in aria se siamo al soffitto o se avremo una velocità verticale positiva. La velocità verticale positiva significherebbe che il personaggio non ha perso un salto.
++mFramesFromJumpStart; if (mFramesFromJumpStart <= Constants.cJumpFramesThreshold) if (mAtCeiling || mSpeed.y > 0.0f) mFramesFromJumpStart = Constants.cJumpFramesThreshold + 1;
Se questo non è il caso e il tasto di salto viene premuto, tutto ciò che dobbiamo fare è impostare la velocità verticale sul valore di salto, come se saltassimo normalmente, anche se il personaggio si trova già nello stato di salto.
if (mFramesFromJumpStart <= Constants.cJumpFramesThreshold) if (mAtCeiling || mSpeed.y > 0.0f) mFramesFromJumpStart = Constants.cJumpFramesThreshold + 1; else if (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed;
E questo è tutto! Possiamo impostare il cJumpFramesThreshold
ad un grande valore come 10 fotogrammi per assicurarsi che funzioni.
L'effetto qui è abbastanza esagerato. Non è molto evidente se permettiamo al personaggio di saltare solo 1-4 fotogrammi dopo che in realtà non è più a terra, ma nel complesso questo ci permette di modificare quanto volentieri vogliamo che i nostri salti siano.
Rendiamo possibile scalare gli oggetti. Abbiamo già il mScale
nel MovingObject
classe, quindi tutto ciò che dobbiamo effettivamente fare è assicurarsi che influenzi correttamente l'AABB e l'offset AABB.
Prima di tutto, modifichiamo la nostra classe AABB in modo che abbia un componente di scala.
struttura pubblica AABB scala pubblica Vector2; centro pubblico Vector2; halfSize pubblico Vector2; public AABB (Vector2 center, Vector2 halfSize) scale = Vector2.one; this.center = center; this.halfSize = halfSize;
Ora modifichiamo il mezza misura
, in modo che quando ci accediamo, in realtà otteniamo una dimensione ridimensionata invece di quella non graduata.
scala pubblica Vector2; centro pubblico Vector2; halfSize privato Vector2; public Vector2 HalfSize set halfSize = valore; get return new Vector2 (halfSize.x * scale.x, halfSize.y * scale.y);
Vogliamo anche essere in grado di ottenere o impostare solo un valore X o Y della mezza dimensione, quindi dobbiamo fare getter e setter separati anche per quelli.
public float HalfSizeX set halfSize.x = value; get return halfSize.x * scale.x; public float HalfSizeY set halfSize.y = value; get return halfSize.y * scale.y;
Oltre a ridimensionare la stessa AABB, avremo anche bisogno di ridimensionare il file mAABBOffset
, in modo tale che dopo aver scalato l'oggetto, il suo sprite corrisponderà comunque all'AABB nello stesso modo in cui lo faceva quando l'oggetto non era in scala. Torniamo al MovingObject
classe per modificarlo.
private Vector2 mAABBOffset; public Vector2 AABBOffset set mAABBOffset = value; get return new Vector2 (mAABBOffset.x * mScale.x, mAABBOffset.y * mScale.y);
Come prima, vorremmo avere accesso anche ai componenti X e Y separatamente.
public float AABBOffsetX set mAABBOffset.x = value; get return mAABBOffset.x * mScale.x; public float AABBOffsetY set mAABBOffset.y = value; get return mAABBOffset.y * mScale.y;
Infine, dobbiamo anche assicurarci che quando la scala viene modificata nel MovingObject
, è anche modificato in AABB. La scala dell'oggetto può essere negativa, ma lo stesso AABB non dovrebbe avere una scala negativa, perché ci basiamo sulla mezza dimensione per essere sempre positivi. Ecco perché invece di passare semplicemente la scala all'AABB, passeremo una scala che ha tutte le componenti positive.
private Vector2 mScale; public Vector2 Scale set mScale = value; mAABB.scale = new Vector2 (Mathf.Abs (value.x), Mathf.Abs (value.y)); get return mScale; public float ScaleX set mScale.x = value; mAABB.scale.x = Mathf.Abs (valore); get return mScale.x; public float ScaleY set mScale.y = value; mAABB.scale.y = Mathf.Abs (valore); get return mScale.y;
Tutto ciò che resta da fare ora è assicurarsi che, ovunque siano state utilizzate direttamente le variabili, le usiamo ora attraverso i getter e i setter. Ovunque abbiamo usato halfSize.x
, vorremmo usare HalfSizeX
, ovunque abbiamo usato halfSize.y
, vorremmo usare HalfSizeY
, e così via. Alcuni usi di una funzione di ricerca e sostituzione dovrebbero occuparsi di questo bene.
Il ridimensionamento dovrebbe funzionare bene ora e, a causa del modo in cui abbiamo costruito le nostre funzioni di rilevamento delle collisioni, non importa se il personaggio è gigante o minuscolo, dovrebbe interagire bene con la mappa.
Questa parte conclude il nostro lavoro con la tilemap. Nelle parti successive, imposteremo le cose per rilevare le collisioni tra gli oggetti.
Ci sono voluti tempo e sforzi, ma il sistema in generale dovrebbe essere molto robusto. Una cosa che potrebbe mancare al momento è il supporto per le piste. Molti giochi non si basano su di essi, ma molti lo fanno, quindi questo è il più grande obiettivo di miglioramento di questo sistema. Grazie per aver letto fino ad ora, ci vediamo nella prossima parte!