Grunnleggende 2D Platformer Fysikk, Del 6 Objekt vs Objekt Kollisionsrespons

I den forrige delen av serien implementerte vi en kollisjonsdeteksjonsmekanisme mellom spillobjektene. I denne delen bruker vi kollisjonsdeteksjonsmekanismen til å bygge et enkelt, men robust fysisk svar mellom objektene.

Demoen viser sluttresultatet av denne opplæringen. Bruk WASD til å flytte tegnet. Den midtre museknappen gir en enveisplattform, den høyre museknappen gir en solid flis, og mellomromstasten gir en tegnklon. Skyvekontrollene endrer størrelsen på spillerens karakter. 

Demoen har blitt publisert under Unity 5.4.0f3, og kildekoden er også kompatibel med denne versjonen av Unity.

Kollisionsrespons

Nå som vi har alle kollisionsdataene fra arbeidet vi har gjort i forrige del, kan vi legge til et enkelt svar på å kollidere objekter. Målet vårt er å gjøre det mulig for objektene ikke å gå gjennom hverandre som om de var på et annet plan - vi vil at de skal være faste og til å fungere som en hindring eller en plattform for andre gjenstander. For det må vi bare gjøre én ting: flytt objektet ut av en overlapping hvis en oppstår.

Dekke tilleggsdataene

Vi trenger ytterligere data for MovingObject klasse for å håndtere objektet vs. objektrespons. Først og fremst er det fint å ha en boolsk til å markere et objekt som kinematisk, det vil si, dette objektet vil ikke bli presset rundt av noe annet objekt. 

Disse objektene fungerer som plattformer, og de kan også flytte plattformer. De skal være de tyngste tingene rundt, slik at deres posisjon ikke blir korrigert på noen måte - andre gjenstander må flytte bort for å få plass til dem.

offentlig bool mIsKinematisk = false;

De andre dataene jeg liker å ha er informasjon om hvorvidt vi står på toppen av et objekt eller til venstre eller høyre side. Så langt kan vi bare samhandle med fliser, men nå kan vi også samhandle med andre objekter. 

For å få litt harmoni inn i dette, trenger vi et nytt sett med variabler som beskriver om tegnet skyver noe til venstre, høyre, topp eller bunn.

offentlig bool mPushesRight = false; offentlig bool mPushesLeft = false; offentlig bool mPushesBottom = false; offentlig bool mPushesTop = false; offentlig bool mPushedTop = false; offentlig bool mPushedBottom = false; offentlig bool mPushedRight = false; offentlig bool mPushedLeft = false; offentlig bool mPushesLeftObject = false; offentlig bool mPushesRightObject = false; offentlig bool mPushesBottomObject = false; offentlig bool mPushesTopObject = false; offentlig bool mPushedLeftObject = false; offentlig bool mPushedRightObject = false; offentlig bool mPushedBottomObject = false; offentlig bool mPushedTopObject = false; offentlig bool mPushesRightTile = false; offentlig bool mPushesLeftTile = false; offentlig bool mPushesBottomTile = false; offentlig bool mPushesTopTile = false; offentlig bool mPushedTopTile = false; offentlig bool mPushedBottomTile = false; offentlig bool mPushedRightTile = false; offentlig bool mPushedLeftTile = false;

Nå er det mange variabler. I en produksjonsinnstilling vil det være verdt å snu disse til flagg og ha bare ett heltall i stedet for alle disse booleanene, men for enkelhets skyld vil vi håndtere å forlate disse som de er. 

Som du kanskje ser, har vi ganske finkornet data her. Vi vet om tegnet skyver eller skyver et hinder i en bestemt retning generelt, men vi kan også enkelt spørre om vi er ved siden av en flis eller et objekt.

Flytt ut av overlapping

La oss lage UpdatePhysicsResponse funksjon, der vi skal håndtere objektet vs. objektrespons.

privat ugyldig UpdatePhysicsResponse () 

Først og fremst, hvis objektet er merket som kinematisk, returnerer vi bare. Vi håndterer ikke svaret fordi den kinematiske gjenstanden ikke trenger å svare på noe annet objekt - de andre objektene må svare på det.

hvis (mIsKinematisk) tilbake;

Nå forutsetter det at vi ikke trenger en kinematisk gjenstand for å få de riktige dataene om det skaper et objekt på venstre side, osv. Hvis det ikke er tilfelle, vil dette måtte bli endret litt, noe jeg Jeg kommer til å berøre senere på linjen.

La oss nå begynne å håndtere variablene som vi nylig har erklært.

