Elementaire 2D Platformer-fysica, deel 4

In dit deel van de serie over 2D-platformerfysica zullen we richelgrappen toevoegen, clementie mechanismen springen en een mogelijkheid om het object te schalen.

Ledge Grabbing

Nu we kunnen springen, van platformen met één kant naar beneden kunnen vallen en rondrennen, kunnen we richel grijpen. Mechanismen met richel grijpen zijn zeker geen must-have in elke game, maar het is een zeer populaire methode om het mogelijke bewegingsbereik van een speler te vergroten terwijl je nog steeds niet iets extreems doet zoals een dubbele sprong.

Laten we eens kijken naar hoe we bepalen of een richel kan worden gepakt. Om te bepalen of het personage een richel kan grijpen, controleren we voortdurend de kant waar het personage naartoe beweegt. Als we een lege tegel aan de bovenkant van de AABB vinden en dan een stevige tegel eronder, dan is de bovenkant van die massieve tegel de rand die ons personage kan vastgrijpen.

Variabelen instellen

Laten we naar onze gaan Karakter klas, waar we richel grijpen zullen implementeren. Het heeft geen zin om dit in de. Te doen MovingObject klasse, omdat de meeste objecten geen optie hebben om een ​​richel te pakken, dus het zou zonde zijn om elke bewerking in die richting daar uit te voeren.

Eerst moeten we een paar constanten toevoegen. Laten we beginnen met het creëren van de sensor-offset-constanten.

openbare const float cGrabLedgeStartY = 0.0f; openbare const float cGrabLedgeEndY = 2.0f;

De cGrabLedgeStartY en cGrabLedgeEndY zijn verschuivingen vanaf de bovenkant van de AABB; de eerste is het eerste sensorpunt en de tweede is het uiteinde van de sensor. Zoals je ziet, zal het personage binnen 2 pixels een richel moeten vinden.

We hebben ook een extra constante nodig om het personage uit te lijnen met de tegel die hij net heeft gepakt. Voor ons karakter zal dit worden ingesteld op -4.

openbare const float cGrabLedgeTileOffsetY = -4.0f;

Afgezien daarvan willen we de coördinaten onthouden van de tegel die we hebben gepakt. Laten we deze opslaan als de ledvariabele van een personage.

openbare Vector2i mLedgeTile;

Implementatie

We zullen moeten kijken of we de richel van de sprongstatus kunnen pakken, dus laten we daar naartoe gaan. Direct nadat we hebben gecontroleerd of het personage op de grond is geland, gaan we kijken of de voorwaarden om een ​​richel te pakken zijn vervuld. De primaire voorwaarden zijn als volgt:

  • De verticale snelheid is minder dan of gelijk aan nul (het personage valt).
  • Het personage staat niet aan het plafond - het heeft geen zin om een ​​richel te grijpen als je er niet vanaf kunt springen.
  • Het personage botst tegen de muur en beweegt ernaartoe.
if (mOnGround) // als er geen verplaatsingswijzigingstoestand is in stand-by (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Stand; mSpeed ​​= Vector2.zero; mAudioSource.PlayOneShot (mHitWallSfx, 0.5f);  else // of ga naar rechts of naar links worden ingedrukt, dus we veranderen de status om te lopen 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))))  

Als aan deze drie voorwaarden is voldaan, moeten we op zoek naar de richel om te grijpen. Laten we beginnen met het berekenen van de bovenste positie van de sensor, die ofwel de linkerbovenhoek of de rechterbovenhoek van de AABB zal zijn. 

Vector2 aabbCornerOffset; if (mPushesRightWall && mInputs [(int) KeyInput.GoRight]) aabbCornerOffset = mAABB.halfSize; else aabbCornerOffset = new Vector2 (-mAABB.halfSize.x - 1.0f, mAABB.halfSize.y);

Nu, zoals je je misschien kunt voorstellen, zullen we hier een soortgelijk probleem tegenkomen als degene die we vonden bij het uitvoeren van de aanvaringscontroles - als het personage erg snel daalt, is het zeer waarschijnlijk dat het de hotspot mist waar het de richel kan pakken. . Daarom moeten we controleren op welke tegel we moeten graaien, niet beginnend in de hoek van het huidige frame, maar de vorige, zoals hier wordt geïllustreerd:


De bovenste afbeelding van een personage is de positie in het vorige frame. In deze situatie moeten we op zoek gaan naar mogelijkheden om een ​​richel te pakken vanuit de rechterbovenhoek van de AABB van het vorige frame en te stoppen bij de huidige positie van het frame.

