Elementaire 2D Platformer-fysica, deel 3

One-Way Platforms

Omdat we net klaar zijn met de grondbotsingscontrole, kunnen we net zo goed eenrichtingsplatform toevoegen terwijl we bezig zijn. Ze gaan hoe dan ook alleen over de botsende botsproef. Eenrichtingsplatforms verschillen van massieve blokken doordat ze een object alleen stoppen als het naar beneden valt. Daarnaast zullen we ook toestaan ​​dat een personage uit zo'n platform komt.

Allereerst willen we, wanneer we een eenrichtingsplatform willen inzetten, de botsing met de grond negeren. Een eenvoudige manier om hier uit te komen is om een ​​offset in te stellen, na te hebben doorgegeven, waardoor het personage of object niet meer tegen een platform botst. 

Als het personage bijvoorbeeld al twee pixels onder de bovenkant van het platform staat, zou het geen botsing meer moeten detecteren. In dat geval, als we het platform willen verlaten, hoeven we het personage slechts twee pixels naar beneden te verplaatsen. Laten we deze offsetconstante maken.

openbare const float cOneWayPlatformThreshold = 2.0f;

Laten we nu een variabele toevoegen die ons laat weten of een object zich momenteel op een eenrichtingsplatform bevindt.

public bool mOnOneWayPlatform = false;

Laten we de definitie van de wijzigen HasGround functie om ook een verwijzing naar een boolean te maken die wordt ingesteld als het object op een eenrichtingsplatform is geland.

public bool HasGround (Vector2 oldPosition, Vector2 position, Vector2 speed, out float groundY, ref bool onOneWayPlatform)

Nu, nadat we hebben gecontroleerd of de tegel waar we ons nu bevinden een obstakel is, en dat is het niet, moeten we controleren of het een eenrichtingsplatform is.

if (mMap.IsObstacle (tileIndexX, tileIndexY)) return true; else if (mMap.IsOneWayPlatform (tileIndexX, tileIndexY)) onOneWayPlatform = true;

Zoals eerder uitgelegd, moeten we er ook voor zorgen dat deze botsing wordt genegeerd als we voorbij de cOneWayPlatformThreshold onder het platform. 

Natuurlijk kunnen we niet simpelweg het verschil tussen de bovenkant van de tegel en de sensor vergelijken, omdat je je gemakkelijk kunt voorstellen dat, zelfs als we vallen, we mogelijk ver onder de twee pixels van de bovenkant van het platform gaan. Voor de eenrichtingsplatforms om een ​​object te stoppen, willen we dat de sensorafstand tussen de bovenkant van de tegel en de sensor kleiner is dan of gelijk is aan de cOneWayPlatformThreshold plus de offset van de positie van dit frame naar de vorige positie.

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

Eindelijk, er is nog een ding om te overwegen. Wanneer we een eenrichtingsplatform vinden, kunnen we niet echt de lus verlaten, omdat er situaties zijn waarin het personage gedeeltelijk op een platform en gedeeltelijk op een vast blok staat.

We zouden een dergelijke positie niet echt als "op een eenrichtingsplatform" moeten beschouwen, omdat we niet echt van daaruit kunnen vallen - het vaste blok houdt ons tegen. Daarom moeten we eerst op zoek blijven naar een solide blokkering. Als we het resultaat vinden voordat we het resultaat retourneren, moeten we ook onOneWayPlatform naar false.

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

Als we nu alle tegels doorlopen die we horizontaal moesten controleren en we een eenrichtingsplatform hadden gevonden maar geen solide blokken, dan kunnen we er zeker van zijn dat we op een eenrichtingsplatform staan ​​vanwaar we naar beneden kunnen gaan.

if (mMap.IsObstacle (tileIndexX, tileIndexY)) onOneWayPlatform = false; geef waar terug;  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) return true; breken; 

Dat is het, dus laten we nu de karakterklasse toevoegen aan een optie om op het platform neer te zetten. In zowel de stand- als de uitvoeringstoestand moeten we de volgende code toevoegen.

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

Laten we kijken hoe het werkt.

Alles werkt correct.

Omgaan met botsingen voor het plafond

We moeten voor elke kant van de AABB een analoge functie voor de HasGround maken, dus laten we beginnen met het plafond. De verschillen zijn als volgt:

  • De sensorlijn bevindt zich boven de AABB in plaats van eronder
  • We controleren de plafondtegel van beneden naar boven, als we naar boven gaan
  • Het is niet nodig om one-way platforms te gebruiken

Hier is de gewijzigde functie.

public bool HasCeiling (Vector2 oldPosition, Vector2 position, out float ceilingY) var center = position + mAABBOffset; var oldCenter = oldPosition + mAABBOffset; plafondY = 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; for (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) pauze;  return false; 

Omgaan met aanrijdingen voor de linkermuur

Op dezelfde manier als hoe we de botsingcontrole voor het plafond en de grond hebben uitgevoerd, moeten we ook controleren of het object in botsing komt met de muur aan de linkerkant of de muur aan de rechterkant. Laten we beginnen vanaf de linkermuur. Het idee hier is vrijwel hetzelfde, maar er zijn een paar verschillen:

  • De sensorlijn bevindt zich aan de linkerkant van de AABB.
  • Het innerlijk voor lus moet door de tegels verticaal worden herhaald, omdat de sensor nu een verticale lijn is.
  • De buitenste lus moet de tegels horizontaal doorlopen om te zien of we een muur niet hebben overgeslagen bij een grote horizontale snelheid.
