Fisica di base 2D per piattaforme, parte 3

Piattaforme a una via

Dato che abbiamo appena finito di lavorare sul controllo della collisione sul terreno, potremmo anche aggiungere piattaforme a senso unico mentre ci siamo. In ogni caso, riguarderanno solo il controllo di collisione sul terreno. Le piattaforme a una via differiscono dai blocchi solidi in quanto fermano un oggetto solo se cade. Inoltre, consentiremo anche a un personaggio di scendere da tale piattaforma.

Prima di tutto, quando vogliamo abbandonare una piattaforma a senso unico, fondamentalmente vogliamo ignorare la collisione con il terreno. Una facile via d'uscita è impostare un offset, dopo il quale il personaggio o l'oggetto non si scontreranno più con una piattaforma. 

Ad esempio, se il personaggio è già due pixel sotto la parte superiore della piattaforma, non dovrebbe più rilevare una collisione. In tal caso, quando vogliamo lasciare la piattaforma, tutto ciò che dobbiamo fare è spostare il carattere di due pixel verso il basso. Creiamo questa costante di offset.

public const float cOneWayPlatformThreshold = 2.0f;

Ora aggiungiamo una variabile che ci farà sapere se un oggetto è attualmente su una piattaforma a senso unico.

public bool mOnOneWayPlatform = false;

Modifichiamo la definizione di HasGround funzione di prendere anche un riferimento a un booleano che verrà impostato se l'oggetto è atterrato su una piattaforma a una via.

HasGround pubblico booleano (Vector2 oldPosition, posizione Vector2, velocità Vector2, galleggiante esterno, rif bool su OneWayPlatform)

Ora, dopo aver controllato se il riquadro in cui ci troviamo attualmente è un ostacolo, e non lo è, dovremmo controllare se si tratta di una piattaforma a senso unico.

if (mMap.IsObstacle (tileIndexX, tileIndexY)) restituisce true; altrimenti se (mMap.IsOneWayPlatform (tileIndexX, tileIndexY)) onOneWayPlatform = true;

Come spiegato in precedenza, dobbiamo anche assicurarci che questa collisione venga ignorata se siamo caduti oltre il cOneWayPlatformThreshold sotto la piattaforma. 

Naturalmente, non possiamo semplicemente confrontare la differenza tra la parte superiore della piastrella e il sensore, perché è facile immaginare che anche se cadiamo, potremmo andare ben al di sotto di due pixel dalla parte superiore della piattaforma. Per le piattaforme a senso unico per fermare un oggetto, vogliamo che la distanza del sensore tra la parte superiore della piastrella e il sensore sia inferiore o uguale al cOneWayPlatformThreshold più l'offset dalla posizione di questo frame al precedente.

if (mMap.IsObstacle (tileIndexX, tileIndexY)) restituisce true; altrimenti se (mMap.IsOneWayPlatform (tileIndexX, tileIndexY) && Mathf.Abs (checkedTile.y - groundY) <= Constants.cOneWayPlatformThreshold + mOldPosition.y - position.y) onOneWayPlatform = true;

Infine, c'è ancora una cosa da considerare. Quando troviamo una piattaforma unidirezionale, non possiamo davvero uscire dal ciclo, perché ci sono situazioni in cui il personaggio è parzialmente su una piattaforma e parzialmente su un blocco solido.

Non dovremmo davvero prendere in considerazione una posizione come "su una piattaforma a senso unico", perché non possiamo davvero scendere da lì - il blocco solido ci sta fermando. Ecco perché prima dobbiamo cercare un blocco solido, e se ne viene trovato uno prima di restituire il risultato, dobbiamo anche impostare onOneWayPlatform a falso.

if (mMap.IsObstacle (tileIndexX, tileIndexY)) onOneWayPlatform = false; ritorna vero;  else if (mMap.IsOneWayPlatform (tileIndexX, tileIndexY) && Mathf.Abs (checkedTile.y - groundY) <= Constants.cOneWayPlatformThreshold + mOldPosition.y - position.y) onOneWayPlatform = true;