Laten we de coördinaten van de tegels bekijken die we moeten controleren, beginnend door de variabelen te declareren. We controleren tegels in één kolom, dus we hebben alleen de X-coördinaat van de kolom nodig, evenals de Y-coördinaten aan de boven- en onderkant.

int tegelX, topY, bottomY;

Laten we de X-coördinaat van de hoek van de AABB halen.

int tegelX, topY, bottomY; tileX = mMap.GetMapTileXAtPoint (mAABB.center.x + aabbCornerOffset.x);

We willen alleen op zoek gaan naar een richel uit de positie van het vorige frame als we in die tijd al in de richting van de geduwde muur waren gegaan - dus de X-positie van ons personage veranderde niet.

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); 

Zoals je kunt zien, berekenen we in dat geval de topY met behulp van de positie van het vorige frame en de onderste met die van het huidige frame. Als we niet naast een muur staan, gaan we gewoon kijken of we een richel kunnen pakken met alleen de positie van het object in het huidige frame.

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); 

Oké, nu we weten welke tegels we moeten controleren, kunnen we er doorheen beginnen te itereren. We gaan van boven naar beneden, omdat deze volgorde het meest logisch is omdat we richel grijpen alleen toestaan ​​wanneer het personage valt.

for (int y = topY; y> = bottomY; --y) 

Laten we nu eens kijken of de tile die we itereren voldoet aan de voorwaarden die het personage toestaan ​​om een ​​richel te pakken. De voorwaarden, zoals eerder uitgelegd, zijn als volgt:

  • De tegel is leeg.
  • De tegel eronder is een solide tegel (dit is de steen die we willen pakken).
for (int y = topY; y> = bottomY; --y) if (! mMap.IsObstacle (tileX, y) && mMap.IsObstacle (tileX, y - 1)) 

De volgende stap is het berekenen van de positie van de hoek van de tegel die we willen pakken. Dit is vrij eenvoudig: we moeten alleen de positie van de tegel bepalen en deze vervolgens compenseren met de grootte van de tegel.

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; 

Nu we dit weten, moeten we controleren of de hoek tussen onze sensorpunten ligt. Natuurlijk willen we dat alleen doen als we de tegel controleren met betrekking tot de positie van het huidige frame, de tegel met Y-coördinaat gelijk aan de onderkant. Als dat niet het geval is, kunnen we veilig aannemen dat we de richel hebben gepasseerd tussen het vorige en het huidige frame - dus we willen de richel toch pakken.

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)) 

Nu we thuis zijn, hebben we de richel gevonden die we willen grijpen. Laten we eerst de tegelpositie van de gegrepen richel opslaan.

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); 

We moeten ook het personage uitlijnen met de richel. Wat we willen doen, is de bovenkant van de richelsensor van het personage uitlijnen met de bovenkant van de tegel en die positie vervolgens verschuiven met cGrabLedgeTileOffsetY.

mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY;

Afgezien van dit, moeten we dingen doen zoals de snelheid op nul zetten en de status wijzigen in CharacterState.GrabLedge. Hierna kunnen we van de lus breken omdat het geen zin heeft om door de rest van de tegels te itereren.

mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY; mSpeed ​​= Vector2.zero; mCurrentState = CharacterState.GrabLedge; breken;

Dat zal het zijn! De richels kunnen nu worden gedetecteerd en gepakt, dus nu hoeven we alleen maar de GrabLedge staat, die we eerder hebben overgeslagen.

Ledge Grab Controls

Zodra het personage een richel vastpakt, heeft de speler twee opties: ze kunnen springen of naar beneden vallen. Springen werkt zoals normaal; de speler drukt op de springtoets en de kracht van de sprong is identiek aan de kracht die wordt uitgeoefend bij het springen uit de grond. Laat vallen doe je door op de neerwaartse knop te drukken of op de richtingstoets die van de rand af wijst.

Bestuurt implementatie

Het eerste wat hier te doen is, is detecteren of de rand zich links of rechts van het personage bevindt. We kunnen dit doen omdat we de coördinaten van de richel die het personage neemt, hebben opgeslagen.

bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft;

We kunnen die informatie gebruiken om te bepalen of het personage van de richel moet vallen. Om te laten vallen, moet de speler een van beide:

  • druk op de knop omlaag
  • druk op de linkerknop wanneer we een richel rechts pakken, of
  • druk op de rechterknop wanneer we een richel aan de linkerkant pakken
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))  

