A * Pathfinding for 2D Grid-Based Platformers Ledge Grabbing

I denne delen av serien vår om å tilpasse A * -sporingsalgoritmen til plattformspillere, presenterer vi en ny mekaniker til tegnet: Ledge griper. Vi vil også gjøre hensiktsmessige endringer i både pathfinding-algoritmen og bot AI, slik at de kan benytte seg av den forbedrede mobiliteten.

Demo

Du kan spille Unity demo, eller WebGL versjonen (16MB), for å se det endelige resultatet i aksjon. Bruk WASD å flytte tegnet, venstre klikk på et sted for å finne en sti du kan følge for å komme dit, Høyreklikk en celle for å skifte bakken på det punktet, middle-klikk å plassere en enveis plattform, og Klikk og dra skyvekontrollene for å endre sine verdier.

Ledge Grabbing Mechanics

Kontrollerer oversikt

La oss først ta en titt på hvordan ledge-gripende mekaniker jobber i demonstrasjonen for å få innblikk i hvordan vi skal endre vår veiviseralgoritme for å ta denne nye mekanikeren i betraktning.

Kontrollene for hengsling er ganske enkle: hvis tegnet ligger rett ved siden av en kant mens du faller, og spilleren trykker på venstre eller høyre retnings-tast for å flytte dem mot den aktuelle hylle, da når tegnet er i riktig posisjon, vil det gripe skråningen.

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 (S), eller den retningsmessige keyn som peker bort fra skråningen.

Implementere kontrollene

La oss gå over hvordan ledge grab kontrollerer arbeid i koden. Det første du må gjøre er å oppdage om skjermen er til venstre eller til høyre for tegnet:

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. Som du kan se, for å slippe ned, må spilleren enten:

  • trykk på ned-knappen,
  • trykk på venstre knapp når vi tar en kant til høyre, eller
  • trykk på høyre knapp når vi tar en kant til venstre.
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. Det er det følgende kodestykket gjør:

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; 

Etter dette endrer vi karakteren til tegnet til Hoppe, som skal håndtere hoppefysikken:

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; 

Til slutt, hvis tegnet ikke droppet fra skjermen, kontrollerer vi om hoppetasten har blitt trykket. Hvis ja, setter vi hoppens vertikale hastighet og endrer tilstanden:

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;  else if (mInputs[(int)KeyInput.Jump])  mSpeed.y = mJumpSpeed; mCurrentState = CharacterState.Jump; 

Registrere en Ledge Grab Point

La oss se på hvordan vi avgjør om en hylle kan bli tatt. Vi bruker noen hotspots rundt kanten av tegnet:

Den gule konturen representerer karakterens grenser. De røde segmentene representerer veggfølere; disse brukes til å håndtere karakterfysikken. De blå segmentene representerer hvor vår karakter kan ta tak i en kant.

For å finne ut om tegnet kan ta tak i en kant, kontrollerer koden vår den siden den beveger seg mot. Det er på jakt etter en tom flis øverst i det blå segmentet, og deretter en solid flis under den som tegnet kan gripe inn på. 

Merk: Ledge griper er låst av hvis tegnet hopper opp. Dette kan lett legges merke til i demoen og i animasjonen i avsnittet Kontroller oversikt.

Hovedproblemet med denne metoden er at hvis vår karakter faller i høy hastighet, er det lett å savne et vindu der det kan ta tak i en kant. Vi kan løse dette ved å slå opp alle flisene som går fra forrige rammeposisjon til den nåværende rammen på jakt etter noen tomme fliser over en solid en. Hvis en slik flis er funnet, så kan den bli tatt.

Nå har vi klargjort hvordan håndteringsmaskinen som går i gang, la oss se hvordan vi skal innlemme den i vår veiingsalgoritme.

Pathfinder Changes

Gjør det mulig å slå ledge på og av

Først av alt, la oss legge til en ny parameter til vår FindPath funksjon som angir om stiften skal overveie å ta tak i leddene. Vi nevner det useLedges:

offentlig liste FindPath (Vector2i start, Vector2i ende, int characterWidth, int characterHeight, short maxCharacterJumpHeight, bool useLedges)

