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.
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.
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.
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:
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;
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..
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 lijstFindPath (Vector2i start, Vector2i end, int characterWidth, int characterHeight, short maxCharacterJumpHeight, bool useLedges)
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:
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.
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.
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:
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.
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.
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;
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.
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.
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
.
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.
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))
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:
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:
(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:
(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))
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.
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:
Lijstpath1 = null; var path = mMap.mPathFi