Ora, se avessimo esaminato tutte le tessere che dovevamo controllare orizzontalmente e abbiamo trovato una piattaforma unidirezionale ma non blocchi solidi, allora possiamo essere sicuri che siamo su una piattaforma a senso unico da cui possiamo scendere.

if (mMap.IsObstacle (tileIndexX, tileIndexY)) onOneWayPlatform = false; ritorna vero;  else if (mMap.IsOneWayPlatform (tileIndexX, tileIndexY) && Mathf.Abs (checkedTile.y - groundY) <= Constants.cOneWayPlatformThreshold + mOldPosition.y - position.y) onOneWayPlatform = true; if (checkedTile.x >= bottomRight.x) if (onOneWayPlatform) restituisce true; rompere; 

Questo è tutto, quindi ora aggiungiamo alla classe dei personaggi un'opzione per far cadere la piattaforma. In entrambi gli stati stand and run, dobbiamo aggiungere il seguente codice.

if (KeyState (KeyInput.GoDown)) if (mOnOneWayPlatform) mPosition.y - = Constants.cOneWayPlatformThreshold; 

Vediamo come funziona.

Tutto funziona correttamente.

Gestire le collisioni per il soffitto

Dobbiamo creare una funzione analoga a HasGround per ciascun lato dell'AABB, quindi iniziamo dal soffitto. Le differenze sono le seguenti:

  • La linea del sensore è sopra l'AABB invece di essere sotto di esso
  • Controlliamo il pannello del controsoffitto dal basso verso l'alto, mentre stiamo salendo
  • Non c'è bisogno di gestire piattaforme a senso unico

Ecco la funzione modificata.

public bool HasCeiling (Vector2 oldPosition, Vector2 position, out float ceilingY) var center = position + mAABBOffset; var oldCenter = oldPosition + mAABBOffset; ceilingY = 0.0f; var oldTopRight = oldCenter + mAABB.halfSize + Vector2.up - Vector2.right; var newTopRight = center + mAABB.halfSize + Vector2.up - Vector2.right; var newTopLeft = new Vector2 (newTopRight.x - mAABB.halfSize.x * 2.0f + 2.0f, newTopRight.y); int endY = mMap.GetMapTileYAtPoint (newTopRight.y); int begY = Mathf.Min (mMap.GetMapTileYAtPoint (oldTopRight.y) + 1, endY); int dist = Mathf.Max (Mathf.Abs (endY - begY), 1); int tileIndexX; per (int tileIndexY = begY; tileIndexY <= endY; ++tileIndexY)  var topRight = Vector2.Lerp(newTopRight, oldTopRight, (float)Mathf.Abs(endY - tileIndexY) / dist); var topLeft = new Vector2(topRight.x - mAABB.halfSize.x * 2.0f + 2.0f, topRight.y); for (var checkedTile = topLeft; ; checkedTile.x += Map.cTileSize)  checkedTile.x = Mathf.Min(checkedTile.x, topRight.x); tileIndexX = mMap.GetMapTileXAtPoint(checkedTile.x); if (mMap.IsObstacle(tileIndexX, tileIndexY))  ceilingY = (float)tileIndexY * Map.cTileSize - Map.cTileSize / 2.0f + mMap.mPosition.y; return true;  if (checkedTile.x >= topRight.x) interruzione;  return false; 

Gestire le collisioni per la parete sinistra

Analogamente al modo in cui abbiamo gestito il controllo di collisione per il soffitto e il terreno, dobbiamo anche verificare se l'oggetto è in collisione con il muro a sinistra o il muro a destra. Iniziamo dalla parete sinistra. L'idea qui è praticamente la stessa, ma ci sono alcune differenze:

  • La linea del sensore si trova sul lato sinistro di AABB.
  • L'interno per loop ha bisogno di scorrere le tessere verticalmente, perché il sensore ora è una linea verticale.
  • Il ciclo esterno ha bisogno di scorrere le tessere orizzontalmente per vedere se non abbiamo saltato un muro quando ci si muove con una grande velocità orizzontale.