Oppdag Ledge Grab Noder

Forhold

Nå må vi endre funksjonen for å oppdage om en bestemt node kan brukes til hyllefeste. Vi kan gjøre det etter å ha sjekket om noden er en "på bakken" knutepunkt eller en "i tak" knutepunkt, fordi det i begge tilfeller ikke kan brukes til hyllefeste.

hvis (onGround) newJumpLength = 0; ellers hvis (atCeiling) if (mNewLocationX! = mLocationX) newJumpLength = (kort) Mathf.Max (maxCharacterJumpHeight * 2 + 1, jumpLength + 1); ellers newJumpLength = (short) Mathf.Max (maxCharacterJumpHeight * 2, jumpLength + 2);  ellers hvis (/ * kontroller om det er en ledge-grabbende node her * /)  annet hvis (mNewLocationY < mLocationY) 

Ok, nå må vi finne ut når en node skal betraktes som en hyllehåndteringsnode. For cliarity, her er et diagram som viser noen eksempler på hylleposisjoner:

... og her er hvordan disse kan se i spillet:

Toppkarakter sprites er strukket for å vise hvordan dette ser ut med tegn i forskjellige størrelser.

De røde cellene representerer de kontrollerte nodene; sammen med de grønne cellene representerer de tegnet i vår algoritme. De to øverste situasjonene viser henholdsvis 2x2 tegnflagger til venstre og høyre. Bunnen to viser det samme, men tegnets størrelse her er 1x3 i stedet for 2x2.

Som du kan se, bør det være ganske enkelt å oppdage disse sakene i algoritmen. Betingelsene for ledge grab node vil være som følger:

  1. Det er en solid flis ved siden av topp-høyre / topp-venstre tegnet flis.
  2. Det er en tom flise over den solide flisen som er funnet.
  3. Det er ingen solid flis under karakteren (ikke nødvendig å ta tak i ledges hvis på bakken).

Vær oppmerksom på at den tredje tilstanden allerede er tatt vare på, siden vi kontrollerer at ledgegradsnoden bare er hvis tegnet ikke er på bakken.

Først og fremst, la oss sjekke om vi faktisk vil oppdage ledge grabs:

ellers hvis (brukLedges)

La oss nå sjekke om det er en flis til høyre for tegn til høyre til høyre:

