Nella puntata precedente della serie, abbiamo implementato un meccanismo di rilevamento delle collisioni tra gli oggetti del gioco. In questa parte, utilizzeremo il meccanismo di rilevamento delle collisioni per costruire una risposta fisica semplice ma robusta tra gli oggetti.
La demo mostra il risultato finale di questo tutorial. Usa WASD per spostare il personaggio. Il pulsante centrale del mouse genera una piattaforma unidirezionale, il pulsante destro del mouse genera una tessera solida e la barra spaziatrice genera un clone di carattere. I cursori cambiano le dimensioni del personaggio del giocatore.
La demo è stata pubblicata sotto Unity 5.4.0f3 e il codice sorgente è anche compatibile con questa versione di Unity.
Ora che abbiamo tutti i dati di collisione del lavoro svolto nella parte precedente, possiamo aggiungere una semplice risposta agli oggetti in collisione. Il nostro obiettivo qui è di rendere possibile che gli oggetti non passino attraverso l'un l'altro come se fossero su un piano diverso: vogliamo che siano solidi e fungano da ostacolo o da piattaforma per altri oggetti. Per questo, dobbiamo fare solo una cosa: spostare l'oggetto da una sovrapposizione se si verifica uno.
Avremo bisogno di alcuni dati aggiuntivi per il MovingObject
classe per gestire la risposta all'oggetto e all'oggetto. Prima di tutto, è bello avere un booleano per contrassegnare un oggetto come cinematico, cioè questo oggetto non verrà spinto da nessun altro oggetto.
Questi oggetti funzionano bene come piattaforme e possono anche essere piattaforme mobili. Si suppone che siano le cose più pesanti intorno, quindi la loro posizione non sarà corretta in alcun modo - gli altri oggetti dovranno spostarsi per fare spazio a loro.
public bool mIsKinematic = false;
Gli altri dati che mi piacciono sono le informazioni su se siamo in cima a un oggetto o alla sua parte sinistra o destra, ecc. Finora abbiamo potuto interagire solo con le tessere, ma ora possiamo anche interagire con altri oggetti.
Per portare un po 'di armonia in questo, avremo bisogno di un nuovo set di variabili che descrivano se il personaggio sta spingendo qualcosa a sinistra, a destra, in alto o in basso.
public bool mPushesRight = false; public bool mPushesLeft = false; public bool mPushesBottom = false; public bool mPushesTop = false; public bool mPushedTop = false; public bool mPushedBottom = false; public bool mPushedRight = false; public bool mPushedLeft = false; public bool mPushesLeftObject = false; public bool mPushesRightObject = false; public bool mPushesBottomObject = false; public bool mPushesTopObject = false; public bool mPushedLeftObject = false; public bool mPushedRightObject = false; public bool mPushedBottomObject = false; public bool mPushedTopObject = false; public bool mPushesRightTile = false; public bool mPushesLeftTile = false; public bool mPushesBottomTile = false; public bool mPushesTopTile = false; public bool mPushedTopTile = false; public bool mPushedBottomTile = false; public bool mPushedRightTile = false; public bool mPushedLeftTile = false;
Ora ci sono molte variabili. In un ambiente di produzione varrebbe la pena di trasformarli in bandiere e di avere un solo numero intero invece di tutti questi booleani, ma per semplicità gestiremo congedandoli così come sono.
Come potresti notare, qui abbiamo dati abbastanza dettagliati. Sappiamo se il personaggio spinge o spinge un ostacolo in una determinata direzione in generale, ma possiamo anche facilmente chiedere se siamo accanto a una tessera o un oggetto.
Creiamo il UpdatePhysicsResponse
funzione, in cui gestiremo la risposta oggetto-oggetto.
private void UpdatePhysicsResponse ()
Prima di tutto, se l'oggetto è contrassegnato come cinematico, torniamo semplicemente. Non gestiamo la risposta perché l'oggetto cinematico non deve rispondere a nessun altro oggetto, gli altri oggetti devono rispondere ad esso.
if (mIsKinematic) return;
Ora, supponiamo che non avremo bisogno di un oggetto cinematico per avere i dati corretti riguardo al fatto che stia spingendo un oggetto sul lato sinistro, ecc. Se non è così, allora questo dovrebbe essere modificato un po ', cosa che Toccheremo più tardi lungo la linea.
Ora iniziamo a gestire le variabili che abbiamo appena dichiarato.
mPushedBottomObject = mPushesBottomObject; mPushedRightObject = mPushesRightObject; mPushedLeftObject = mPushesLeftObject; mPushedTopObject = mPushesTopObject; mPushesBottomObject = false; mPushesRightObject = false; mPushesLeftObject = false; mPushesTopObject = false;
Salviamo i risultati del frame precedente sulle variabili appropriate e per ora assumiamo che non stiamo toccando nessun altro oggetto.
Iniziamo ora a scorrere tutti i nostri dati di collisione.
per (int i = 0; i < mAllCollidingObjects.Count; ++i) var other = mAllCollidingObjects[i].other; var data = mAllCollidingObjects[i]; var overlap = data.overlap;
Prima di tutto, gestiamo i casi in cui gli oggetti si toccano a malapena l'un l'altro, non proprio sovrapposti. In questo caso, sappiamo che non dobbiamo spostare nulla, basta impostare le variabili.
Come accennato prima, l'indicatore che gli oggetti stanno toccando è che la sovrapposizione su uno degli assi è uguale a 0. Cominciamo controllando l'asse x.
if (overlap.x == 0.0f)
Se la condizione è vera, dobbiamo vedere se l'altro oggetto si trova sul lato sinistro o destro del nostro AABB.
if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) else
Infine, se è a destra, imposta il mPushesRightObject
true e imposta la velocità in modo che non sia maggiore di 0, perché il nostro oggetto non può più spostarsi a destra mentre il percorso è bloccato.
if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f); altro
Gestiamo il lato sinistro allo stesso modo.
if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f);
Infine, sappiamo che non avremo bisogno di fare altro qui, quindi continuiamo con l'iterazione del ciclo successivo.
if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f); Continua;
Gestiamo l'asse y allo stesso modo.
if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f); Continua; else if (overlap.y == 0.0f) if (other.mAABB.center.y> mAABB.center.y) mPushesTopObject = true; mSpeed.y = Mathf.Min (mSpeed.y, 0.0f); else mPushesBottomObject = true; mSpeed.y = Mathf.Max (mSpeed.y, 0.0f); Continua;
Questo è anche un buon posto per impostare le variabili per un corpo cinematico, se abbiamo bisogno di farlo. Non ci farebbe caso se la sovrapposizione è uguale a zero o no, perché non sposteremo comunque un oggetto cinematico. Dovremmo anche saltare la regolazione della velocità perché non vogliamo fermare un oggetto cinematico. Salteremo comunque tutto questo per la demo, dato che non useremo le variabili helper per gli oggetti cinematici.
Ora che questo è coperto, possiamo gestire gli oggetti che si sono sovrapposti correttamente con il nostro AABB. Prima di farlo, lasciatemi spiegare l'approccio che ho preso alla risposta alla collisione nella demo.
Prima di tutto, se l'oggetto non si muove e ci imbattiamo in esso, l'altro oggetto dovrebbe rimanere immobile. Lo trattiamo come un corpo cinematico. Ho deciso di andare in questo modo perché ritengo che sia più generico e il comportamento di spinta può sempre essere gestito più avanti nell'aggiornamento personalizzato di un oggetto particolare.
Se entrambi gli oggetti si muovevano durante la collisione, abbiamo diviso la sovrapposizione tra loro in base alla loro velocità. Più velocemente stavano andando, la maggior parte del valore di sovrapposizione verrà spostato indietro.
L'ultimo punto è, analogamente all'approccio di risposta alla tilemap, se un oggetto sta cadendo e mentre scendendo, graffia un altro oggetto anche di un pixel orizzontalmente, l'oggetto non scivolerà via e continuerà a scendere, ma rimarrà su quell'uno pixel.
Penso che questo sia l'approccio più malleabile, e la sua modifica non dovrebbe essere molto difficile se si desidera gestire una risposta diversa.
Continuiamo l'implementazione calcolando il vettore di velocità assoluta per entrambi gli oggetti durante la collisione. Avremo anche bisogno della somma delle velocità, quindi sappiamo quale percentuale della sovrapposizione del nostro oggetto dovrebbe essere spostata.
Vector2 absSpeed1 = new Vector2 (Mathf.Abs (data.pos1.x - data.oldPos1.x), Mathf.Abs (data.pos1.y - data.oldPos1.y)); Vector2 absSpeed2 = new Vector2 (Mathf.Abs (data.pos2.x - data.oldPos2.x), Mathf.Abs (data.pos2.y - data.oldPos2.y)); Vector2 speedSum = absSpeed1 + absSpeed2;
Si noti che invece di utilizzare la velocità salvata nei dati di collisione, stiamo usando l'offset tra la posizione al momento della collisione e il frame precedente. Questo sarà solo più accurato in questo caso, poiché la velocità rappresenta il vettore di movimento prima della correzione fisica. Le posizioni stesse sono corrette se l'oggetto ha colpito una tessera solida, per esempio, quindi se vogliamo ottenere un vettore di velocità corretto dovremmo calcolarlo come questo.
Ora iniziamo a calcolare il rapporto di velocità per il nostro oggetto. Se l'altro oggetto è cinematico, imposteremo il rapporto di velocità su uno, per assicurarci di spostare l'intero vettore di sovrapposizione, rispettando la regola per cui l'oggetto cinematico non deve essere spostato.
float speedRatioX, speedRatioY; if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; altro
Ora iniziamo con un caso strano in cui entrambi gli oggetti si sovrappongono, ma entrambi non hanno alcuna velocità. Questo non dovrebbe accadere, ma se un oggetto viene generato sovrapposto a un altro oggetto, vorremmo che si spostassero naturalmente. In tal caso, vorremmo che entrambi si spostassero del 50% del vettore di sovrapposizione.
if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else if (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f;
Un altro caso è quando il speedSum
sull'asse x è uguale a zero. In tal caso calcoliamo il rapporto corretto per l'asse y e impostiamo che dovremmo spostare il 50% della sovrapposizione per l'asse x.
if (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f; else if (speedSum.x == 0.0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y;
Allo stesso modo gestiamo il caso in cui il speedSum
è zero solo sull'asse y, e per l'ultimo caso calcoliamo entrambi i rapporti correttamente.
if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else if (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f; else if (speedSum.x == 0.0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y; else if (speedSum.y == 0.0f) speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = 0.5f; else speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = absSpeed1.y / speedSum.y;
Ora che i rapporti sono calcolati, possiamo vedere quanto abbiamo bisogno per compensare il nostro oggetto.
float offsetX = overlap.x * speedRatioX; float offsetY = overlap.y * speedRatioY;
Ora, prima di decidere se spostare l'oggetto fuori collisione sull'asse x o sull'asse y, calcoliamo la direzione da cui è avvenuta la sovrapposizione. Ci sono tre possibilità: o ci siamo imbattuti in un altro oggetto orizzontalmente, verticalmente o diagonalmente.
Nel primo caso, vogliamo uscire dalla sovrapposizione sull'asse x, nel secondo caso vogliamo uscire dalla sovrapposizione sull'asse y, e nell'ultimo caso, vogliamo uscire dalla sovrapposizione su qualsiasi asse l'asse ha avuto la minima sovrapposizione.
Ricorda che per sovrapporsi a un altro oggetto abbiamo bisogno che gli AABB si sovrappongano l'un l'altro su entrambi gli assi xey. Per verificare se ci siamo imbattuti in un oggetto in orizzontale, vedremo se il frame precedente stavamo già sovrapponendo l'oggetto sull'asse y. Se questo è il caso, e non ci siamo sovrapposti sull'asse x, allora la sovrapposizione deve essere avvenuta perché nel frame corrente gli AABB iniziarono a sovrapporsi sull'asse x, e quindi deduciamo che ci siamo imbattuti in un altro oggetto orizzontalmente.
Prima di tutto, calcoliamo se ci sovrapponiamo con l'altro AABB nel frame precedente.
bool overlappedLastFrameX = Mathf.Abs (data.oldPos1.x - data.oldPos2.x) < mAABB.HalfSizeX + other.mAABB.HalfSizeX; bool overlappedLastFrameY = Mathf.Abs(data.oldPos1.y - data.oldPos2.y) < mAABB.HalfSizeY + other.mAABB.HalfSizeY;
Ora impostiamo le condizioni per uscire dalla sovrapposizione orizzontale. Come spiegato in precedenza, dovevamo sovrapporsi sull'asse y e non sovrapposti sull'asse x nel frame precedente.
if (! overlappedLastFrameX && overlappedLastFrameY)
Se questo non è il caso, allora ci sposteremo dalla sovrapposizione sull'asse y.
if (! overlappedLastFrameX && overlappedLastFrameY) else
Come accennato in precedenza, abbiamo anche bisogno di coprire lo scenario di urtare l'oggetto in diagonale. Abbiamo incrociato l'oggetto in diagonale se i nostri AABB non si sovrapponevano nel frame precedente su nessuno degli assi, perché sappiamo che nel frame corrente si sovrappongono su entrambi, quindi la collisione deve essere avvenuta su entrambi gli assi contemporaneamente.
if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX && overlappedLastFrameY)) else
Ma vogliamo spostare la sovrapposizione sull'asse in caso di un dorso diagonale solo se la sovrapposizione sull'asse x è più piccola della sovrapposizione sull'asse y.
if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX && overlappedLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y))) else
Sono tutti i casi risolti. Ora abbiamo effettivamente bisogno di spostare l'oggetto dalla sovrapposizione.
if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX && overlappedLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y))) mPosition.x += offsetX; if (overlap.x < 0.0f) mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); else
Come puoi vedere, lo gestiamo in modo molto simile al caso in cui sfioriamo appena un altro AABB, ma in aggiunta spostiamo il nostro oggetto per l'offset calcolato.
La correzione verticale è fatta allo stesso modo.
if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX &&! overlappedLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y))) mPosition.x += offsetX; if (overlap.x < 0.0f) mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); else mPosition.y += offsetY; if (overlap.y < 0.0f) mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); else mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
Questo è quasi; c'è solo un altro avvertimento da coprire. Immagina lo scenario in cui atterriamo su due oggetti contemporaneamente. Abbiamo due istanze di dati di collisione quasi identiche. Quando iteriamo attraverso tutte le collisioni, correggiamo la posizione della collisione con il primo oggetto, spostandoci un po '.
Quindi, gestiamo la collisione per il secondo oggetto. La sovrapposizione salvata al momento della collisione non è più aggiornata, poiché ci siamo già spostati dalla posizione originale, e se dovessimo gestire la seconda collisione allo stesso modo in cui abbiamo gestito la prima, avremmo spostato di nuovo un po ' , rendendo il nostro oggetto corretto due volte la distanza che avrebbe dovuto.
Per risolvere questo problema, terremo traccia di quanto abbiamo già corretto l'oggetto. Dichiariamo il vettore offsetSum
giusto prima di iniziare a scorrere tra tutte le collisioni.
Vector2 offsetSum = Vector2.zero;
Ora, assicuriamoci di sommare tutti gli offset che abbiamo applicato al nostro oggetto in questo vettore.
if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX &&! overlappedLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y))) mPosition.x += offsetX; offsetSum.x += offsetX; if (overlap.x < 0.0f) mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); else mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); else mPosition.y += offsetY; offsetSum.y += offsetY; if (overlap.y < 0.0f) mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); else mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
Infine, compensiamo la sovrapposizione di ogni collisione consecutiva con il vettore cumulativo di correzioni che abbiamo fatto finora.
var overlap = data.overlap - offsetSum;
Ora se atterriamo su due oggetti della stessa altezza allo stesso tempo, la prima collisione verrebbe elaborata correttamente e la sovrapposizione della seconda collisione verrebbe spostata a zero, il che non sposterebbe più il nostro oggetto.
Ora che la nostra funzione è pronta, assicuriamoci di usarla. Un buon posto per chiamare questa funzione sarebbe dopo il CheckCollisions
chiamata. Questo ci richiederà di dividere il nostro UpdatePhysics
funzione in due parti, quindi creiamo la seconda parte proprio ora, nel MovingObject
classe.
public void UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject;
Nella seconda parte chiamiamo il nostro appena finito UpdatePhysicsResponse
funzione e aggiorna il generale spinge le variabili sinistra, destra, inferiore e superiore. Dopo questo, abbiamo solo bisogno di applicare la posizione.
public void UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; // aggiorna l'aabb mAABB.center = mPosition; // applica le modifiche alla trasformazione transform.position = new Vector3 (Mathf.Round (mPosition.x), Mathf.Round (mPosition.y), mSpriteDepth); transform.localScale = new Vector3 (ScaleX, ScaleY, 1.0f);
Ora, nel ciclo di aggiornamento del gioco principale, chiamiamo la seconda parte dell'aggiornamento della fisica dopo CheckCollisions
chiamata.
void FixedUpdate () for (int i = 0; i < mObjects.Count; ++i) switch (mObjects[i].mType) case ObjectType.Player: case ObjectType.NPC: ((Character)mObjects[i]).CustomUpdate(); mMap.UpdateAreas(mObjects[i]); mObjects[i].mAllCollidingObjects.Clear(); break; mMap.CheckCollisions(); for (int i = 0; i < mObjects.Count; ++i) mObjects[i].UpdatePhysicsP2();
Fatto! Ora i nostri oggetti non possono sovrapporsi l'uno sull'altro. Naturalmente, in un'impostazione di gioco dovremmo aggiungere alcune cose come gruppi di collisione, ecc., Quindi non è obbligatorio rilevare o rispondere alla collisione con ogni oggetto, ma queste sono cose che dipendono da come si desidera hai creato delle cose nel tuo gioco, quindi non ci addentreremo in questo.
Questo è tutto per un'altra parte della semplice serie di fisica del platform 2D. Abbiamo utilizzato il meccanismo di rilevamento delle collisioni implementato nella parte precedente per creare una semplice risposta fisica tra gli oggetti.
Con questi strumenti è possibile creare oggetti standard come piattaforme mobili, blocchi di spinta, ostacoli personalizzati e molti altri tipi di oggetti che non possono realmente far parte della mappa del terreno, ma devono comunque essere parte del terreno pianeggiante in qualche modo. C'è un'altra caratteristica popolare che manca ancora nella nostra implementazione della fisica, e quelle sono le pendenze.
Speriamo che nella prossima parte iniziamo ad estendere la nostra tilemap con il supporto per questi, che completerebbe il set di funzionalità base di una semplice implementazione fisica per un platformer 2D dovrebbe avere, e che finirebbe la serie.
Certo, c'è sempre spazio per migliorare, quindi se hai una domanda o un consiglio su come fare qualcosa di meglio, o semplicemente hai un'opinione sul tutorial, sentiti libero di usare la sezione commenti per farmi sapere!