A * Pathfinding voor 2D op grid gebaseerde platformgames Ledge Grabbing

In dit deel van onze serie over het aanpassen van het A * -padvervalsalgoritme aan platformers, introduceren we een nieuwe monteur bij het personage: richel grijpen. We zullen ook de nodige wijzigingen aanbrengen in zowel het pathfinding-algoritme als de bot-AI, zodat ze gebruik kunnen maken van de verbeterde mobiliteit.

demonstratie

U kunt de Unity-demo of de WebGL-versie (16 MB) spelen om het uiteindelijke resultaat in actie te zien. Gebruik WASD om het personage te verplaatsen, links klikken op een plek om een ​​pad te vinden dat je kunt volgen om er te komen, klik met de rechtermuisknop een cel om de grond op dat punt te wisselen, middelste muisknop om een ​​eenrichtingsplatform te plaatsen, en Klik en sleep de schuifregelaars om hun waarden te veranderen.

Ledge Grabbing Mechanics

Besturing Overzicht

Laten we eerst eens kijken hoe de richelmagneet werkt in de demo om inzicht te krijgen in hoe we ons pathfinding-algoritme moeten veranderen om rekening te houden met deze nieuwe monteur.

De knoppen voor richel grijpen zijn vrij simpel: als het personage vlak naast een richel staat terwijl je valt, en de speler op de linker of rechter richtingstoets drukt om ze naar die richel te verplaatsen, dan zal het personage op de juiste positie grijpen de richel.

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. Uitvallen doe je door op de neer-knop te drukken (S), of de directionele keyn die van de rand af wijst.

De besturing implementeren

Laten we eens kijken hoe de richelgreepbedieningen werken in de code. Het eerste wat hier te doen is, is detecteren of de richel zich links of rechts van het personage bevindt:

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. Zoals je kunt zien, moet de speler het volgende doen om naar beneden te gaan:

  • 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. Dat is wat het volgende fragment doet:

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; 

Hierna veranderen we de status van het personage in Springen, die de sprongfysica zal verwerken:

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; 

Als het personage niet van de rand is gevallen, controleren we tot slot of de spring-toets is ingedrukt; als dat zo is, stellen we de verticale snelheid van de sprong in en wijzigen we de staat:

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; 

Een richelpunt detecteren

Laten we eens kijken naar hoe we bepalen of een richel kan worden gepakt. We gebruiken een paar hotspots rond de rand van het personage:

De gele contour geeft de grenzen van het personage weer. De rode segmenten vertegenwoordigen de wandsensoren; deze worden gebruikt om de karakterfysica aan te pakken. De blauwe segmenten geven aan waar ons personage een richel kan pakken.

Om te bepalen of het personage een richel kan pakken, controleert onze code voortdurend de kant waarnaar het beweegt. Het zoekt naar een lege tegel aan de bovenkant van het blauwe segment, en dan een stevige tegel eronder waar het personage zich op kan vastgrijpen. 

Opmerking: richel grijpen wordt geblokkeerd als het personage omhoog springt. Dit kan gemakkelijk worden opgemerkt in de demo en in de animatie in het gedeelte Besturingsoverzicht.

Het grootste probleem met deze methode is dat als ons personage op hoge snelheid valt, je gemakkelijk een venster mist waarin het een richel kan grijpen. We kunnen dit oplossen door alle tegels op te zoeken vanaf de positie van het vorige frame tot de huidige frames op zoek naar een lege tegel boven een solide. Als een dergelijke tegel wordt gevonden, kan deze worden gepakt.

Nu hebben we duidelijk gemaakt hoe de richel grijpende monteur werkt, laten we eens kijken hoe we het in ons pathfinding-algoritme kunnen integreren..

Pathfinder-wijzigingen

Maak het mogelijk om Ledge Grabbing in en uit te schakelen