bool pubblico CollidesWithLeftWall (Vector2 oldPosition, posizione Vector2, uscita float wallX) var center = position + mAABBOffset; var oldCenter = oldPosition + mAABBOffset; wallX = 0.0f; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.right; var newBottomLeft = center - mAABB.halfSize - Vector2.right; var newTopLeft = newBottomLeft + new Vector2 (0.0f, mAABB.halfSize.y * 2.0f); int tileIndexY; var endX = mMap.GetMapTileXAtPoint (newBottomLeft.x); var begX = Mathf.Max (mMap.GetMapTileXAtPoint (oldBottomLeft.x) - 1, endX); int dist = Mathf.Max (Mathf.Abs (endX - begX), 1); for (int tileIndexX = begX; tileIndexX> = endX; --tileIndexX) var bottomLeft = Vector2.Lerp (newBottomLeft, oldBottomLeft, (float) Mathf.Abs (endX - tileIndexX) / dist); var topLeft = bottomLeft + new Vector2 (0.0f, mAABB.halfSize.y * 2.0f); for (var checkedTile = bottomLeft;; checkedTile.y + = Map.cTileSize) checkedTile.y = Mathf.Min (checkedTile.y, topLeft.y); tileIndexY = mMap.GetMapTileYAtPoint (checkedTile.y); if (mMap.IsObstacle (tileIndexX, tileIndexY)) wallX = (float) tileIndexX * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.x; ritorna vero;  se break (checkedTile.y> = topLeft.y);  return false; 

Gestire le collisioni per la parete destra

Finalmente, creiamo il CollidesWithRightWall funzione, che come potete immaginare, farà una cosa molto simile a CollidesWithLeftWall, ma invece di usare un sensore a sinistra, useremo un sensore sul lato destro del personaggio. 

L'altra differenza qui è che invece di controllare le tessere da destra a sinistra, li controlleremo da sinistra a destra, poiché questa è la direzione in movimento assunta.

public bool CollidesWithRightWall (Vector2 oldPosition, Posizione Vector2, float wallX) var center = position + mAABBOffset; var oldCenter = oldPosition + mAABBOffset; wallX = 0.0f; var oldBottomRight = oldCenter + new Vector2 (mAABB.halfSize.x, -mAABB.halfSize.y) + Vector2.right; var newBottomRight = center + new Vector2 (mAABB.halfSize.x, -mAABB.halfSize.y) + Vector2.right; var newTopRight = newBottomRight + new Vector2 (0.0f, mAABB.halfSize.y * 2.0f); var endX = mMap.GetMapTileXAtPoint (newBottomRight.x); var begX = Mathf.Min (mMap.GetMapTileXAtPoint (oldBottomRight.x) + 1, endX); int dist = Mathf.Max (Mathf.Abs (endX - begX), 1); int tileIndexY; for (int tileIndexX = begX; tileIndexX <= endX; ++tileIndexX)  var bottomRight = Vector2.Lerp(newBottomRight, oldBottomRight, (float)Mathf.Abs(endX - tileIndexX) / dist); var topRight = bottomRight + new Vector2(0.0f, mAABB.halfSize.y * 2.0f); for (var checkedTile = bottomRight; ; checkedTile.y += Map.cTileSize)  checkedTile.y = Mathf.Min(checkedTile.y, topRight.y); tileIndexY = mMap.GetMapTileYAtPoint(checkedTile.y); if (mMap.IsObstacle(tileIndexX, tileIndexY))  wallX = (float)tileIndexX * Map.cTileSize - Map.cTileSize / 2.0f + mMap.mPosition.x; return true;  if (checkedTile.y >= topRight.y) interruzione;  return false; 

Sposta l'oggetto fuori dalla collisione