ellers hvis (useLedges && mGrid [mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0)

Og så, hvis over den flisen er det et tomt rom:

ellers hvis (useLedges && mGrid [mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 && mGrid [mNewLocationX + characterWidth, mNewLocationY + characterHeight]! = 0)

Nå må vi gjøre det samme for venstre side:

ellers hvis (useLedges && ((mGrid [mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 && mGrid [mNewLocationX + characterWidth, mNewLocationY + characterHeight]! = 0) || (mGrid [mNewLocationX - 1, mNewLocationY + characterHeight - 1] == 0 && mGrid [mNewLocationX - 1, mNewLocationY + characterHeight]! = 0)))

Det er enda en ting vi kan gjøre, noe som deaktiverer å finne leddgrabnutene hvis fallhastigheten er for høy, slik at banen ikke returnerer noen ekstreme hylleposisjoner som ville være vanskelig å følge med boten:

ellers hvis (useLedges && jumpLength <= maxCharacterJumpHeight * 2 + 6 && ((mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 && mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight] != 0) || (mGrid[mNewLocationX - 1, mNewLocationY + characterHeight - 1] == 0 && mGrid[mNewLocationX - 1, mNewLocationY + characterHeight] != 0)))  

Etter alt dette kan vi være sikre på at den funnet noden er en ledge grab node.

Legge til en spesiell knutepunkt

Hva gjør vi når vi finner en ledge grab node? Vi må sette hoppverdien sin. 

Husk at hoppverdien er tallet som representerer hvilken fase av hoppet tegnet ville være, hvis det nådde denne cellen. Hvis du trenger et sammendrag på hvordan algoritmen fungerer, ta en titt på teoriartikkelen.

Det ser ut til at alt vi trenger å gjøre er å sette hoppverdien til node til 0, fordi karakteren effektivt kan tilbakestille et hopp som om det var på bakken, men det er noen poeng å vurdere her. 

  • For det første ville det være fint om vi kunne fortelle et øyeblikk om noden er en ledge grab node eller ikke: dette vil være utrolig nyttig når du oppretter en botadferd, og også når du filtrerer noder. 
  • For det andre kan vanligvis hopping fra bakken utføres fra hvilket punkt som helst som passer best på en bestemt flis, men når du hopper fra en ledge, griper karakteren seg til en bestemt posisjon og kan ikke gjøre noe annet enn å begynne å falle eller hoppe oppover.

Tatt i betraktning disse forbeholdene, legger vi til en spesiell hoppeverdi for ledgegrabnoder. Det spiller ingen rolle hva denne verdien er, men det er en god ide å gjøre det negativt, da det vil redusere sjansene for feilfortolkning av noden.

const kort cLedgeGrabJumpValue = -9;

La oss nå tilordne denne verdien når vi oppdager en ledge grab node:

ellers hvis (useLedges && jumpLength <= maxCharacterJumpHeight * 2 + 6 && ((mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 && mGrid[mNewLocationX + characterWidth, mNewLocationY + characterHeight] != 0) || (mGrid[mNewLocationX - 1, mNewLocationY + characterHeight - 1] == 0 && mGrid[mNewLocationX - 1, mNewLocationY + characterHeight] != 0)))  newJumpLength = cLedgeGrabJumpValue; 

Lager cLedgeGrabJumpValue negativ vil påvirke nodekostberegningen - det vil gjøre at algoritmen foretrekker å bruke ledges i stedet for å hoppe over dem. Det er to ting å merke seg her:

  1. Ledge grab poeng gir en større mulighet for bevegelse enn noen andre air-noder, fordi karakteren kan hoppe igjen ved å bruke dem; fra dette synspunkt er det bra at disse noderne blir billigere enn andre. 
  2. Å gripe for mange ledges fører ofte til unaturlig bevegelse, for det er vanligvis at spillerne ikke bruker ledge, med mindre de er nødt til å nå et sted.

I animasjonen ovenfor kan du se forskjellen mellom å flytte opp når kantene er foretrukne og når de ikke er.

For nå vil vi forlate kostnadsberegningen som den er, men det er ganske enkelt å endre det, for å gjøre ledge noder dyrere.

Endre hoppverdien når du hopper eller faller fra en kant

Nå må vi justere hoppverdiene for noder som starter fra ledgepunktet. Vi trenger å gjøre dette fordi det er ganske annerledes å hoppe fra en hylleposisjon enn å hoppe fra bakken. Det er svært lite frihet når du hopper fra en kant, fordi karakteren er festet til et bestemt punkt. 

Når du er på bakken, kan tegnet bevege seg fritt til venstre eller høyre og hoppe på det passende tidspunktet.

Først, la oss sette saken når tegnet faller ned fra en ledge grab:

ellers hvis (mNewLocationY < mLocationY)  if (jumpLength == cLedgeGrabJumpValue) newJumpLength = (short)(maxCharacterJumpHeight * 2 + 4); else if (jumpLength % 2 == 0) newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 2); else newJumpLength = (short)Mathf.Max(maxCharacterJumpHeight * 2, jumpLength + 1); 

Som du ser, er den nye hoppelengden litt større hvis tegnet faller fra en kant: På denne måten kompenserer vi for manglende manøvrerbarhet mens du tar tak i en kant, noe som gir høyere vertikal hastighet før spilleren kan nå andre noder.

Neste er tilfellet der tegnet faller til en side fra å ta tak i en kant:

ellers hvis (! onGround && mNewLocationX! = mLocationX) if (jumpLength == cLedgeGrabJumpValue) newJumpLength = (kort) (maxCharacterJumpHeight * 2 + 3); ellers newJumpLength = (kort) Mathf.Max (jumpLength + 1, 1); 

Alt vi trenger å gjøre er å sette hoppverdien til fallende verdi.

Ignorer flere noder

Vi må legge til et par tilleggsbetingelser for når vi trenger å ignorere noder. 

Først av alt, når vi hopper fra en hylleposisjon, må vi gå opp, ikke til siden. Dette fungerer på samme måte som å bare hoppe fra bakken. Den vertikale hastigheten er mye høyere enn mulig horisontal fart på dette punktet, og vi må modellere dette faktum i algoritmen:

hvis (jumpLength == cLedgeGrabJumpValue && mLocationX! = mNewLocationX && newJumpLength < maxCharacterJumpHeight * 2) continue;

Hvis vi vil tillate å slippe fra ledgen til motsatt side slik:

Da må vi redigere tilstanden som ikke tillater horisontal bevegelse når hoppverdien er merkelig. Det er fordi for øyeblikket er vår spesielle ledgegradsverdi lik -9, så det er bare hensiktsmessig å ekskludere alle negative tall fra denne tilstanden.

hvis (jumpLength> = 0 && jumpLength% 2! = 0 && mLocationX! = mNewLocationX) fortsett;

Oppdater nodens filter

Til slutt, la oss gå videre til nodefiltrering. Alt vi trenger å gjøre her er å legge til en betingelse for ledge-grabbende noder, slik at vi ikke filtrerer dem ut. Vi må bare sjekke om nodens hoppverdien er lik cLedgeGrabJumpValue:

|| (fNodeTmp.JumpLength == cLedgeGrabJumpValue)

Hele filtreringen ser slik ut nå:

hvis ((mClose.Count == 0) || (mMap.IsOneWayPlatform (fNode.x, fNode.y - 1)) || (mGrid [fNode.x, fNode.y - 1] == 0 && mMap.IsOneWayPlatform (fPrevNode.x, fPrevNode.y - 1)) || (fNodeTmp.JumpLength == 3) || (fNextNodeTmp.JumpLength! = 0 && fNodeTmp.JumpLength == 0) // mark hopp starter || (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength! = 0) // mark landinger || (fNode.y> mClose [mClose.Count - 1] .y && fNode.y> fNodeTmp.PY) || (fNodeTmp.JumpLength == cLedgeGrabJumpValue ) || (fNode.y < mClose[mClose.Count - 1].y && fNode.y < fNodeTmp.PY) || ((mMap.IsGround(fNode.x - 1, fNode.y) || mMap.IsGround(fNode.x + 1, fNode.y)) && fNode.y != mClose[mClose.Count - 1].y && fNode.x != mClose[mClose.Count - 1].x)) mClose.Add(fNode);

Det er det - dette er alle endringene som vi trengte å gjøre for å oppdatere veiviseringsalgoritmen.

Bot-endringer

Nå som vår bane viser stedene hvor et tegn kan ta tak i en hylle, la vi endre botens oppførsel slik at den benytter seg av disse dataene.

Stopp Kalkulere nådd nå og nå

Først av alt, for å gjøre ting klarere i botten, la oss oppdatere GetContext () funksjon. Det nåværende problemet med det er det reachedX og reachedY verdiene beregnes konstant, noe som fjerner litt informasjon om konteksten. Disse verdiene brukes til å se om boten allerede har nådd målkoden på henholdsvis sine x- og y-akser. (Hvis du trenger en oppdatering på hvordan dette virker, sjekk ut min veiledning om koding av bot.)

La oss bare endre dette slik at hvis et tegn når noden på x- eller y-aksen, forblir disse verdiene sant så lenge vi ikke går videre til neste knutepunkt.

For å gjøre dette mulig, må vi erklære reachedX og reachedY som klassemedlemmer:

offentlig bool mReachedNodeX; offentlig bool mReachedNodeY;

Dette betyr at vi ikke lenger trenger å sende dem til GetContext () funksjon:

offentlig tomgang GetContext (ut Vector2 prevDest, ut Vector2 currentDest, ut Vector2 nextDest, out bool destOnGround)

Med disse endringene må vi også nullstille variablene manuelt når vi begynner å bevege oss mot neste knutepunkt. Den første forekomsten er når vi nettopp har funnet banen og kommer til å bevege seg mot den første noden:

hvis (vei! = null && path.Count> 1) for (var i = path.Count - 1; i> = 0; --i) mPath.Add (sti [i]); mCurrentNodeId = 1; mReachedNodeX = false; mReachedNodeY = false;

Den andre er når vi har nådd gjeldende målknutepunkt og ønsker å bevege seg mot det neste:

hvis (mReachedNodeX && mReachedNodeY) int prevNodeId = mCurrentNodeId; mCurrentNodeId ++; mReachedNodeX = false; mReachedNodeY = false;

For å stoppe omberegning av variablene, må vi erstatte følgende linjer:

reachedX = ReachedNodeOnXAxis (pathPosition, prevDest, currentDest); reachedY = ReachedNodeOnYAxis (pathPosition, prevDest, currentDest);

... med disse, som vil oppdage om vi bare har nådd en knute på en akse hvis vi ikke allerede har nådd det:

hvis (! mReachedNodeX) mReachedNodeX = ReachedNodeOnXAxis (pathPosition, prevDest, currentDest); hvis (! mReachedNodeY) mReachedNodeY = ReachedNodeOnYAxis (pathPosition, prevDest, currentDest);

Selvfølgelig må vi også erstatte alle andre forekomster av reachedX og reachedY med de nylig erklærte versjonene mReachedNodeX og mReachedNodeY.

Se hvis tegnet trenger å ta en kant

La oss forklare et par variabler som vi skal bruke for å avgjøre om boten trenger å ta en hylle, og i så fall hvilken:

offentlig bool mGrabsLedges = false; bool mMustGrabLeftLedge; bool mMustGrabRightLedge;

mGrabsLedges er et flagg som vi sender til algoritmen for å fortelle det om det skal finne en sti, inkludert ledgehullet. mMustGrabLeftLedge og mMustGrabRightLedge vil bli brukt til å avgjøre om neste knutepunkt er en grab-skjerm, og om boten skal ta tak i skjermen til venstre eller til høyre.

Det vi vil gjøre nå, er å skape en funksjon som, gitt en knute, vil kunne oppdage om tegnet ved den knutepunktet vil kunne ta tak i en hylle. 

Vi trenger to funksjoner for dette: man vil sjekke om tegnet kan ta en hylle til venstre, og den andre vil sjekke om tegnet kan ta en hylle til høyre. Disse funksjonene fungerer på samme måte som vår banekode for å registrere ledger:

offentlig bool CanGrabLedgeOnLeft (int nodeId) return (mMap.IsObstacle (mPath [nodeId] .x - 1, mPath [nodeId] .y + mHeight - 1) &&! mMap.IsObstacle (mPath [nodeId] .x - 1, mPath [nodeId] .y + mHeight));  offentlig bool CanGrabLedgeOnRight (int nodeId) return (mMap.IsObstacle (mPath [nodeId] .x + mWidth, mPath [nodeId] .y + mHeight - 1) &&! mMap.IsObstacle (mPath [nodeId] .x + mWidth, mPath [nodeId] .y + mHeight)); 

Som du kan se, ser vi om det er en solid flis ved siden av vår karakter med en tom flise over den.

La oss nå gå til GetContext () funksjon, og tilordne de riktige verdiene til mMustGrabRightLedge og mMustGrabLeftLedge. Vi må sette dem til ekte hvis karakteren er ment å ta tak i ledges i det hele tatt (det vil si hvis mGrabsLedges er ekte) og hvis det er en kant å gripe inn på.

mMustGrabLeftLedge = mGrabsLedges &&! destOnGround && CanGrabLedgeOnLeft (mCurrentNodeId); mMustGrabRightLedge = mGrabsLedges &&! destOnGround && CanGrabLedgeOnRight (mCurrentNodeId);

Legg merke til at vi heller ikke vil ta tak i ledges hvis bestemmelsesnoden er på bakken.

Oppdater hoppverdiene

Som du kanskje legger merke til, er karakterens posisjon når du griper en hylle litt annerledes enn sin posisjon når du står rett under den:

Ledge-gripeposisjonen er litt høyere enn stående stilling, selv om disse tegnene okkuperer samme knutepunkt. Dette betyr at å ta tak i en hylle vil kreve et litt høyere hopp enn å bare hoppe på en plattform, og vi må ta hensyn til dette.

La oss se på funksjonen som bestemmer hvor lenge hoppeknappen skal trykkes:

offentlig int GetJumpFramesForNode (int prevNodeId) int currentNodeId = prevNodeId + 1; hvis (mPath [currentNodeId] .y - mPath [prevNodeId] .y> 0 && mOnGround) int jumpHeight = 1; for (int i = currentNodeId; i < mPath.Count; ++i)  if (mPath[i].y - mPath[prevNodeId].y >= jumpHeight) jumpHeight = mPath [i] .y - mPath [prevNodeId] .y; hvis (mPath [i] .y - mPath [prevNodeId] .y < jumpHeight || mMap.IsGround(mPath[i].x, mPath[i].y - 1)) return GetJumpFrameCount(jumpHeight);   return 0; 

Først av alt endrer vi den opprinnelige tilstanden. Boten skal kunne hoppe, ikke bare fra bakken, men også når den tar tak i en kant:

hvis (mPath [currentNodeId] .y - mPath [prevNodeId] .y> 0 && (mOnGround || mCurrentState == CharacterState.GrabLedge))

Nå må vi legge til noen flere rammer hvis det hopper for å hente en kant. Først og fremst må vi vite om det faktisk kan gjøre det, så la oss lage en funksjon som vil fortelle oss om tegnet kan ta en hylle enten til venstre eller til høyre:

offentlig bool CanGrabLedge (int nodeId) return CanGrabLedgeOnLeft (nodeId) || CanGrabLedgeOnRight (Node); 

La oss nå legge til et par rammer til hoppet når boten trenger å ta en kant:

hvis (mPath [i] .y - mPath [prevNodeId] .y> = jumpHeight) jumpHeight = mPath [i] .y - mPath [prevNodeId] .y; hvis (mPath [i] .y - mPath [prevNodeId] .y < jumpHeight || mMap.IsGround(mPath[i].x, mPath[i].y - 1)) return (GetJumpFrameCount(jumpHeight)); else if (grabLedges && CanGrabLedge(i)) return (GetJumpFrameCount(jumpHeight) + 4);

Som du kan se, forlenger vi hoppet ved 4 rammer, som burde gjøre jobben greit i vårt tilfelle.

Men det er enda en ting vi trenger for å endre her, noe som egentlig ikke har mye å gjøre med å få tak i ledge. Det løser et tilfelle når neste knutepunkt er samme høyde som den nåværende, men ikke på bakken, og knuten etter det er høyere opp, noe som betyr at et hopp er nødvendig:

hvis ((mPath [currentNodeId] .y - mPath [prevNodeId] .y> 0 || (mPath [currentNodeId] .y - mPath [prevNodeId] .y == 0 &&! mMap.IsGround (mPath [currentNodeId] .x, mPath [currentNodeId] .y - 1) && mPath [currentNodeId + 1] .y - mPath [prevNodeId] .y> 0)) && (mOnGround || mCurrentState == CharacterState.GrabLedge))

Implementer bevegelseslogikken for å få tak i og slippe av ledger

Vi vil splitte ledgegrapslogikken i to faser: en for når boten fremdeles ikke er nær nok til å skape å ta tak i, så vi vil bare fortsette bevegelsen som vanlig, og en for når gutten trygt kan starte beveger seg mot det for å ta det.

La oss begynne med å erklære en boolsk som vil indikere om vi allerede har flyttet til den andre fasen. Vi nevner det mCanGrabLedge:

offentlig bool mGrabsLedges = false; bool mMustGrabLeftLedge; bool mMustGrabRightLedge; bool mCanGrabLedge = false; 

Nå må vi definere forhold som lar karakteren flytte til den andre fasen. Disse er ganske enkle:

  • Boten har allerede nådd målkoden på X-aksen.
  • Boten må ta tak i venstre eller høyre kant.
  • Hvis botmen beveger seg mot skråningen, vil den støte på en vegg i stedet for å gå videre.

Ok, de to første betingelsene er veldig enkle å sjekke nå fordi vi har gjort alt arbeidet som allerede er nødvendig:

hvis (! mCanGrabLedge && mReachedNodeX && (mMustGrabLeftLedge || mMustGrabRightLedge))  annet hvis (mReachedNodeX && mReachedNodeY)

Nå, den tredje betingelsen vi kan skille i to deler. Den første vil ta vare på situasjonen der tegnet beveger seg mot skråningen fra bunnen og den andre fra toppen. Vilkårene vi vil sette for første sak er:

  • Botens nåværende posisjon er lavere enn målposisjonen (den nærmer seg fra bunnen).
  • Øverst på tegnets grenseboks er høyere enn kanthøydehøyde.
(pathPosition.y < currentDest.y && (currentDest.y + Map.cTileSize*mHeight) < pathPosition.y + mAABB.HalfSizeY * 2)

Hvis boten nærmer seg fra toppen, er forholdene som følger:

  • Botens nåværende posisjon er høyere enn målposisjonen (den nærmer seg fra toppen).
  • Forskjellen mellom tegns posisjon og målposisjon er mindre enn tegnets høyde.
(pathPosition.y> currentDest.y && pathPosition.y - currentDest.y < mHeight * Map.cTileSize)

La oss nå kombinere alle disse og sette flagget som indikerer at vi trygt kan bevege oss mot en hylle:

 ellers hvis (! mCanGrabLedge && mReachedNodeX && (mMustGrabLeftLedge || mMustGrabRightLedge) && ((pathPosition.y < currentDest.y && (currentDest.y + Map.cTileSize*mHeight) < pathPosition.y + mAABB.HalfSizeY * 2) || (pathPosition.y > currentDest.y && pathPosition.y - currentDest.y < mHeight * Map.cTileSize)))  mCanGrabLedge = true; 

Det er en ting vi vil gjøre her, og det er å umiddelbart begynne å bevege seg mot skjeden:

hvis (! mCanGrabLedge && mReachedNodeX && (mMustGrabLeftLedge || mMustGrabRightLedge) && ((pathPosition.y < currentDest.y && (currentDest.y + Map.cTileSize*mHeight) < pathPosition.y + mAABB.HalfSizeY * 2) || (pathPosition.y > currentDest.y && pathPosition.y - currentDest.y < mHeight * Map.cTileSize)))  mCanGrabLedge = true; if (mMustGrabLeftLedge) mInputs[(int)KeyInput.GoLeft] = true; else if (mMustGrabRightLedge) mInputs[(int)KeyInput.GoRight] = true; 

OK, nå før denne store tilstanden la oss lage en mindre en. Dette vil i utgangspunktet være en forenklet versjon for bevegelsen når boten er i ferd med å ta tak i en hylle:

hvis (mCanGrabLedge && mCurrentState! = CharacterState.GrabLedge) hvis (mMustGrabLeftLedge) mInputs [(int) KeyInput.GoLeft] = true; ellers hvis (mMustGrabRightLedge) mInputs [(int) KeyInput.GoRight] = true;  annet hvis (! mCanGrabLedge && mReachedNodeX && (mMustGrabLeftLedge || mMustGrabRightLedge) &&

Det er hovedlogikken bak hylsen, men det er fortsatt et par ting å gjøre. 

Vi må redigere tilstanden der vi sjekker om det er OK å flytte til neste knutepunkt. For tiden ser tilstanden ut slik:

ellers hvis (mReachedNodeX && mReachedNodeY)

Nå må vi også flytte til neste knutepunkt hvis botten var klar til å ta tak i slekten og da gjorde det faktisk:

ellers hvis ((mReachedNodeX && mReachedNodeY) || (mCanGrabLedge && mCurrentState == CharacterState.GrabLedge))

Håndter Hopping og Droping fra Ledge

Når boten er på skjermen, skal den kunne hoppe som vanlig, så la oss legge til en ekstra betingelse for hoppingsrutinen:

hvis (mFramesOfJumping> 0 && (mCurrentState == CharacterState.GrabLedge ||! mOnGround || (mReachedNodeX &&! destOnGround) || (mOnGround && destOnGround))) mInputs [(int) KeyInput.Jump] = true; hvis (! mOnGround) --mFramesOfJumping; 

Den neste tingen som boten må kunne gjøre, er grasiøst tømt av skjermen. Med den nåværende implementeringen er det veldig enkelt: hvis vi tar en hylle og vi ikke hopper, så må vi selvsagt slippe av det!

hvis (mCurrentState == Character.CharacterState.GrabLedge && mFramesOfJumping <= 0)  mInputs[(int)KeyInput.GoDown] = true; 

Det er det! Nå er karakteren veldig greit å forlate ledgepostposisjonen, uansett om den trenger å hoppe opp eller bare slippe ned.

Stopp grabende ledges hele tiden!

I øyeblikket griper boten hver eneste kant det kan, uansett om det er fornuftig å gjøre det. 

En løsning på dette er å tilordne en stor heuristisk kostnad til skjeden, så algoritmen prioriterer å bruke dem hvis den ikke trenger - men dette ville kreve at boten vår får litt mer informasjon om noderne. Siden alt vi passerer til bot er en liste over poeng, vet vi ikke om algoritmen mente en bestemt knutepunkt for å bli ledge fanget eller ikke; bot antar at hvis en hylle kan bli fanget, burde den sikkert! 

Vi kan implementere en rask løsning for denne oppførselen: vi vil ringe til pathfinding-funksjonen to ganger. Første gang vi kaller det med useLedges parameter satt til falsk, og den andre gangen med den satt til ekte.

La oss tilordne den første banen som banen funnet uten å bruke noen hyllegrep:

Liste sti1 = null; var path = mMap.mPathFinder.FindPath (startTile, destinasjon, Mathf.CeilToInt (mAABB.HalfSizeX / 8.0f), Mathf.CeilToInt (mAABB.HalfSizeY / 8,0f), (kort) mMaxJumpHeight, false);

Nå, hvis dette sti er ikke null, må vi kopiere resultatene til vår STI1 liste, fordi når vi kaller pathfinder andre gang, resultatet i sti vil bli overskrevet.

hvis (sti! = null) path1 = ny liste(); path1.AddRange (bane); 

La oss nå ringe veibeskriveren igjen, denne gangen gjør det mulig å ta tak i ledgen:

var path2 = mMap.mPathFinder.FindPath (startTile, destinasjon, Mathf.CeilToInt (mAABB.HalfSizeX / 8.0f), Mathf.CeilToInt (mAABB.HalfSizeY / 8.0f), (kort) mMaxJumpHeight, true);

Vi antar at vår siste bane kommer til å være banen med hyllegrep:

bane = bane2; mGrabsLedges = true;

Og rett etter dette, la oss verifisere vår antagelse. Hvis vi har funnet en sti uten ledge griper, og den banen er ikke mye lenger enn stien som bruker dem, så får vi botten til å slå av kantene.

hvis (sti1! = null && path1.Count <= path2.Count + 6)  path = path1; mGrabsLedges = false; 

Vær oppmerksom på at vi måler lengden på banen i nodetall, noe som kan være ganske unøyaktig på grunn av nodens filtreringsprosess. Det ville være mye mer nøyaktig å beregne, for eksempel Manhattan-lengden på stien (| x1 - x2 | + | y1 - y2 | av hver knutepunkt), men siden denne hele metoden er mer en hack enn en ekte løsning, er det greit å bruke denne typen heuristiske her.

Resten av funksjonen følger som den var; banen er kopiert til bot-instansens buffer og den begynner å følge den.

Sammendrag

Det er alt for opplæringen! Som du ser, er det ikke så vanskelig å utvide algoritmen for å legge til flere bevegelsesmuligheter, men gjør det definitivt øker kompleksiteten og legger til noen plagsomme problemer. 

Igjen kan mangel på nøyaktighet bite oss her mer enn en gang, spesielt når det gjelder den fallende bevegelsen - dette er området som trenger mest forbedring, men jeg har forsøkt å gjøre algoritmen til å passe til fysikken så vel som jeg kan med gjeldende sett med verdier. 

Alt i alt kan boten krysse et nivå på en måte som ville konkurrere med mange spillere, og jeg er veldig fornøyd med det resultatet!