Laten we eerst een nieuwe parameter aan onze toevoegen FindPath functie die aangeeft of de pathfinder overwegen richels moet grijpen. We zullen het een naam geven useLedges:

openbare lijst FindPath (Vector2i start, Vector2i end, int characterWidth, int characterHeight, short maxCharacterJumpHeight, bool useLedges)

Detectie Ledge Grab Nodes

Voorwaarden

Nu moeten we de functie aanpassen om te detecteren of een bepaald knooppunt kan worden gebruikt voor richel grijpen. We kunnen dat doen nadat we hebben gecontroleerd of het knooppunt een "on ground" -knooppunt of een "at ceiling" -knooppunt is, omdat het in beide gevallen niet kan worden gebruikt voor richelklemmen.

if (onGround) newJumpLength = 0; else if (atCeiling) if (mNewLocationX! = mLocationX) newJumpLength = (kort) Mathf.Max (maxCharacterJumpHeight * 2 + 1, jumpLength + 1); else newJumpLength = (kort) Mathf.Max (maxCharacterJumpHeight * 2, jumpLength + 2);  else if (/ * controleer of hier een richelknooppunt is * /)  else if (mNewLocationY < mLocationY) 

Oké: nu moeten we uitzoeken wanneer een knoop moet worden beschouwd als een richelknooppunt. Voor cliarity, hier is een diagram dat enkele voorbeeldrichel-grijpposities laat zien:

... en hier is hoe deze er in de game uit kunnen zien:

De sprites van het bovenste personage worden uitgerekt om te laten zien hoe dit eruit ziet met tekens van verschillende grootten.

De rode cellen vertegenwoordigen de gecontroleerde knooppunten; samen met de groene cellen vertegenwoordigen ze het karakter in ons algoritme. De bovenste twee situaties laten een riching van 2x2 tekens respectievelijk links en rechts zien. De onderste twee tonen hetzelfde, maar de grootte van het personage is hier 1x3 in plaats van 2x2.

Zoals je ziet, zou het vrij eenvoudig moeten zijn om deze gevallen in het algoritme te detecteren. De voorwaarden voor het richelknooppunt van de richel zijn als volgt:

  1. Er is een effen tegel naast de tekentegel linksboven / links bovenaan.
  2. Er is een lege tegel boven de gevonden massieve tegel.
  3. Er is geen massieve tegel onder het personage (je hoeft geen richels te pakken als je op de grond staat).

Merk op dat de derde voorwaarde al in acht genomen is, omdat we alleen naar het richelknooppunt van de richel kijken als het karakter niet op de grond ligt.

Laten we eerst eens kijken of we richelgrijpers daadwerkelijk willen detecteren:

else if (useLedges)

Laten we nu eens kijken of er rechts van het teken met rechtsboven teken een tegel staat:

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

En dan, als boven die tegel is er een lege ruimte:

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

Nu moeten we hetzelfde doen voor de linkerkant:

else if (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)))

Er is nog een ding dat we optioneel kunnen doen, dat is het uitschakelen van het vinden van de richelklikknoppen als de valsnelheid te hoog is, dus het pad keert niet terug naar extreme richel grijpposities die moeilijk te volgen zijn door de bot:

else if (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)))  

Na dit alles kunnen we er zeker van zijn dat het gevonden knooppunt een richelknooppunt is.

Een speciaal knooppunt toevoegen

Wat doen we als we een richelknooppunt vinden? We moeten de sprongwaarde instellen. 

Houd er rekening mee dat de sprongwaarde het getal is dat aangeeft in welke fase van de sprong het personage zich zou bevinden als deze deze cel zou bereiken. Als je een samenvatting nodig hebt over hoe het algoritme werkt, kijk dan nog eens naar het theorieartikel.

