I denne delen av serien på 2D-plattformsfysikk, legger vi til hyllehåndtak, hopper mekanismer for fleksibilitet og evne til å skalere objektet.
Nå som vi kan hoppe, slippe ned fra enveisplattformer og løpe rundt, vi kan også implementere hyllefeste. Ledge-grabbing mekanikk er definitivt ikke en må-ha i hvert spill, men det er en veldig populær metode for å forlenge en spiller mulig bevegelsesområde mens du fortsatt ikke gjør noe ekstreme som et dobbelt hopp.
La oss se på hvordan vi avgjør om en hylle kan bli tatt. For å avgjøre om tegnet kan ta en hylle, vil vi hele tiden sjekke siden karakteren beveger seg mot. Hvis vi finner en tom flis øverst på AABB, og deretter en solid flis under den, så er toppen av den faste flisen skråningen vår karakter kan gripe inn på.
La oss gå til vår Karakter
klassen, hvor vi skal gjennomføre hyllefeste. Det er ikke noe poeng å gjøre dette i MovingObject
klassen, siden de fleste objektene ikke har mulighet til å ta tak i en hylle, så det ville være en sløsing med å gjøre noen behandling i den retningen der.
Først må vi legge til et par konstanter. La oss begynne med å skape sensorkompensasjonskonstantene.
offentlig const float cGrabLedgeStartY = 0,0f; offentlig const float cGrabLedgeEndY = 2.0f;
De cGrabLedgeStartY
og cGrabLedgeEndY
er offsets fra toppen av AABB; den første er det første sensorpunktet, og det andre er sluttpunktsensorpunktet. Som du kan se, vil tegnet trenge å finne en hylle innen 2 piksler.
Vi trenger også en ekstra konstant for å justere tegnet til flisen det bare grep. For vår karakter blir dette satt til -4.
offentlig const float cGrabLedgeTileOffsetY = -4.0f;
Bortsett fra det, vil vi gjerne huske koordinatene til flisen vi grep. La oss lagre dem som et tegns medlemsvariabel.
offentlig Vector2i mLedgeTile;
Vi må se om vi kan ta tak i hylle fra hoppestaten, så la oss hodet der. Like etter at vi har sjekket om tegnet har landet på bakken, la vi se om betingelsene for å få tak i en kant er oppfylt. De primære forholdene er som følger:
hvis (mOnGround) // hvis det ikke er noen bevegelsesendringstilstand til stående hvis (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Stand; mSpeed = Vector2.zero; mAudioSource.PlayOneShot (mHitWallSfx, 0,5f); else // enten gå til høyre eller gå til venstre, trykkes så vi endrer tilstanden for å gå mCurrentState = CharacterState.Walk; mSpeed.y = 0.0f; mAudioSource.PlayOneShot (mHitWallSfx, 0,5f); annet hvis (mSpeed.y <= 0.0f && !mAtCeiling && ((mPushesRightWall && KeyState(KeyInput.GoRight)) || (mPushesLeftWall && KeyState(KeyInput.GoLeft))))
Hvis disse tre betingelsene er oppfylt, må vi se etter skjæret å gripe. La oss begynne med å beregne sensorens øverste posisjon, som skal være enten øverst til venstre eller øverste høyre hjørne av AABB.
Vector2 aabbCornerOffset; hvis (mPushesRightWall && mInputs [(int) KeyInput.GoRight]) aabbCornerOffset = mAABB.halfSize; ellers aabbCornerOffset = ny Vector2 (-mAABB.halfSize.x - 1.0f, mAABB.halfSize.y);
Nå, som du kanskje kan forestille deg, vil vi møte et lignende problem med det vi fant ved implementering av kollisjonskontrollene. Hvis karakteren faller veldig fort, er det faktisk veldig sannsynlig å savne hotspotet hvor det kan ta tak i rammen . Det er derfor vi må sjekke at flisen vi trenger å ta ikke fra den nåværende rammens hjørne, men den forrige - som illustrert her:
Toppbildet av et tegn er dets posisjon i forrige ramme. I denne situasjonen må vi begynne å lete etter muligheter til å ta tak i en liste fra øverste høyre hjørne av den forrige rammens AABB og stoppe ved den aktuelle rammens posisjon.
La oss få koordinatene til flisene vi må sjekke, først ved å erklære variablene. Vi sjekker fliser i en enkelt kolonne, så alt vi trenger er X-koordinatet til kolonnen, samt topp- og bunn Y-koordinatene.
int tileX, topY, bottomY;
La oss få X-koordinaten til AABBs hjørne.
int tileX, topY, bottomY; tileX = mMap.GetMapTileXAtPoint (mAABB.center.x + aabbCornerOffset.x);
Vi ønsker å begynne å lete etter en liste fra den forrige rammens posisjon bare hvis vi faktisk allerede beveget seg mot den trykte veggen på den tiden - så vår karakters X-posisjon endret seg ikke.
hvis ((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);
Som du kan se, beregner vi det øverste i den forrige rammens posisjon, og den nederste bruker den aktuelle rammen. Hvis vi ikke var ved siden av en vegg, så skal vi bare se om vi kan ta tak i en skjema med bare objektets posisjon i den nåværende rammen.
hvis ((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); ellers topY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeStartY); bottomY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeEndY);
Ok, nå som vi vet hvilke fliser å sjekke, kan vi begynne å iterere gjennom dem. Vi går fra topp til bunn, fordi denne ordningen gir mest mening når vi tillater at ledge tar bare når karakteren faller.
for (int y = topY; y> = bottomY; -y)
La oss nå sjekke om flisen vi er iterating oppfyller betingelsene som tillater tegnet å ta tak i en hylle. Betingelsene, som forklart før, er som følger:
for (int y = topY; y> = bottomY; -y) if (! mMap.IsObstacle (tileX, y) && mMap.IsObstacle (tileX, y - 1))
Det neste trinnet er å beregne posisjonen til hjørnet av flisen vi ønsker å ta tak i. Dette er ganske enkelt-vi trenger bare å få flisens posisjon og deretter kompensere det ved flisens størrelse.
hvis (! 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;
Nå som vi vet dette, bør vi sjekke om hjørnet er mellom sensorpoengene våre. Selvfølgelig vil vi bare gjøre det hvis vi sjekker flisen om den aktuelle rammens posisjon, hvilket er flisen med Y-koordinat lik botten. Hvis det ikke er tilfelle, kan vi trygt anta at vi passerte skråningen mellom forrige og nåværende ramme, så vi vil likevel få tak i hylsen uansett.
hvis (! 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; hvis (y> bottomY || ((mAABB.center.y + aabbCornerOffset.y) - tileCorner.y <= Constants.cGrabLedgeEndY && tileCorner.y - (mAABB.center.y + aabbCornerOffset.y) >= Constants.cGrabLedgeStartY))
Nå er vi hjemme, vi har funnet hylsen vi vil ta tak i. Først, la oss lagre den grepde kantens flisposisjon.
hvis (y> bottomY || ((mAABB.center.y + aabbCornerOffset.y) - tileCorner.y <= Constants.cGrabLedgeEndY && tileCorner.y - (mAABB.center.y + aabbCornerOffset.y) >= Constants.cGrabLedgeStartY)) mLedgeTile = ny Vector2i (tileX, y - 1);
Vi må også justere tegnet med skjermen. Det vi ønsker å gjøre er å justere toppen av tegnsensorføleren med toppen av flisen, og motvirke deretter denne posisjonen ved å cGrabLedgeTileOffsetY
.
mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY;
Bortsett fra dette, må vi gjøre ting som å sette hastigheten til null og endre staten til CharacterState.GrabLedge
. Etter dette kan vi bryte fra løkken fordi det ikke er noe poeng i det hele tatt gjennom flisene.
mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY; mSpeed = Vector2.zero; mCurrentState = CharacterState.GrabLedge; gå i stykker;
Det kommer til å bli det! Ledgene kan nå bli oppdaget og fanget, så nå trenger vi bare å implementere GrabLedge
stat, som vi hoppet over tidligere.
Når tegnet griper en kant, har spilleren to alternativer: de kan enten hoppe opp eller slippe ned. Hopping fungerer som normalt; spilleren trykker på hoppeknappen og hoppens kraft er identisk med kraften som brukes når du hopper fra bakken. Nedløsningen gjøres ved å trykke på ned-knappen eller retningstasten som peker bort fra skjermen.
Det første du må gjøre er å oppdage om skjermen er til venstre eller til høyre for tegnet. Vi kan gjøre dette fordi vi lagret koordinatene til ledgen karakteren griper.
bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft;
Vi kan bruke denne informasjonen til å avgjøre om tegnet skal falle fra skjermen. For å slippe ned, må spilleren enten:
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))
Det er en liten advarsel her. Tenk på en situasjon når vi holder nede-knappen og den høyre knappen, når tegnet holder på en hylle til høyre. Det vil resultere i følgende situasjon:
Problemet her er at tegnet tar tak i hylsen umiddelbart etter at den lar det gå.
En enkel løsning på dette er å låse bevegelsen mot skjermen for et par rammer etter at vi har slått av skråningen. For det må vi legge til to nye variabler; la oss ringe dem mCannotGoLeftFrames
og mCannotGoRightFrames
.
offentlig int mCannotGoLeftFrames = 0; offentlig int mCannotGoRightFrames = 0;
Når tegnet faller av skjermen, må vi sette de variablene og endre tilstanden for å hoppe.
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;
La oss nå gå tilbake for litt til Hoppe
stat, og la oss sørge for at det respekterer vårt forbud mot å flytte enten venstre eller høyre etter at du har slått av skjermen. La oss nullstille inngangene rett før vi sjekker om vi skal se etter en hylle å ta tak i.
hvis (mCannotGoLeftFrames> 0) - mCannotGoLeftFrames; mInputs [(int) KeyInput.GoLeft] = false; hvis (mCannotGoRightFrames> 0) - mCannotGoRightFrames; mInputs [(int) KeyInput.GoRight] = false; hvis (mSpeed.y <= 0.0f && !mAtCeiling && ((mPushesRightWall && mInputs[(int)KeyInput.GoRight]) || (mPushesLeftWall && mInputs[(int)KeyInput.GoLeft])))
Som du kan se, vil vi på denne måten ikke oppfylle betingelsene som kreves for å få tak i en hylle så lenge den blokkerte retningen er den samme som retningen til hylsen, karakteren kan prøve å ta tak i. Hver gang vi nekter en bestemt inngang, avtar vi fra de gjenværende blokkeringsrammene, så til slutt vil vi kunne flytte igjen - i vårt tilfelle etter 3 rammer.
La oss fortsette å jobbe på GrabLedge
stat. Siden vi har håndtert å slippe av skråningen, må vi nå gjøre det mulig å hoppe fra gripeposisjonen.
Hvis tegnet ikke droppet fra skjermen, må vi sjekke om hoppetasten har blitt trykket. I så fall må vi sette hoppets vertikale hastighet og endre tilstanden:
hvis (mInputs [(int) KeyInput.GoDown] || (mInputs [(int) KeyInput.GoLeft] && ledgeOnRight) || (mInputs [(int) KeyInput.GoRight] && ledgeOnLeft)) hvis (ledgeOnLeft) mCannotGoLeftFrames = 3 ; ellers mCannotGoRightFrames = 3; mCurrentState = CharacterState.Jump; annet hvis (mInputs [(int) KeyInput.Jump]) mSpeed.y = mJumpSpeed; mCurrentState = CharacterState.Jump;
Det er ganske mye det! Nå skal hengslingen gripe ordentlig i alle slags situasjoner.
Ofte, for å gjøre hoppe enklere i plattformspill, kan karakteren hoppe hvis den bare gikk ut av kanten av en plattform og ikke lenger er på bakken. Dette er en populær metode for å redusere en illusjon at spilleren har trykket på hoppeknappen, men tegnet hoppet ikke, noe som kan ha oppstått på grunn av innspilt lag eller spilleren trykke på hoppeknappen rett etter at tegnet har flyttet seg fra plattformen.
La oss implementere en slik mekaniker nå. Først av alt, må vi legge til en konstant av hvor mange rammer etter at tegnet går ut av plattformen, kan det fortsatt utføre et hopp.
offentlig const int cJumpFramesThreshold = 4;
Vi trenger også en rammeteller i Karakter
klasse, så vi vet hvor mange rammer tegnet er i luften allerede.
beskyttet int mFramesFromJumpStart = 0;
La oss nå stille mFramesFromJumpStart
til 0 hver gang vi nettopp forlot bakken. La oss gjøre det rett etter at vi ringer UpdatePhysics
.
UpdatePhysics (); hvis (mWasOnGround &&! mOnGround) mFramesFromJumpStart = 0;
Og la oss øke det hver ramme vi er i hoppestaten.
tilfelle CharacterState.Jump: ++ mFramesFromJumpStart;
Hvis vi er i hoppetilstanden, kan vi ikke tillate et air jump hvis vi enten er i taket eller har en positiv vertikal hastighet. Positiv vertikal hastighet vil bety at karakteren ikke har gått glipp av et hopp.
++mFramesFromJumpStart; hvis (mFramesFromJumpStart <= Constants.cJumpFramesThreshold) if (mAtCeiling || mSpeed.y > 0.0f) mFramesFromJumpStart = Constants.cJumpFramesThreshold + 1;
Hvis det ikke er tilfelle og hoppetasten trykkes, er alt vi trenger å gjøre, sett den vertikale hastigheten til hoppverdien, som om vi hoppet normalt, selv om tegnet er i hoppestaten allerede.
hvis (mFramesFromJumpStart <= Constants.cJumpFramesThreshold) if (mAtCeiling || mSpeed.y > 0.0f) mFramesFromJumpStart = Constants.cJumpFramesThreshold + 1; ellers hvis (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed;
Og det er det! Vi kan sette cJumpFramesThreshold
til en stor verdi som 10 rammer for å sikre at den fungerer.
Effekten her er ganske overdrevet. Det er ikke veldig merkbart hvis vi tillater tegnet å hoppe bare 1-4 rammer etter at det faktisk ikke lenger er på bakken, men generelt gir dette oss mulighet til å endre hvor lindrende vi vil at våre hopp skal være.
La oss gjøre det mulig å skalere objektene. Vi har allerede mScale
i MovingObject
klasse, så alt vi faktisk trenger å gjøre er å sørge for at det påvirker AABB og AABB-kompensasjonen på riktig måte.
Først og fremst, la oss redigere vår AABB-klasse slik at den har en skala komponent.
offentlig struktur AABB offentlig Vector2 skala; offentlig Vector2-senter; offentlig Vector2 halfSize; offentlig AABB (Vector2 senter, Vector2 halfSize) skala = Vector2.one; this.center = center; this.halfSize = halfSize;
La oss nå redigere halv størrelse
, slik at når vi får tilgang til det, får vi faktisk en skalert størrelse i stedet for den ubestemte.
offentlig Vector2 skala; offentlig Vector2-senter; privat Vector2 halfSize; offentlig Vector2 HalfSize set halfSize = value; få returner ny Vector2 (halfSize.x * scale.x, halfSize.y * scale.y);
Vi vil også kunne få eller sette bare en X eller Y verdi av halv størrelse, så vi må lage separate getters og setters for dem også.
offentlig flyte HalfSizeX set halfSize.x = value; få return halfSize.x * scale.x; offentlig flyte HalfSizeY set halfSize.y = value; få return halfSize.y * scale.y;
Foruten å skalere AABB selv, må vi også skalere mAABBOffset
, slik at etter at vi skalere objektet, vil sprite fortsatt stemme overens med AABB på samme måte som objektet var ubeskyttet. La oss gå tilbake til MovingObject
klassen for å redigere den.
privat Vector2 mAABBOffset; offentlig Vector2 AABBOffset set mAABBOffset = verdi; få returner ny Vector2 (mAABBOffset.x * mScale.x, mAABBOffset.y * mScale.y);
Det samme som tidligere, vil vi også ha tilgang til X- og Y-komponenter separat.
offentlig float AABBOffsetX set mAABBOffset.x = value; få return mAABBOffset.x * mScale.x; offentlig flyt AABBOffsetY set mAABBOffset.y = value; få return mAABBOffset.y * mScale.y;
Til slutt må vi også sørge for at når skalaen er endret i MovingObject
, det er også endret i AABB. Objektets skala kan være negativ, men AABB selv skal ikke ha en negativ skala fordi vi stoler på halv størrelse for alltid å være positiv. Det er derfor i stedet for å bare passere skalaen til AABB, skal vi passere en skala som har alle komponentene positive.
privat Vector2 mScale; offentlig Vector2 Scale set mScale = value; mAABB.scale = ny Vector2 (Mathf.Abs (value.x), Mathf.Abs (value.y)); få return mScale; offentlig float ScaleX set mScale.x = value; mAABB.scale.x = Mathf.Abs (verdi); få return mScale.x; offentlig float ScaleY set mScale.y = value; mAABB.scale.y = Mathf.Abs (verdi); få return mScale.y;
Alt som er igjen å gjøre nå, er å sørge for at hvor vi brukte variablene direkte, bruker vi dem gjennom getters og setters nå. Uansett hvor vi brukte halfSize.x
, vi vil bruke HalfSizeX
, hvor vi brukte halfSize.y
, vi vil bruke HalfSizeY
, og så videre. Noen få bruksområder av en funn og erstattingsfunksjon bør håndtere denne brønnen.
Skalingen skal fungere godt nå, og på grunn av måten vi bygget våre kollisjonsdetekteringsfunksjoner, spiller det ingen rolle om tegnet er gigantisk eller lite - det skal samhandle med kartbrønnen.
Denne delen avslutter vårt arbeid med tilkartet. I de neste delene vil vi sette opp ting for å oppdage kollisjoner mellom objekter.
Det tok litt tid og krefter, men systemet generelt bør være veldig robust. En ting som kanskje mangler akkurat nå, er støtten til bakker. Mange spill stoler ikke på dem, men mange gjør det, så det er det største forbedringsmålet for dette systemet. Takk for at du har lest så langt, ser deg i neste del!