Hier is een kleine waarschuwing. Overweeg een situatie wanneer we de knop Omlaag en de rechterknop ingedrukt houden wanneer het personage een richel rechts vasthoudt. Dit leidt tot de volgende situatie:

Het probleem hier is dat het personage de richel grijpt zodra hij het loslaat. 

Een eenvoudige oplossing hiervoor is om de beweging naar de richel te vergrendelen voor een paar frames nadat we de richel hebben laten vallen. Daarvoor moeten we twee nieuwe variabelen toevoegen; laten we ze bellen mCannotGoLeftFrames en mCannotGoRightFrames.

public int mCannotGoLeftFrames = 0; public int mCannotGoRightFrames = 0;

Als het personage van de rand valt, moeten we die variabelen instellen en de staat wijzigen om te springen.

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; 

Laten we nu even teruggaan naar de Springen staat, en laten we ervoor zorgen dat het ons verbod respecteert om links of rechts te bewegen nadat je van de richel bent gevallen. Laten we de invoer opnieuw instellen voordat we controleren of we een richel moeten zoeken om te grijpen.

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])))  

Zoals je kunt zien, zullen we op deze manier niet voldoen aan de voorwaarden om een ​​richel te grijpen zolang de geblokkeerde richting dezelfde is als de richting van de richel die het personage kan proberen te grijpen. Telkens wanneer we een bepaalde invoer weigeren, verlagen we de resterende blokkeerkaders, dus uiteindelijk kunnen we weer verdergaan, in ons geval na 3 frames.

Laten we nu verder werken aan de GrabLedge staat. Omdat we de richel hebben laten vallen, moeten we het nu mogelijk maken om van de grijppositie te springen.

Als het personage niet van de richel is gevallen, moeten we controleren of de sneltoets is ingedrukt; als dat zo is, moeten we de verticale snelheid van de sprong instellen en de status wijzigen:

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; 

Dat is het eigenlijk wel! Nu moet de richel grijpen in allerlei situaties goed werken.

Laat het personage kort springen na het verlaten van een platform

Vaak, om sprongen makkelijker te maken in platformgames, mag het personage springen als het net van de rand van een platform is gestapt en niet langer op de grond staat. Dit is een populaire methode om een ​​illusie te onderdrukken dat de speler op de springknop heeft gedrukt, maar het personage sprong niet, wat mogelijk het gevolg was van een invoervertraging of de speler die op de springknop drukte nadat het personage van het platform was verwijderd.

Laten we een dergelijke monteur nu implementeren. Allereerst moeten we een constante toevoegen van het aantal frames nadat het personage het platform heeft verlaten en nog steeds een sprong kan uitvoeren.

public const int cJumpFramesThreshold = 4;

We hebben ook een frameteller nodig in de Karakter klasse, dus we weten hoeveel frames het personage al in de lucht is.

protected int mFramesFromJumpStart = 0;

Laten we nu de mFramesFromJumpStart tot 0 elke keer dat we net de grond verlaten. Laten we dat doen nadat we gebeld hebben  UpdatePhysics.

UpdatePhysics (); if (mWasOnGround &&! mOnGround) mFramesFromJumpStart = 0;

En laten we het in elk frame verhogen dat we in de sprongstatus zijn.

case CharacterState.Jump: ++ mFramesFromJumpStart;

Als we in de sprongstatus zijn, kunnen we geen luchtsprong toestaan ​​als we ons aan het plafond bevinden of een positieve verticale snelheid hebben. Een positieve verticale snelheid betekent dat het personage geen sprong heeft gemist.

++mFramesFromJumpStart; if (mFramesVanStart Start <= Constants.cJumpFramesThreshold)  if (mAtCeiling || mSpeed.y > 0.0f) mFramesVanJumpStart = Constants.cJumpFramesThreshold + 1; 

Als dat niet het geval is en er op de jump-toets wordt gedrukt, hoeven we alleen de verticale snelheid op de sprongwaarde in te stellen, alsof we normaal zijn gesprongen, hoewel het personage al in de sprongstaat is.