Tutte le nostre funzioni di rilevamento collisioni sono fatte, quindi usiamole per completare la risposta di collisione contro la tilemap. Prima di farlo, però, dobbiamo capire l'ordine in cui controlleremo le collisioni. Consideriamo le seguenti situazioni.

In entrambe queste situazioni, possiamo vedere che il personaggio ha finito per sovrapporsi a una tessera, ma dobbiamo capire come risolvere la sovrapposizione. 

La situazione a sinistra è piuttosto semplice: possiamo vedere che stiamo cadendo verso il basso, e per questo dovremmo sicuramente atterrare sul blocco. 

La situazione a destra è un po 'più complicata, poiché in realtà potremmo atterrare proprio sull'angolo della tessera, e spingere il personaggio verso l'alto è ragionevole quanto spingerlo a destra. Scegliamo di dare la priorità al movimento orizzontale. Non importa molto quale sia l'allineamento che desideriamo fare per primo; entrambe le scelte sembrano corrette in azione.

Andiamo al nostro UpdatePhysics funzione e aggiungere le variabili che manterranno i risultati delle nostre query di collisione.

float groundY = 0.0f, ceilingY = 0.0f; float rightWallX = 0.0f, leftWallX = 0.0f;

Ora iniziamo osservando se dovremmo spostare l'oggetto a destra. Le condizioni qui sono che:

  • la velocità orizzontale è inferiore o uguale a zero
  • ci scontriamo con la parete sinistra 
  • nel fotogramma precedente non abbiamo sovrapposto la tessera sull'asse orizzontale, una situazione simile a quella a destra nella figura sopra

L'ultimo è una condizione necessaria, perché se non fosse soddisfatto avremmo a che fare con una situazione simile a quella a sinistra nella figura sopra, nella quale sicuramente non dovremmo spostare il personaggio a destra.

if (mSpeed.x <= 0.0f && CollidesWithLeftWall(mOldPosition, mPosition, out leftWallX) && mOldPosition.x - mAABB.halfSize.x + mAABBOffset.x >= leftWallX) 

Se le condizioni sono vere, dobbiamo allineare il lato sinistro del nostro AABB al lato destro della tessera, assicurarci di smettere di spostarci a sinistra e segnare che siamo vicini al muro a sinistra.

if (mSpeed.x <= 0.0f && CollidesWithLeftWall(mOldPosition, mPosition, out leftWallX) && mOldPosition.x - mAABB.halfSize.x + mAABBOffset.x >= leftWallX) mPosition.x = leftWallX + mAABB.halfSize.x - mAABBOffset.x; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f); mPushesLeftWall = true; 

Se una delle condizioni oltre l'ultima è falsa, dobbiamo impostare mPushesLeftWall a falso. Questo perché l'ultima condizione è falsa non necessariamente ci dice che il personaggio non sta spingendo il muro, ma al contrario, ci dice che era in collisione con esso già nel frame precedente. Per questo motivo è meglio cambiare mPushesLeftWall false solo se una delle prime due condizioni è falsa.

if (mSpeed.x <= 0.0f && CollidesWithLeftWall(mOldPosition, mPosition, out leftWallX))  if (mOldPosition.x - mAABB.HalfSizeX + AABBOffsetX >= leftWallX) mPosition.x = leftWallX + mAABB.HalfSizeX - AABBOffsetX; mPushesLeftWall = true;  mSpeed.x = Mathf.Max (mSpeed.x, 0.0f);  else mPushesLeftWall = false;

Ora controlliamo la collisione con la parete destra.

if (mSpeed.x> = 0.0f && CollidesWithRightWall (mOldPosition, mPosition, out rightWallX)) if (mOldPosition.x + mAABB.HalfSizeX + AABBOffsetX <= rightWallX)  mPosition.x = rightWallX - mAABB.HalfSizeX - AABBOffsetX; mPushesRightWall = true;  mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);  else mPushesRightWall = false;