mPushedBottomObject = mPushesBottomObject; mPushedRightObject = mPushesRightObject; mPushedLeftObject = mPushesLeftObject; mPushedTopObject = mPushesTopObject; mPushesBottomObject = false; mPushesRightObject = false; mPushesLeftObject = false; mPushesTopObject = false;

Vi lagrer resultatene fra forrige ramme til de riktige variablene, og antar nå at vi ikke berører noe annet objekt.

La oss begynne å iterere gjennom alle våre kollisionsdata nå.

for (int i = 0; i < mAllCollidingObjects.Count; ++i)  var other = mAllCollidingObjects[i].other; var data = mAllCollidingObjects[i]; var overlap = data.overlap; 

Først av, la oss håndtere de tilfellene der objektene knapt berører hverandre, ikke egentlig overlappende. I dette tilfellet vet vi at vi ikke virkelig trenger å flytte noe, bare sett variablene. 

Som nevnt før er indikatoren at objektene berører at overlappingen på en av aksene er lik 0. La oss starte med å sjekke x-aksen.

hvis (overlap.x == 0.0f) 

Hvis tilstanden er sant, må vi se om det andre objektet er på venstre eller høyre side av vårt AABB.

hvis (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x)  else 

Til slutt, hvis det er til høyre, sett deretter mPushesRightObject å true og sett hastigheten slik at den ikke er større enn 0, fordi vårt objekt ikke lenger kan bevege seg til høyre når banen er blokkert.

hvis (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0,0f);  annet 

La oss håndtere venstre side på samme måte.

hvis (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0,0f);  ellers mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0,0f); 

Til slutt vet vi at vi ikke trenger å gjøre noe annet her, så la oss fortsette til neste loopherreasjon.

hvis (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0,0f);  ellers mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0,0f);  Fortsette; 

La oss håndtere y-aksen på samme måte.

hvis (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0,0f);  ellers mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0,0f);  Fortsette;  annet hvis (overlap.y == 0.0f) if (other.mAABB.center.y> mAABB.center.y) mPushesTopObject = true; mSpeed.y = Mathf.Min (mSpeed.y, 0.0f);  ellers mPushesBottomObject = true; mSpeed.y = Mathf.Max (mSpeed.y, 0,0f);  Fortsette; 

Dette er også et godt sted å sette variablene for en kinematisk kropp, hvis vi trenger det. Vi ville ikke bryr seg om overlappingen er lik null eller ikke, fordi vi ikke kommer til å bevege en kinematisk gjenstand allikevel. Vi vil også måtte hoppe over hastighetsjusteringen som vi ikke ønsker å stoppe en kinematisk gjenstand. Vi hopper over å gjøre alt dette for demoen, da vi ikke skal bruke hjelpevariablene for kinematiske objekter.

Nå som dette er dekket, kan vi håndtere objektene som har skikkelig overlappet med vårt AABB. Før vi gjør det, skjønt, la meg forklare tilnærmingen jeg tok for å kollisjonere respons i demoen.

Først av alt, hvis objektet ikke beveger seg, og vi støter på det, bør det andre objektet forbli uberørt. Vi behandler det som en kinematisk kropp. Jeg bestemte meg for å gå på denne måten fordi jeg føler at den er mer generisk, og den pushing oppførelsen kan alltid håndteres lenger nede i køen i tilpasset oppdatering av en bestemt gjenstand.

Hvis begge objektene beveget seg under kollisjonen, splittede vi overlappene mellom dem basert på deres fart. Jo raskere de skulle, desto større del av overlappingsverdien vil de bli flyttet tilbake.

Det siste punktet er, på samme måte som tilkryssningsrespons-tilnærmingen, hvis et objekt faller og riper et annet objekt med en piksel horisontalt, vil objektet ikke glide av og fortsette å gå ned, men vil stå på den ene piksel.

Jeg tror dette er den mest sårbare tilnærmingen, og modifisering av det bør ikke være veldig vanskelig hvis du vil håndtere noe svar annerledes.

La oss fortsette implementeringen ved å beregne absolutt hastighetsvektoren for begge objektene under kollisjonen. Vi trenger også summen av hastighetene, så vi vet hvilken prosentandel av overlappingen vårt objekt skal flyttes.

Vector2 absSpeed1 = ny Vector2 (Mathf.Abs (data.pos1.x - data.oldPos1.x), Mathf.Abs (data.pos1.y - data.oldPos1.y)); Vector2 absSpeed2 = ny Vector2 (Mathf.Abs (data.pos2.x - data.oldPos2.x), Mathf.Abs (data.pos2.y - data.oldPos2.y)); Vector2 speedSum = absSpeed1 + absSpeed2;

