Fisica di base 2D per piattaforme, parte 4

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.

Ledge Grabbing

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.

Impostazione delle variabili

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;

Implementazione

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:

  • La velocità verticale è inferiore o uguale a zero (il personaggio sta cadendo).
  • Il personaggio non è al soffitto - non serve afferrare una sporgenza se non puoi saltarci sopra.
  • Il personaggio si scontra con il muro e si muove verso di esso.
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:

  • La tessera è vuota.
  • La tessera sottostante è una tessera solida (questa è la tessera che vogliamo afferrare).
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.

Controlli Grab di sporgenza

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.

Implementazione dei controlli

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:

  • premere il pulsante Giù
  • premere il tasto sinistro quando stiamo afferrando una sporgenza sulla destra, o
  • premi il tasto destro quando afferriamo una sporgenza sulla sinistra
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.

Lascia che il personaggio salti subito dopo aver lasciato una piattaforma

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.

Ridimensionamento degli oggetti

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.

Controlla i risultati

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.

Sommario

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!