public bool CollidesWithLeftWall (Vector2 oldPosition, Vector2 position, out 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 = (zwevend) tileIndexX * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.x; geef waar terug;  if (checkedTile.y> = topLeft.y) pauze;  return false; 

Omgaan met aanrijdingen voor de rechtermuur

Laten we tot slot de CollidesWithRightWall functie, die zoals je je kunt voorstellen, een soortgelijk ding zal doen als CollidesWithLeftWall, maar in plaats van een sensor aan de linkerkant te gebruiken, gebruiken we een sensor aan de rechterkant van het personage. 

Het andere verschil hier is dat in plaats van de tegels van rechts naar links te controleren, we ze van links naar rechts zullen controleren, omdat dat de aangenomen bewegingsrichting is.

public bool CollidesWithRightWall (Vector2 oldPosition, Vector2 position, out 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; voor (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) pauze;  return false; 

Verplaats het object uit de botsing

Al onze botsdetectiefuncties zijn klaar, dus laten we ze gebruiken om de botsingreactie tegen de tilemap te voltooien. Voordat we dat doen, moeten we echter uitzoeken in welke volgorde we de botsingen zullen controleren. Laten we de volgende situaties overwegen.

In beide situaties kunnen we zien dat het personage overlappend is met een tegel, maar we moeten uitzoeken hoe we de overlapping moeten oplossen. 

De situatie aan de linkerkant is vrij eenvoudig - we kunnen zien dat we recht naar beneden vallen, en daarom moeten we zeker bovenop het blok landen. 

De situatie aan de rechterkant is een beetje lastiger, omdat we in werkelijkheid op de hoek van de tegel kunnen landen en het personage naar de top duwen is even redelijk als het naar rechts duwen. Laten we ervoor kiezen om prioriteit te geven aan de horizontale beweging. Het maakt niet echt veel uit welke afstemming we het eerst willen doen; beide keuzes zien er correct uit in actie.

Laten we naar onze gaan UpdatePhysics functie en voeg de variabelen toe die de resultaten van onze collisiequery's bevatten.

vlotter grondY = 0,0f, plafond Y = 0,0f; float rightWallX = 0.0f, leftWallX = 0.0f;

Laten we nu eens kijken of we het object naar rechts moeten verplaatsen. De voorwaarden hier zijn dat:

  • de horizontale snelheid is minder of gelijk aan nul
  • we botsen tegen de linkermuur 
  • in het vorige frame overlappen we niet de tegel op de horizontale as - een situatie vergelijkbaar met die aan de rechterkant in de bovenstaande afbeelding

De laatste is een noodzakelijke voorwaarde, want als het niet was vervuld, zouden we te maken hebben met een situatie die lijkt op die links in de bovenstaande afbeelding, waarin we het karakter beslist niet naar rechts moeten verplaatsen.

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

Als de voorwaarden waar zijn, moeten we de linkerkant van onze AABB aan de rechterkant van de tegel uitlijnen, zorgen dat we niet meer naar links gaan en markeren dat we naast de linker muur staan.

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; 

Als een van de voorwaarden naast de laatste onjuist is, moeten we instellen mPushesLeftWall naar false. Dat komt omdat de laatste voorwaarde die onwaar is niet noodzakelijkerwijs ons vertelt dat het personage niet tegen de muur duwt, maar omgekeerd vertelt het ons dat het al in het vorige frame ermee in botsing kwam. Daarom is het het beste om te veranderen mPushesLeftWall alleen false als één van de eerste twee voorwaarden ook onjuist is.

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;

Laten we nu kijken naar de botsing met de juiste muur.

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;

Zoals u kunt zien, is het dezelfde formule die we hebben gebruikt voor het controleren van de botsing met de linkermuur, maar die is gespiegeld.

We hebben al de code om de botsing met de grond te controleren, dus daarna moeten we de botsing met het plafond controleren. Hier is ook niets nieuws, plus we hoeven geen extra controles uit te voeren, behalve dat de verticale snelheid groter of gelijk aan nul moet zijn en we botsen tegen een tegel die bovenop ons staat.

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;

Rond de hoeken op

Voordat we testen of de collisiereacties werken, is er nog een belangrijk ding om te doen, namelijk het afronden van de waarden van de hoeken die we berekenen voor de aanvaringscontroles. We moeten dat doen, zodat onze cheques niet worden vernietigd door drijvende-kommaberichten, die kunnen ontstaan ​​door een rare kaartpositie, karakterschaal of gewoon een rare AABB-maat.

Ten eerste, laten we voor ons gemak een functie maken die een vector van drijvers omzet in een vector met afgeronde drijvers.

Vector2 RoundVector (Vector2 v) retourneer nieuwe Vector2 (Mathf.Round (v.x), Mathf.Round (v.y)); 

Laten we nu deze functie gebruiken bij elke botsingcontrole. Laten we eerst het probleem oplossen HasCeiling functie.

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

Volgende is OnGround.

var oldBottomLeft = RoundVector (oldCenter - mAABB.HalfSize - Vector2.up + Vector2.right); var newBottomLeft = RoundVector (center - mAABB.HalfSize - Vector2.up + Vector2.right); var newBottomRight = RoundVector (nieuwe 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));

En tenslotte, 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));

Dat zou onze problemen moeten oplossen!

Bekijk de resultaten

Dat zal het zijn. Laten we testen hoe onze botsingen nu werken.

Samenvatting

Dat is het voor dit deel! We hebben een volledig werkende reeks tilemap-botsingen, die zeer betrouwbaar moeten zijn. We weten in welke positie het object zich momenteel bevindt: of het zich op de grond bevindt, een tegel links of rechts aanraakt, of tegen een plafond botst. We hebben ook de eenrichtingsplatforms geïmplementeerd, die een zeer belangrijk hulpmiddel zijn in elk platformgame-spel. 

In het volgende deel voegen we marge-grijpende mechanica toe, die de mogelijke beweging van het personage nog verder zal vergroten, dus houd het in de gaten!