Come puoi vedere, è la stessa formula che abbiamo usato per controllare la collisione con la parete sinistra, ma con mirroring.

Abbiamo già il codice per controllare la collisione con il terreno, quindi dopo quello dobbiamo controllare la collisione con il soffitto. Niente di nuovo anche qui, in più non abbiamo bisogno di fare ulteriori controlli eccetto che la velocità verticale deve essere maggiore o uguale a zero e in realtà entriamo in collisione con una tessera che è sopra di noi.

if (mSpeed.y> = 0.0f && HasCeiling (mOldPosition, mPosition, out ceilingY)) mPosition.y = ceilingY - mAABB.halfSize.y - mAABBOffset.y - 1.0f; mSpeed.y = 0.0f; mAtCeiling = true;  else mAtCeiling = false;

Arrotonda gli angoli

Prima di testare se funzionano le risposte di collisione, c'è una cosa più importante da fare, ovvero arrotondare i valori degli angoli che calcoliamo per i controlli di collisione. Dobbiamo farlo, in modo che i nostri controlli non vengano distrutti da errori in virgola mobile, che potrebbero derivare dalla strana posizione della mappa, dalla scala dei caratteri o solo da una strana dimensione AABB.

Innanzitutto, per nostra comodità, creiamo una funzione che trasforma un vettore di galleggianti in un vettore di galleggianti arrotondati.

Vector2 RoundVector (Vector2 v) return new Vector2 (Mathf.Round (v.x), Mathf.Round (v.y)); 

Ora usiamo questa funzione in ogni controllo di collisione. Per prima cosa, sistemiamo il HasCeiling funzione.

var oldTopRight = RoundVector (oldCenter + mAABB.HalfSize + Vector2.up - Vector2.right); var newTopRight = RoundVector (centro + mAABB.HalfSize + Vector2.up - Vector2.right); var newTopLeft = RoundVector (nuovo Vector2 (newTopRight.x - mAABB.HalfSizeX * 2.0f + 2.0f, newTopRight.y));

Il prossimo è A terra.

var oldBottomLeft = RoundVector (oldCenter - mAABB.HalfSize - Vector2.up + Vector2.right); var newBottomLeft = RoundVector (centro - mAABB.HalfSize - Vector2.up + Vector2.right); var newBottomRight = RoundVector (nuovo Vector2 (newBottomLeft.x + mAABB.HalfSizeX * 2.0f - 2.0f, newBottomLeft.y));

PushesRightWall.

var oldBottomRight = RoundVector (oldCenter + new Vector2 (mAABB.HalfSizeX, -mAABB.HalfSizeY) + Vector2.right); var newBottomRight = RoundVector (center + new Vector2 (mAABB.HalfSizeX, -mAABB.HalfSizeY) + Vector2.right); var newTopRight = RoundVector (newBottomRight + new Vector2 (0.0f, mAABB.HalfSizeY * 2.0f));

E infine, PushesLeftWall.

var oldBottomLeft = RoundVector (oldCenter - mAABB.HalfSize - Vector2.right); var newBottomLeft = RoundVector (center - mAABB.HalfSize - Vector2.right); var newTopLeft = RoundVector (newBottomLeft + new Vector2 (0.0f, mAABB.HalfSizeY * 2.0f));

Questo dovrebbe risolvere i nostri problemi!

Controlla i risultati

Sarà così. Proviamo ora come funzionano le nostre collisioni.

Sommario

Questo è tutto per questa parte! Abbiamo una serie di collisioni tilemap completamente funzionante, che dovrebbe essere molto affidabile. Sappiamo in quale posizione si trova attualmente l'oggetto: se è a terra, tocca una tessera a sinistra o a destra, o urtando un soffitto. Abbiamo anche implementato le piattaforme a senso unico, che sono uno strumento molto importante in ogni gioco platform. 

Nella parte successiva, aggiungeremo meccaniche per la presa di spigolo, che aumenteranno ulteriormente il possibile movimento del personaggio, quindi rimanete sintonizzati!