if (mFramesVanStart Start <= Constants.cJumpFramesThreshold)  if (mAtCeiling || mSpeed.y > 0.0f) mFramesVanJumpStart = Constants.cJumpFramesThreshold + 1; else if (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed; 

En dat is het! We kunnen de cJumpFramesThreshold tot een grote waarde zoals 10 frames om ervoor te zorgen dat het werkt.

Het effect is hier behoorlijk overdreven. Het is niet erg merkbaar als we het personage toestaan ​​om slechts 1-4 frames te laten springen nadat het in feite niet langer op de grond is, maar over het algemeen kunnen we aanpassen hoe lenig we willen dat onze sprongen worden.

De objecten schalen

Laten we het mogelijk maken om de objecten te schalen. We hebben al de mscale in de MovingObject klasse, dus alles wat we moeten doen is zorgen dat het de AABB- en de AABB-offset correct beïnvloedt.

Laten we eerst onze AABB-klasse aanpassen zodat deze een schaalcomponent heeft.

public struct AABB public Vector2 scale; openbaar Vector2-centrum; openbare Vector2 halfSize; openbare AABB (Vector2 center, Vector2 halfSize) scale = Vector2.one; this.center = centrum; this.halfSize = halfSize;  

Laten we nu de halve maat, zodat we bij het openen een geschaalde grootte krijgen in plaats van de ongeschaalde.

openbare Vector2-schaal; openbaar Vector2-centrum; privé Vector2 halfSize; openbare Vector2 HalfSize set halfSize = value;  krijg return new Vector2 (halfSize.x * scale.x, halfSize.y * scale.y); 

We willen ook alleen een X- of Y-waarde van de halve grootte kunnen krijgen of instellen, dus we moeten ook aparte getters en setters maken voor die.

openbare float HalfSizeX set halfSize.x = value;  krijg return halfSize.x * scale.x;  openbare float HalfSizeY set halfSize.y = value;  krijg return halfSize.y * scale.y; 

Naast het schalen van de AABB zelf, zullen we ook de. Moeten schalen mAABBOffset, zodat nadat we het object hebben geschaald, de sprite ervan nog steeds overeenkomt met de AABB op dezelfde manier als toen het object niet werd geschaald. Laten we teruggaan naar de MovingObject klasse om het te bewerken.

private Vector2 mAABBOffset; public Vector2 AABBOffset set mAABBOffset = value;  krijg return new Vector2 (mAABBOffset.x * mScale.x, mAABBOffset.y * mScale.y); 

Hetzelfde als voorheen, we willen ook afzonderlijk toegang hebben tot X- en Y-componenten.

public float AABBOffsetX set mAABBOffset.x = value;  krijg return mAABBOffset.x * mScale.x;  public float AABBOffsetY set mAABBOffset.y = value;  krijg return mAABBOffset.y * mScale.y; 

Ten slotte moeten we er ook voor zorgen dat wanneer de schaal wordt aangepast in de MovingObject, het is ook gewijzigd in de AABB. De schaal van het object kan negatief zijn, maar de AABB zelf zou geen negatieve schaal moeten hebben omdat we ervan uitgaan dat de helft altijd positief is. Daarom gaan we in plaats van alleen de weegschaal door te geven aan de AABB, een schaal door waarin alle componenten positief zijn.

privé Vector2 mScale; public Vector2 Scale set mScale = value; mAABB.scale = new Vector2 (Mathf.Abs (value.x), Mathf.Abs (value.y));  krijg return mScale;  openbare float ScaleX set mScale.x = waarde; mAABB.scale.x = Mathf.Abs (waarde);  krijg return mScale.x;  openbare float ScaleY set mScale.y = value; mAABB.scale.y = Mathf.Abs (waarde);  krijg return mScale.y; 

Het enige wat u nu hoeft te doen, is ervoor zorgen dat waar we de variabelen ook rechtstreeks gebruiken, we ze nu via de getters en setters gebruiken. Waar we ook gebruikten halfSize.x, we willen gebruiken HalfSizeX, waar we ook gebruikten halfSize.y, we willen gebruiken HalfSizeY, enzovoorts. Een paar toepassingen van een zoek- en vervangfunctie moeten hier goed mee omgaan.

Bekijk de resultaten

De schaal zou nu goed moeten werken, en vanwege de manier waarop we onze botsingsdetectiefuncties hebben gebouwd, maakt het niet uit of het personage groot of klein is - het moet goed met de kaart kunnen communiceren.

Samenvatting

Dit deel besluit ons werk met de tilemap. In de volgende delen zullen we dingen instellen om botsingen tussen objecten te detecteren. 

Het kostte wat tijd en moeite, maar het systeem zou over het algemeen zeer robuust moeten zijn. Een ding dat op dit moment misschien ontbreekt, is de ondersteuning voor hellingen. Veel games vertrouwen niet op hen, maar veel van hen doen dat, dus dat is het grootste verbeteringsdoel voor dit systeem. Bedankt voor het lezen tot nu toe, zie je in het volgende deel!