Het lijkt erop dat we alleen de sprongwaarde van het knooppunt hoeven in te stellen 0, want vanaf het richelpunt van de richel kan het personage effectief een sprong resetten, alsof het op de grond ligt, maar er zijn een paar punten om hier te overwegen. 

  • Ten eerste zou het leuk zijn als we in één oogopslag konden zien of het knooppunt een richelknooppunt is of niet: dit zal enorm nuttig zijn bij het maken van een botgedrag en ook bij het filteren van de knooppunten. 
  • Ten tweede kan meestal van de grond springen worden uitgevoerd vanaf welk punt dan ook het meest geschikt is op een bepaalde tegel, maar bij het springen van een richel grijpt het personage naar een bepaalde positie en kan niets anders doen dan beginnen te vallen of naar boven springen.

Gezien deze restricties, voegen we een speciale sprongwaarde toe voor de richelklikknooppunten. Het maakt niet echt uit wat deze waarde is, maar het is een goed idee om het negatief te maken, omdat dat onze kansen op een verkeerde interpretatie van het knooppunt zal verminderen.

const short cLedgeGrabJumpValue = -9;

Laten we deze waarde nu toewijzen wanneer we een richelknoop op richel detecteren:

else if (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; 

maken cLedgeGrabJumpValue Negatief zal een effect hebben op de berekening van de knooppuntkosten - het algoritme zal er de voorkeur aan geven richels te gebruiken in plaats van deze over te slaan. Er zijn twee dingen om op te merken:

  1. Ledge-grijppunten bieden een grotere bewegingsmogelijkheid dan andere knooppunten in de lucht, omdat het personage weer kan springen door ze te gebruiken; vanuit dit oogpunt is het een goede zaak dat deze knooppunten goedkoper zijn dan andere. 
  2. Grijpen van te veel richels leidt vaak tot onnatuurlijke bewegingen, omdat spelers meestal geen gebruik maken van richelgrepen tenzij ze nodig zijn om ergens te bereiken.

In de bovenstaande animatie kunt u het verschil zien tussen omhoog gaan wanneer richels de voorkeur hebben en wanneer dat niet het geval is.

Voorlopig laten we de kostenberekening zoals deze is, maar het is vrij eenvoudig om deze te wijzigen om richelpunten duurder te maken.

Wijzig de sprongwaarde bij het springen of laten vallen van een richel

Nu moeten we de sprongwaarden aanpassen voor de knooppunten die starten vanaf het richelpunt van de richel. We moeten dit doen omdat springen vanuit een richelpositie van een richel nogal anders is dan van een grond springen. Er is heel weinig vrijheid wanneer je van een richel springt, omdat het personage op een bepaald punt is gefixeerd. 

Op de grond kan het personage zich naar links of rechts vrij bewegen en op het meest geschikte moment springen.

Laten we eerst de zaak instellen wanneer het personage uit een richelklem naar beneden valt:

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

Zoals je kunt zien, is de nieuwe spronglengte iets groter als het personage uit een richel valt: op deze manier compenseren we het gebrek aan manoeuvreerbaarheid terwijl je een richel vastpakt, wat zal resulteren in een hogere verticale snelheid voordat de speler andere knooppunten kan bereiken.

Het volgende is het geval waarbij het personage naar de zijkant valt van het grijpen van een richel:

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

Het enige wat we moeten doen is de sprongwaarde op de valwaarde instellen.

Negeer meer knooppunten

We moeten een aantal extra voorwaarden toevoegen voor wanneer we nodes moeten negeren. 

Ten eerste, als we van een richelpositie springen, moeten we naar boven gaan, niet naar de zijkant. Dit werkt op dezelfde manier als gewoon uit de grond springen. De verticale snelheid is op dit punt veel hoger dan de mogelijke horizontale snelheid, en we moeten dit feit in het algoritme modelleren:

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

Als we op de volgende manier van de richel naar de andere kant willen laten vallen:

Vervolgens moeten we de voorwaarde bewerken die geen horizontale verplaatsing toestaat wanneer de sprongwaarde oneven is. Dat komt omdat momenteel onze speciale ledge grijpwaarde gelijk is aan -9, het is dus alleen passend om alle negatieve getallen van deze voorwaarde uit te sluiten.

if (jumpLength> = 0 && jumpLength% 2! = 0 && mLocationX! = mNewLocationX) doorgaan;

Werk het knooppuntfilter bij

Laten we ten slotte verder gaan met het filteren van knooppunten. Het enige wat we hier moeten doen is een voorwaarde toevoegen voor richelklemmen, zodat we ze niet filteren. We hoeven alleen maar te controleren of de sprongwaarde van het knooppunt gelijk is aan cLedgeGrabJumpValue:

|| (fNodeTmp.JumpLength == cLedgeGrabJumpValue)

Het hele filter ziet er nu als volgt uit:

if ((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 sprongen start || (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength! = 0) // markeer landingen || (fNode.y> mSluit [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);

Dat is het - dit zijn alle veranderingen die we moesten aanbrengen om het pathfinding-algoritme bij te werken.

Bot verandert

Nu ons pad de plekken toont waar een personage een richel kan pakken, laten we het gedrag van de bot aanpassen zodat het gebruikmaakt van deze gegevens.

Stop Herberekenen bereikteX en bereiktY

Allereerst, om dingen duidelijker te maken in de bot, laten we het bijwerken GetContext () functie. Het huidige probleem daarmee is dat reachedX en reachedY waarden worden voortdurend opnieuw berekend, waardoor informatie over de context wordt verwijderd. Deze waarden worden gebruikt om te zien of de bot het doelknooppunt al op de x- en y-assen heeft bereikt. (Als je een opfrissing nodig hebt over hoe dit werkt, bekijk dan mijn tutorial over het coderen van de bot.)

Laten we dit eenvoudig veranderen, zodat als een teken het knooppunt op de x- of y-as bereikt, deze waarden waar blijven zolang we niet naar het volgende knooppunt gaan.

Om dit mogelijk te maken, moeten we aangifte doen reachedX en reachedY als klasleden:

public bool mReachedNodeX; public bool mReachedNodeY;

Dit betekent dat we ze niet langer hoeven door te geven aan de GetContext () functie:

openbare void GetContext (out Vector2 prevDest, out Vector2 currentDest, out Vector2 nextDest, out bool destOnGround)

Met deze wijzigingen moeten we de variabelen ook handmatig opnieuw instellen wanneer we naar het volgende knooppunt gaan. De eerste keer dat we het pad hebben gevonden, gaan we naar het eerste knooppunt:

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

De tweede is wanneer we het huidige doelknooppunt hebben bereikt en naar de volgende willen gaan:

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

Om de herberekening van de variabelen te stoppen, moeten we de volgende regels vervangen:

reachX = ReachedNodeOnXAxis (pathPosition, prevDest, currentDest); reachY = ReachedNodeOnYAxis (pathPosition, prevDest, currentDest);

... hiermee, die alleen detecteert of we een knooppunt op een as hebben bereikt als we dit nog niet hebben bereikt:

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

Natuurlijk moeten we ook elk ander voorkomen van reachedX en reachedY met de nieuw gedeclareerde versies mReachedNodeX en mReachedNodeY.

Zie Als het personage een richel moet pakken

Laten we een aantal variabelen declareren die we zullen gebruiken om te bepalen of de bot een richel moet pakken en, zo ja, welke:

public bool mGrabsLedges = false; bool mMustGrabLeftLedge; bool mMustGrabRightLedge;

mGrabsLedges is een vlag die we doorgeven aan het algoritme om het te laten weten of het een pad moet vinden met inbegrip van de richelgrepen. mMustGrabLeftLedge en mMustGrabRightLedge wordt gebruikt om te bepalen of het volgende knooppunt een grijper is en of de bot de rand naar links of rechts moet grijpen.

Wat we nu willen doen is een functie maken die, gegeven een knooppunt, zal kunnen detecteren of het personage op dat knooppunt een richel kan pakken. 

Hiervoor hebben we twee functies nodig: één zal controleren of het personage een richel aan de linkerkant kan pakken en de ander zal controleren of het personage een richel rechts kan pakken. Deze functies werken op dezelfde manier als onze code voor het opsporen van richels:

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

Zoals u kunt zien, controleren we of er naast ons personage een massieve tegel staat met een lege tegel erboven.

Laten we nu naar de GetContext () functie en wijs de juiste waarden toe aan mMustGrabRightLedge en mMustGrabLeftLedge. We moeten ze instellen waar als het personage verondersteld wordt richels te pakken (dat wil zeggen, als mGrabsLedges is waar) en of er een richel is om vast te grijpen.

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

Merk op dat we ook geen richels willen pakken als het bestemmingsknooppunt op de grond ligt.

Werk de sprongwaarden bij

Zoals je misschien opmerkt is de positie van het personage bij het pakken van een richel enigszins anders dan zijn positie wanneer je er vlak onder staat:

De richel grijppositie is iets hoger dan de staande positie, hoewel deze tekens hetzelfde knooppunt innemen. Dit betekent dat voor het grijpen van een richel een iets hogere sprong nodig is dan alleen springen op een platform, en daar moeten we rekening mee houden.

Laten we eens kijken naar de functie die bepaalt hoe lang de springknop moet worden ingedrukt:

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

Allereerst zullen we de beginvoorwaarde wijzigen. De bot moet in staat zijn om te springen, niet alleen van de grond, maar ook wanneer hij een richel vastpakt:

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

Nu moeten we nog enkele frames toevoegen als het springt om een ​​richel te pakken. Allereerst moeten we weten of het dat ook daadwerkelijk kan doen, dus laten we een functie maken die ons vertelt of het personage een richel kan pakken, naar links of rechts:

public bool CanGrabLedge (int nodeId) return CanGrabLedgeOnLeft (nodeId) || CanGrabLedgeOnRight (NodeID); 

Laten we nu een paar frames toevoegen aan de sprong wanneer de bot een rand moet pakken:

if (mPath [i] .y - mPath [prevNodeId] .y> = jumpHeight) jumpHeight = mPath [i] .y - mPath [prevNodeId] .y; if (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);

Zoals je kunt zien, verlengen we de sprong 4 frames, die in ons geval het werk goed moeten doen.

Maar er is nog een ding dat we hier moeten veranderen, wat niet echt veel te maken heeft met richel grijpen. Het herstelt een geval wanneer het volgende knooppunt dezelfde hoogte heeft als het huidige knooppunt, maar niet op de grond ligt, en het knooppunt daarna hoger is, wat betekent dat een sprong nodig is:

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

Implementeer de bewegingslogica voor het grijpen en loslaten van richels

We willen de richellogica op de richel splitsen in twee fasen: één voor wanneer de bot nog steeds niet dichtbij genoeg is om de richel te grijpen, dus we willen gewoon doorgaan met bewegen zoals gewoonlijk, en één voor wanneer de jongen veilig kan beginnen er naartoe gaan om het te grijpen.

Laten we beginnen met het declareren van een Boolean die aangeeft of we al naar de tweede fase zijn verhuisd. We zullen het een naam geven mCanGrabLedge:

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

Nu moeten we condities definiëren die het personage naar de tweede fase laten gaan. Deze zijn vrij eenvoudig:

  • De bot heeft het doelknooppunt op de X-as al bereikt.
  • De bot moet de linker of rechter richel grijpen.
  • Als de bot naar de richel toe beweegt, botst hij tegen een muur in plaats van verder te gaan.

Oké, de eerste twee voorwaarden zijn nu heel eenvoudig te controleren omdat we al het nodige werk al hebben gedaan:

if (! mCanGrabLedge && mReachedNodeX && (mMustGrabLeftLedge || mMustGrabRightLedge))  else if (mReachedNodeX && mReachedNodeY)

Nu, de derde voorwaarde kunnen we in twee delen scheiden. De eerste zorgt voor de situatie waarin het personage van de bodem naar de richel toe beweegt en de tweede vanaf de bovenkant. De voorwaarden die we willen stellen voor de eerste zaak zijn:

  • De huidige positie van de bot is lager dan de doelpositie (deze nadert vanaf de onderkant).
  • De begrenzende kader van het personage is hoger dan de hoogte van de richelpan.
(pathPosition.y < currentDest.y && (currentDest.y + Map.cTileSize*mHeight) < pathPosition.y + mAABB.HalfSizeY * 2)

Als de bot van bovenaf nadert, zijn de voorwaarden als volgt:

  • De huidige positie van de bot is hoger dan de doelpositie (deze nadert van bovenaf).
  • Het verschil tussen de positie van het personage en de doelpositie is kleiner dan de lengte van het personage.
(pathPosition.y> currentDest.y && pathPosition.y - currentDest.y < mHeight * Map.cTileSize)

Laten we nu al deze combineren en de vlag instellen die aangeeft dat we veilig naar een richel kunnen gaan:

 else if (! 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; 

Er is nog een ding dat we hier willen doen, en dat is om meteen naar de richel te gaan:

if (! 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, nu voor deze enorme voorwaarde, laten we een kleinere creëren. Dit zal in principe een vereenvoudigde versie zijn voor de beweging wanneer de bot op het punt staat een richel te pakken:

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

Dat is de hoofdlogica achter het grijpen van richel, maar er zijn nog een paar dingen te doen. 

We moeten de voorwaarde bewerken waarin we controleren of het OK is om naar het volgende knooppunt te gaan. Momenteel ziet de conditie er als volgt uit:

else if (mReachedNodeX && mReachedNodeY)

Nu moeten we ook naar het volgende knooppunt gaan als de bot klaar was om de richel te grijpen en dat toen ook deed:

else if ((mReachedNodeX && mReachedNodeY) || (mCanGrabLedge && mCurrentState == CharacterState.GrabLedge))

Omgaan met springen en laten vallen van de richel

Zodra de bot op de richel staat, zou hij normaal moeten kunnen springen, dus laten we een extra voorwaarde toevoegen aan de springroutine:

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

Het volgende dat de bot moet kunnen doen is sierlijk van de richel vallen. Met de huidige implementatie is het heel simpel: als we een richel vastgrijpen en we niet springen, dan moeten we er duidelijk vanaf gaan!

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

Dat is het! Nu kan het personage heel soepel de richelpositie van de richel verlaten, ongeacht of het omhoog moet springen of gewoon naar beneden moet vallen.

Stop met het achtervolgen van richels de hele tijd!

Op dit moment pakt de bot elke richel die het kan, ongeacht of het zinvol is om dit te doen. 

Een oplossing hiervoor is om een ​​grote heuristische kost toe te kennen aan de richelgrijpers, zodat het algoritme prioriteit geeft aan het gebruik ervan als dat niet nodig is, maar dit vereist dat onze bot een beetje meer informatie over de knooppunten heeft. Aangezien alles wat we doorgeven aan de bot een lijst met punten is, weten we niet of het algoritme een bepaald knooppunt betekende om richel te grijpen of niet; de bot gaat ervan uit dat als een richel kan worden gepakt, dit toch wel zou moeten! 

We kunnen een snelle oplossing voor dit gedrag implementeren: we zullen de pathfinding-functie noemen tweemaal. De eerste keer dat we het zullen noemen met de useLedges parameter ingesteld op vals, en de tweede keer dat ermee wordt ingesteld waar.

Laten we het eerste pad toewijzen als het gevonden pad zonder richelgrepen te gebruiken:

Lijst path1 = null; var path = mMap.mPathFi