I denne delen av 2D-plattformens fysikk-serie, oppretter vi en tilkart, og vi vil delvis implementere kollisjonsdetektering og -respons for objekt-tilskjerming.
Det er to grunnleggende tilnærminger til å bygge plattformsnivåer. En av dem er å bruke et rutenett og plassere de tilhørende fliser i celler, og den andre er et mer fritt form, hvor du kan løst plassere nivå geometri imidlertid og hvor du vil.
Det er fordeler å ulempe for begge tilnærminger. Vi bruker nettverket, så la oss se hva slags fordeler den har over den andre metoden:
La oss begynne med å opprette en kartklass. Den vil holde alle kartspesifikke dataene.
offentlig klasse Kart
Nå må vi definere alle fliser som kartet inneholder, men før vi gjør det, må vi vite hvilke fliser som finnes i vårt spill. For nå planlegger vi bare tre: en tom flis, en solid flis og en enveis plattform.
public enum TileType Tom, Block, OneWay
I demonstrasjonen samsvarer flisetyper direkte med hvilken type kollisjon vi vil ha med en flis, men i et ekte spill som ikke nødvendigvis er slik. Siden du har flere visuelt forskjellige fliser, ville det være bedre å legge til nye typer som GrassBlock, GrassOneWay og så videre, for å la TileType enum definere ikke bare kollisjonstypen, men også utseendet til flisen.
Nå i kartklassen kan vi legge til en rekke fliser.
offentlig klasse Kart private TileType [,] mTiles;
Selvfølgelig er en tilkart som vi ikke kan se, ikke til stor nytte for oss, så vi trenger også sprites for å sikkerhetskopiere flisdataene. Normalt i Unity er det ekstremt ineffektivt å ha hver flis som et eget objekt, men siden vi bare bruker dette til å teste vår fysikk, er det OK å gjøre det på denne måten i demonstrasjonen.
privat SpriteRenderer [,] mTilesSprites;
Kartet trenger også en posisjon i verdensrommet, slik at hvis vi trenger mer enn bare en enkelt, kan vi flytte dem fra hverandre.
offentlig Vector3 mPosition;
Bredde og høyde, i fliser.
offentlig int mWidth = 80; offentlig int mHeight = 60;
Og flisestørrelsen: I demoen jobber vi med en ganske liten flisestørrelse, som er 16 med 16 piksler.
offentlig const int cTileSize = 16;
Det ville være det. Nå trenger vi et par hjelpefunksjoner for å la oss få tilgang til kartens data enkelt. La oss starte med å lage en funksjon som vil konvertere verdens koordinater til kartets flisekoordinater.
offentlig Vector2i GetMapTileAtPoint (Vector2 punkt)
Som du kan se, tar denne funksjonen en Vektor2
som en parameter og returnerer a Vector2i
, som egentlig er en 2D-vektor som opererer på heltall i stedet for flyter.
Konvertere verdensposisjonen til kartposisjonen er veldig grei, vi trenger bare å skifte punkt
av mPosition
slik at vi returnerer flisen i forhold til kartets posisjon og deretter deler resultatet etter flisestørrelsen.
offentlig Vector2i GetMapTileAtPoint (Vector2 punkt) returner ny Vector2i ((int) ((point.x - mPosition.x + cTileSize / 2.0f) / (float) (cTileSize)), (int) ((point.y - mPosition. y + cTileSize / 2.0f) / (float) (cTileSize)));
Legg merke til at vi måtte skifte punkt
i tillegg av cTileSize / 2.0f
, fordi flisens pivot er i sentrum. La oss også lage to tilleggsfunksjoner som bare returnerer X- og Y-komponenten i stillingen i kartrommet. Det vil være nyttig senere.
offentlig int GetMapTileYAtPoint (float y) return (int) ((y - mPosition.y + cTileSize / 2.0f) / (float) (cTileSize)); offentlig int GetMapTileXAtPoint (float x) return (int) ((x - mPosition.x + cTileSize / 2.0f) / (float) (cTileSize));
Vi skal også skape en komplementær funksjon som, gitt en flis, vil returnere sin posisjon i verdensrommet.
offentlig Vector2 GetMapTilePosition (int tileIndexX, int tileIndexY) returner ny Vector2 ((float) (tileIndexX * cTileSize) + mPosition.x, (float) (tileIndexY * cTileSize) + mPosition.y); offentlig Vector2 GetMapTilePosition (Vector2i tileCoords) returner ny Vector2 ((float) (tileCoords.x * cTileSize) + mPosition.x, (float) (tileCoords.y * cTileSize) + mPosition.y);
Bortsett fra å oversette stillinger, må vi også ha et par funksjoner for å se om en flis på en bestemt posisjon er tom, er en solid flis eller en enveis plattform. La oss starte med en svært generell GetTile-funksjon, som vil returnere en type en bestemt flis.
offentlig TileType GetTile (int x, int y) hvis (x < 0 || x >= mWidth || y < 0 || y >= mHeight) returnere TileType.Block; returner mTiles [x, y];
Som du kan se, før vi returnerer flisetypen, kontrollerer vi om den oppgitte posisjonen er ubegrenset. Hvis det er, vil vi behandle det som en solid blokk, ellers returnerer vi en sann type.
Den neste i køen er en funksjon for å sjekke om en flis er et hinder.
offentlig bool IsObstacle (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) returnere sant; returnere (mTiles [x, y] == TileType.Block);
På samme måte som før kontrollerer vi om flisene er ubegrensede, og hvis det er så kommer vi tilbake sant, så en flis utenom grensene blir behandlet som en hindring.
La oss nå sjekke om flisen er en flis. Vi kan stå på både en blokk og en enveis plattform, så vi må returnere sant hvis flisen er noen av disse to.
offentlig bool IsGround (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) returnere false; returnere (mTiles [x, y] == TileType.OneWay || mTiles [x, y] == TileType.Block);
Til slutt, la oss legge til IsOneWayPlatform
og Er tom
Fungerer på samme måte.
offentlig bool IsOneWayPlatform (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) returnere false; returnere (mTiles [x, y] == TileType.OneWay); offentlig bool IsEmpty (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) returnere false; returnere (mTiles [x, y] == TileType.Empty);
Det er alt vi trenger vår kartklass til å gjøre. Nå kan vi gå videre og implementere tegnet kollisjon mot det.
La oss gå tilbake til MovingObject
klasse. Vi må opprette et par funksjoner som vil oppdage om tegnet kolliderer med tilkartet.
Metoden der vi skal vite om karakteren kolliderer med en flis eller ikke, er veldig enkel. Vi kontrollerer alle fliser som eksisterer rett utenfor det bevegelige objektets AABB.
Den gule boksen representerer karakterens AABB, og vi skal sjekke flisene langs de røde linjene. Hvis noen av disse overlapper med en flis, setter vi en tilsvarende kollisjonsvariabel til sann (for eksempel mOnGround
, mPushesLeftWall
, mAtCeiling
eller mPushesRightWall
).
La oss starte med å opprette en funksjon HasGround, som vil sjekke om karakteren kolliderer med en bakkeflise.
offentlig bool HasGround (Vector2 oldPosition, Vector2 posisjon, Vector2 speed, out float groundY)
Denne funksjonen returnerer sant hvis tegn overlapper med noen av bunnflisene. Den tar den gamle posisjonen, gjeldende posisjon og gjeldende hastighet som parametere, og returnerer også Y-posisjonen til toppen av flisen vi kolliderer med, og om kollidert flis er en enveis-plattform eller ikke.
Det første vi ønsker å gjøre er å beregne sentrum av AABB.
offentlig bool HasGround (Vector2 oldPosition, Vector2 posisjon, Vector2 speed, out float groundY) var senter = posisjon + mAABBOffset;
Nå som vi har det, for den nederste kollisjonskontrollen må vi beregne begynnelsen og slutten av bunnsensorlinjen. Sensorlinjen er bare en piksel under AABBs nederste kontur.
offentlig bool HasGround (Vector2 oldPosition, Vector2 posisjon, Vector2 speed, out float groundY) var senter = posisjon + mAABBOffset; var bottomLeft = senter - mAABB.halfSize - Vector2.up + Vector2.right; var bottomRight = ny Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y);
De nede til venstre
og Nede til høyre
representerer sensorens to ender. Nå som vi har disse, kan vi beregne hvilke fliser vi må sjekke. La oss begynne med å lage en løkke der vi skal gå gjennom flisene fra venstre til høyre.
for (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize)
Vær oppmerksom på at det ikke er noen betingelse for å gå ut av sløyfen her - vi gjør det på slutten av løkken.
Det første vi bør gjøre i løkken er å sørge for at checkedTile.x
er ikke større enn den høyre enden av sensoren. Dette kan være tilfellet fordi vi flytter det markerte punktet med multipler av flisestørrelsen, for eksempel hvis tegnet er 1,5 fliser bredt, må vi sjekke flisen på sensorens venstre kant, så en flis til høyre , og deretter 1,5 fliser til høyre i stedet for 2.
for (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkedTile.x, bottomRight.x);
Nå må vi få flisekoordinatet i kartrommet for å kunne sjekke flisens type.
int tileIndexX, tileIndexY; for (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkedTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkedTile.x); tileIndexY = mMap.GetMapTileYAtPoint (checkedTile.y);
Først må vi beregne flisens toppstilling.
int tileIndexX, tileIndexY; for (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkedTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkedTile.x); tileIndexY = mMap.GetMapTileYAtPoint (checkedTile.y); groundY = (float) tileIndexY * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.y;
Nå, hvis den nåværende kontrollerte flisen er et hinder, kan vi lett returnere sant.
int tileIndexX, tileIndexY; for (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkedTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkedTile.x); tileIndexY = mMap.GetMapTileYAtPoint (checkedTile.y); groundY = (float) tileIndexY * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.y; hvis (mMap.IsObstacle (tileIndexX, tileIndexY)) returnere sant;
Til slutt, la oss sjekke om vi allerede har sett gjennom alle fliser som krysser med sensoren. Hvis det er tilfelle, kan vi trygt gå ut av sløyfen. Etter at vi forlot sløyfen, fant vi ikke en flis vi kolliderte med, vi må returnere falsk
å la den som ringer vet at det ikke er noen grunn under objektet.
int tileIndexX, tileIndexY; for (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkedTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkedTile.x); tileIndexY = mMap.GetMapTileYAtPoint (checkedTile.y); groundY = (float) tileIndexY * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.y; hvis (mMap.IsObstacle (tileIndexX, tileIndexY)) returnere sant; hvis (checkedTile.x> = bottomRight.x) pause; returner falsk;
Det er den mest grunnleggende versjonen av sjekken. La oss prøve å få det til jobb nå. Tilbake i UpdatePhysics
funksjonen, vår gamle grunnkontroll ser slik ut.
hvis (mPosition.y <= 0.0f) mPosition.y = 0.0f; mOnGround = true; else mOnGround = false;
La oss erstatte den ved hjelp av den nylig opprettede metoden. Hvis tegnet faller ned og vi har funnet et hinder på vei, må vi flytte det ut av kollisjonen og også sette mOnGround
til sant. La oss starte med tilstanden.
flyte groundY = 0; hvis (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY))
Hvis tilstanden er oppfylt, må vi flytte tegnet på toppen av flisen vi kolliderte med.
flyte groundY = 0; hvis (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY)) mPosition.y = groundY + mAABB.halfSize.y - mAABBOffset.y;
Som du kan se, er det veldig enkelt fordi funksjonen returnerer bakkenivået som vi skal justere objektet på. Etter dette trenger vi bare å stille den vertikale hastigheten til null og sette mOnGround
til sant.
flyte groundY = 0; hvis (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY)) mPosition.y = groundY + mAABB.halfSize.y - mAABBOffset.y; mSpeed.y = 0.0f; mOnGround = true;
Hvis vår vertikale hastighet er større enn null eller vi ikke berører noe grunnlag, må vi sette inn mOnGround
til falsk
.
flyte groundY = 0; hvis (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY)) mPosition.y = groundY + mAABB.halfSize.y - mAABBOffset.y; mSpeed.y = 0.0f; mOnGround = true; else mOnGround = false;
La oss se hvordan dette fungerer.
Som du kan se, fungerer det bra! Kollisjonsdeteksjonen til veggene på begge sider og på toppen av tegnet er fortsatt ikke der, men tegnet stopper hver gang den møter bakken. Vi trenger fortsatt å legge litt mer arbeid i kollisjonskontrollfunksjonen for å gjøre den robust.
Et av problemene vi trenger å løse er synlig hvis tegnet er kompensert fra en ramme til den andre er for stor til å oppdage kollisjonen på riktig måte. Dette er illustrert i det følgende bildet.
Denne situasjonen skjer ikke nå, fordi vi låste maksimal fallhastighet til en rimelig verdi og oppdaterer fysikken med 60 FPS frekvens, så forskjellene i stillinger mellom rammene er ganske små. La oss se hva som skjer hvis vi oppdaterer fysikken kun 30 ganger per sekund.
Som du kan se, i dette scenariet svikter vår grunnkollisjonskontroll oss. For å fikse dette, kan vi ikke bare sjekke om tegnet har jordet under ham på nåværende posisjon, men vi må heller se om det var noen hindringer underveis fra den forrige rammens posisjon.
La oss gå tilbake til vår HasGround
funksjon. Her, foruten beregning av senteret, vil vi også beregne den forrige rammens senter.
offentlig bool HasGround (Vector2 oldPosition, Vector2 posisjon, Vector2 speed, out float groundY) var oldCenter = oldPosition + mAABBOffset; var senter = posisjon + mAABBOffset;
Vi må også få den forrige rammens sensorposisjon.
offentlig bool HasGround (Vector2 oldPosition, Vector2 posisjon, Vector2 speed, out float groundY) var oldCenter = oldPosition + mAABBOffset; var senter = posisjon + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var bottomLeft = senter - mAABB.halfSize - Vector2.up + Vector2.right; var bottomRight = ny Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y);
Nå må vi beregne på hvilken flis vertikalt vi skal begynne å sjekke om det er kollisjon eller ikke, og hvor vi stopper.
offentlig bool HasGround (Vector2 oldPosition, Vector2 posisjon, Vector2 speed, out float groundY) var oldCenter = oldPosition + mAABBOffset; var senter = posisjon + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var bottomLeft = senter - mAABB.halfSize - Vector2.up + Vector2.right; var bottomRight = ny Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y); int endY = mMap.GetMapTileYAtPoint (bottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY);
Vi starter søket fra flisen ved den forrige rammens sensorposisjon, og avslutter den ved den aktuelle rammens sensorposisjon. Det er selvfølgelig fordi når vi ser etter en jordkollisjon, antar vi at vi faller ned, og det betyr at vi beveger oss fra den høyere posisjonen til den nedre.
Til slutt må vi ha en annen iterasjonssløyfe. Nå, før vi fyller koden for denne ytre sløyfen, må vi vurdere følgende scenario.
Her kan du se en pil bevege seg fort. Dette eksemplet viser at vi ikke bare trenger å iterere gjennom alle flisene vi måtte passere vertikalt, men også å interpolere objektets posisjon for hver flis vi går gjennom for å omtrentliggjøre banen fra den forrige rammens posisjon til den nåværende. Hvis vi bare fortsatte å bruke det nåværende objektets posisjon, så ville det i et tilfelle oppdages en kollisjon, selv om den ikke burde være.
La oss omdøpe nede til venstre
og Nede til høyre
som newBottomLeft
og newBottomRight
, så vi vet at dette er den nye rammens sensorposisjoner.
offentlig bool HasGround (Vector2 oldPosition, Vector2 posisjon, Vector2 speed, out float groundY) var oldCenter = oldPosition + mAABBOffset; var senter = posisjon + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomLeft = senter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomRight = ny Vector2 (newBottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, newBottomLeft.y); int endY = mMap.GetMapTileYAtPoint (newBottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY); int tileIndexX; for (int tileIndexY = begY; tileIndexY> = endY; --tileIndexY) returner false;
Nå, innenfor denne nye sløyfen, la vi interpolere sensorposisjonene, slik at i begynnelsen av sløyfen antar vi at sensoren er i forrige rammes posisjon, og i slutten vil den være i den nåværende rammens posisjon.
offentlig bool HasGround (Vector2 oldPosition, Vector2 posisjon, Vector2 speed, out float groundY) var oldCenter = oldPosition + mAABBOffset; var senter = posisjon + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomLeft = senter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomRight = ny Vector2 (newBottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, newBottomLeft.y); int endY = mMap.GetMapTileYAtPoint (newBottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY); int dist = Mathf.Max (Mathf.Abs (endy - begY), 1); int tileIndexX; for (int tileIndexY = begY; tileIndexY> = endY; --tileIndexY) var bottomLeft = Vector2.Lerp (newBottomLeft, oldBottomLeft, (float) Mathf.Abs (endY - tileIndexY) / dist); var bottomRight = ny Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y); returner falsk;
Merk at vi interpolerer vektorer basert på forskjellen i fliser på Y-aksen. Når gamle og nye stillinger er innenfor samme flis, vil den vertikale avstanden være null, så i så fall ville vi ikke kunne dele avstanden. Så for å løse dette problemet, vil vi ha avstanden til å ha en minimumsverdi på 1, slik at hvis et slikt scenario skulle skje (og det kommer til å skje veldig ofte), bruker vi ganske enkelt den nye posisjonen for kollisjon gjenkjenning.
Til slutt, for hver iterasjon, må vi utføre den samme koden vi allerede gjorde for å kontrollere bakken kollisjonen langs objektets bredde.
offentlig bool HasGround (Vector2 oldPosition, Vector2 posisjon, Vector2 speed, out float groundY) var oldCenter = oldPosition + mAABBOffset; var senter = posisjon + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomLeft = senter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomRight = ny Vector2 (newBottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, newBottomLeft.y); int endY = mMap.GetMapTileYAtPoint (newBottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY); int dist = Mathf.Max (Mathf.Abs (endy - begY), 1); int tileIndexX; for (int tileIndexY = begY; tileIndexY> = endY; --tileIndexY) var bottomLeft = Vector2.Lerp (newBottomLeft, oldBottomLeft, (float) Mathf.Abs (endY - tileIndexY) / dist); var bottomRight = ny Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y); for (var checkedTile = bottomLeft;; checkedTile.x + = Map.cTileSize) checkedTile.x = Mathf.Min (checkedTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (checkedTile.x); groundY = (float) tileIndexY * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.y; hvis (mMap.IsObstacle (tileIndexX, tileIndexY)) returnere sant; hvis (checkedTile.x> = bottomRight.x) pause; returnere false;
Det er ganske mye det. Som du kan tenke deg, hvis spillets objekter beveger seg veldig fort, kan denne måten å sjekke kollisjon være ganske mye dyrere, men det forsikrer oss også om at det ikke vil være noen rare glitches med gjenstander som beveger seg gjennom faste vegger.
Phew, det var mer kode enn vi trodde vi skulle trenge, ikke sant? Hvis du ser noen feil eller mulige snarveier å ta, la meg og alle vite i kommentarene! Kollisjonskontrollen skal være robust nok, slik at vi ikke trenger å bekymre oss for noen uheldige hendelser av objekter som glir gjennom tegningens blokker.
Mye av koden ble skrevet for å sikre at det ikke er noen objekter som går gjennom fliser med store hastigheter, men hvis det ikke er et problem for et bestemt spill, kan vi trygt fjerne tilleggskoden for å øke ytelsen. Det kan også være en god ide å få et flagg for bestemte raskt bevegelige objekter, slik at bare de som bruker de dyrere versjonene av kontrollene.
Vi har fortsatt mange ting å dekke, men vi klarte å lage en pålitelig kollisjonskontroll for bakken, som kan speiles ganske enkelt i forhold til de andre tre retningene. Vi gjør det i neste del.