Merk at i stedet for å bruke hastigheten som er lagret i kollisionsdataene, bruker vi forskyvningen mellom posisjonen på tidspunktet for kollisjon og rammen før det. Dette vil bare være mer nøyaktig i dette tilfellet, da hastigheten representerer bevegelsesvektor før den fysiske korreksjonen. Posisjonene selv korrigeres hvis objektet har truffet en solid flis, for eksempel, så hvis vi ønsker å få en korrigert hastighetsvektor, bør vi beregne det slik.

La oss nå begynne å beregne hastighetsforholdet for objektet vårt. Hvis det andre objektet er kinematisk, setter vi hastighetsforholdet til en for å sikre at vi beveger hele overlappingsvektoren, og hevder regelen om at den kinematiske gjenstanden ikke skal flyttes.

float speedRatioX, speedRatioY; hvis (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; ellers 

La oss nå starte med et merkelig tilfelle der begge objektene overlapper hverandre, men begge har ingen hastighet overhodet. Dette burde egentlig ikke skje, men hvis et objekt skaper overlappende et annet objekt, vil vi at de skal bevege seg fra hverandre naturlig. I så fall vil vi at begge skal bevege seg med 50% av overlappingsvektoren.

hvis (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; ellers hvis (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f; 

Et annet tilfelle er når speedSum på x-aksen er lik null. I så fall beregner vi riktig forhold for y-aksen, og angir at vi skal flytte 50% av overlappingen for x-aksen.

hvis (speedSum.x == 0.0f && speedSum.y == 0,0f) speedRatioX = speedRatioY = 0.5f;  annet hvis (speedSum.x == 0,0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y; 

På samme måte håndterer vi saken hvor speedSum er bare null på y-aksen, og i siste tilfelle beregner vi begge forholdene riktig.

hvis (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; ellers hvis (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f;  annet hvis (speedSum.x == 0,0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y;  annet hvis (speedSum.y == 0.0f) speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = 0.5f;  ellers speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = absSpeed1.y / speedSum.y; 

Nå som forholdene er beregnet, kan vi se hvor mye vi trenger for å kompensere for vårt objekt.

flyte offsetX = overlap.x * speedRatioX; float offsetY = overlap.y * speedRatioY;

Nå, før vi bestemmer om vi skal flytte objektet ut av kollisjon på x-aksen eller y-aksen, la vi beregne retningen hvor overlappingen har skjedd. Det er tre muligheter: enten støt vi inn i et annet objekt horisontalt, vertikalt eller diagonalt. 

I det første tilfellet vil vi flytte ut av overlapping på x-aksen, i andre tilfelle ønsker vi å bevege oss ut av overlappingen på y-aksen, og i det siste tilfellet ønsker vi å bevege seg overlapping på hvilken som helst akse hadde minst overlappingen.

Husk at for å overlappe med et annet objekt trenger vi AABBene å overlappe hverandre på både x og y-aksene. For å sjekke om vi støte på et objekt horisontalt, ser vi om forrige ramme vi allerede overlappte objektet på y-aksen. Hvis det er tilfelle, og vi har ikke vært overlappende på x-aksen, må overlappingen ha skjedd, fordi i de nåværende rammene begynte AABBene å overlappe på x-aksen, og derfor trekker vi ut at vi støpte på et annet objekt horisontalt.

Først av, la oss beregne om vi overlappte med den andre AABB i forrige ramme.

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;

La oss nå sette opp betingelsen for å flytte ut av overlappingen horisontalt. Som forklart før, trengte vi å ha overlappet på y-aksen og ikke overlappet på x-aksen i forrige ramme.

hvis (! overlappedLastFrameX && overlappedLastFrameY) 

Hvis det ikke er tilfelle, vil vi flytte ut av overlapping på y-aksen.

hvis (! overlappedLastFrameX && overlappedLastFrameY)  else 

Som nevnt ovenfor, må vi også dekke scenariet for bumping inn i objektet diagonalt. Vi stødte inn i objektet diagonalt hvis våre AABB ikke overlappte i forrige ramme på noen av aksene, fordi vi vet at i den nåværende rammen overlapper de begge, så kollisjonen må ha skjedd på begge aksene samtidig.

hvis ((overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX && overlappedLastFrameY))  else 

Men vi ønsker å bevege seg ut av overlappingen på aksen i tilfelle en diagonal støt bare hvis overlappingen på x-aksen er mindre enn overlappingen på y-aksen.

hvis ((overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX && overlappedLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y)))   else  

Det er alle tilfellene løst. Nå må vi faktisk flytte objektet fra overlappingen.

hvis ((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  

Som du kan se, håndterer vi det veldig likt det tilfellet der vi bare knapt berører en annen AABB, men i tillegg beveger vi vårt objekt med den beregnede forskyvningen.

Den vertikale korreksjonen gjøres på samme måte.

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

Det er nesten det; det er bare en ekstra advarsel å dekke. Tenk scenariet der vi lander på to gjenstander samtidig. Vi har to nesten identiske kollisionsdata-forekomster. Når vi løser gjennom alle sammenstøtene, korrigerer vi stillingen av kollisjonen med det første objektet, flyttet oss litt opp. 

Da håndterer vi kollisjonen for det andre objektet. Den lagrede overlapping på kollisjonstidspunktet er ikke lenger oppdatert, ettersom vi allerede har flyttet fra den opprinnelige posisjonen, og hvis vi skulle håndtere den andre kollisjonen det samme vi håndterte den første, ville vi igjen flytte litt opp , slik at objektet blir korrigert to ganger avstanden det skulle.

For å fikse dette problemet, holder vi oversikt over hvor mye vi allerede har korrigert objektet. La oss erklære vektoren offsetSum rett før vi begynner å iterere gjennom alle kollisjonene.

Vector2 offsetSum = Vector2.zero;

Nå, la oss sørge for å legge opp alle de kompensasjonene vi brukte på vårt objekt i denne vektoren.

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

Og til slutt, la oss kompensere hver sammenhengende kollisjonens overlapping med den kumulative vektoren av korrigeringer vi har gjort så langt.

var overlapping = data.overlap - offsetSum;

Nå dersom vi lander på to gjenstander av samme høyde samtidig, ville den første kollisjonen bli behandlet ordentlig, og den andre kollisjonens overlapping ville bli kompensert til null, noe som ikke lenger ville bevege vårt objekt.

Nå som vår funksjon er klar, la oss sørge for at vi bruker den. Et godt sted å ringe denne funksjonen ville være etter CheckCollisions anrop. Dette vil kreve at vi deler oss UpdatePhysics Fungerer i to deler, så la oss lage den andre delen akkurat nå, i MovingObject klasse.

offentlig ugyldig UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; 

I den andre delen kaller vi vår nystekte ferdig UpdatePhysicsResponse funksjon og oppdatere de generelle skyver venstre, høyre, bunn og toppvariabler. Etter dette trenger vi bare å bruke stillingen.

offentlig ugyldig UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; // oppdater aabb mAABB.center = mPosition; // bruk endringene til transform transform.position = ny Vector3 (Mathf.Round (mPosition.x), Mathf.Round (mPosition.y), mSpriteDepth); transform.localScale = ny Vector3 (ScaleX, ScaleY, 1.0f); 

Nå, i hovedspilloppdateringsløkken, la oss ringe den andre delen av fysikkoppdateringen etter CheckCollisions anrop.

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

Ferdig! Nå kan våre gjenstander ikke overlappe hverandre. Selvfølgelig, i en spillinnstilling, må vi legge til noen få ting som kollisjonsgrupper, etc., så det er ikke obligatorisk å oppdage eller svare på kollisjon med hvert objekt, men dette er ting som er avhengig av hvordan du vil ha ting satt opp i spillet ditt, så vi kommer ikke til å dykke inn i det.

Sammendrag

Det er det for en annen del av den enkle 2D-plattformen fysikk-serien. Vi benyttet kollisjonsdeteksjonsmekanismen som ble implementert i forrige del for å skape en enkel fysisk respons mellom objekter. 

Med disse verktøyene er det mulig å lage standardobjekter som flytteplattformer, skyveblokker, tilpassede hindringer og mange andre objekter som egentlig ikke kan være en del av skjemaet, men likevel må være en del av nivå terrenget på en eller annen måte. Det er en mer populær funksjon at vår fysikkimplementering mangler fremdeles, og det er bakkene. 

Forhåpentligvis i neste del vil vi begynne å utvide vår tilemap med støtten til disse, som vil fullføre det grunnleggende settet av funksjoner som en enkel fysikkimplementering for en 2D-plattformspiller burde ha, og det ville avslutte serien. 

Selvfølgelig er det alltid rom for forbedring, så hvis du har et spørsmål eller et tips om hvordan du gjør noe bedre, eller bare ha en mening om opplæringen, kan du gjerne bruke kommentarseksjonen til å gi meg beskjed!