A * Pathfinding pentru Platforme 2D Grid-Based Grabbing Ledge

În această parte a seriei noastre privind adaptarea algoritmului A * pathfinding la platformeri, vom introduce un mecanic nou pentru caracterul: grabbing. Vom face, de asemenea, modificări corespunzătoare atât algoritmului de căutare a traseului, cât și botului AI, astfel încât să poată utiliza mobilitatea îmbunătățită.

Demo

Puteți reda demonstrația Unity sau versiunea WebGL (16MB) pentru a vedea rezultatul final în acțiune. Utilizare WASD pentru a muta caracterul, stânga-clic pe un loc pentru a găsi o cale pe care să o poți urma pentru a ajunge acolo, Click dreapta o celulă pentru a comuta la sol în acel punct, -Clic mijloc a plasa o platformă unidirecțională și apasa si trage glisoarele să-și schimbe valorile.

Mecanica de prindere a ghearelor

Controale generale

Să aruncăm o privire mai întâi la modul în care mecanismul de preluare a corzilor funcționează în demo pentru a obține o anumită perspectivă asupra modului în care ar trebui să ne schimbăm algoritmul de urmărire pentru a lua în considerare acest mecanic nou.

Comenzile pentru grabbing-ul sunt simplă: dacă personajul se află chiar lângă o muchie în timp ce se încadrează și jucătorul apasă tasta direcțională stânga sau dreapta pentru a le deplasa spre marginea respectivă, atunci când caracterul este în poziția corectă, el va apuca pervazul.

Odată ce personajul apucă o margine, jucătorul are două opțiuni: poate să sară sau să coboare. Jumping funcționează normal; jucătorul apasă tasta de salt, iar forța saltului este identică cu forța aplicată la sărituri de la sol. Scăderea se face prin apăsarea butonului în jos (S) sau cheia direcțională care se îndreaptă spre margine.

Implementarea controalelor

Să trecem peste modul în care controalele de apucare a coroanei funcționează în cod. Primul lucru pe care trebuie să-l faceți este să aflați dacă marginea este la stânga sau la dreapta personajului:

bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft;

Putem folosi acele informații pentru a determina dacă caracterul ar trebui să scadă de pe margine. După cum vedeți, pentru a coborî, jucătorul trebuie să:

  • apăsați butonul în jos,
  • apăsați butonul din stânga atunci când luăm o pervaz pe dreapta, sau
  • apăsați butonul din dreapta atunci când luăm o margine în stânga.
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))  

Există o mică avertizare aici. Luați în considerare o situație când țineți butonul de jos și butonul drept, când personajul se află pe o muchie în dreapta. Va duce la următoarea situație:

Problema aici este că personajul apucă marginea imediat după ce îl lasă. 

O soluție simplă la aceasta este de a bloca mișcarea spre marginea unui cuplu de cadre după ce am scăpat de pe margine. Asta face următorul fragment:

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; 

După aceasta, schimbăm starea personajului A sari, care se va ocupa de fizica saltului:

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; 

În cele din urmă, dacă personajul nu a căzut de pe marginea chenarului, verificăm dacă tasta de salt a fost apăsată; dacă este așa, am setat viteza verticală a saltului și schimbăm starea:

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; 

Detectarea unui punct de prindere a cadrului

Să ne uităm la modul în care determinăm dacă se poate apuca o margine. Folosim câteva hotspoturi în jurul marginea personajului:

Conturul galben reprezintă limitele personajului. Segmentele roșii reprezintă senzorii de perete; acestea sunt folosite pentru a trata fizica caracterului. Segmentele albastre reprezintă locul în care personajul nostru poate lua o muchie.

Pentru a determina dacă personajul poate apuca o muchie, codul nostru verifică în mod constant partea în care se îndreaptă. Căutând o placă goală în partea superioară a segmentului albastru și apoi o piesă solidă sub ea, pe care personajul o poate apuca. 

Notă: Grabbing-ul este blocat dacă caracterul este sărit în sus. Acest lucru se poate observa cu ușurință în demo și în animație din secțiunea Privire de ansamblu a controalelor.

Principala problemă cu această metodă este că, dacă caracterul nostru cade la o viteză mare, este ușor să pierdeți o fereastră în care să poată apuca o muchie. Putem rezolva acest lucru prin căutarea tuturor plăcilor începând de la poziția anterioară a cadrului la cadrele actuale în căutare de orice țiglă goală deasupra unei plăci solide. Dacă se găsește o astfel de țiglă, atunci ea poate fi apucată.

Acum am clarificat modul în care funcționează mecanismul de prindere a muchiilor, să vedem cum să-l încorporăm în algoritmul nostru de căutare.

Modificările Pathfinder

Asigurați-vă că puteți activa sau dezactiva Ledge Grabbing

Mai întâi de toate, să adăugăm un nou parametru la adresa noastră FindPath funcția care indică dacă calea de urmărire ar trebui să ia în considerare apucarea coroanelor. O vom numi useLedges:

Lista publică FindPath (startul Vector2i, sfârșitul Vector2i, int caracterWidth, int characterHeight, maxCharacterJumpHeight scurt, bool useLedges)

Detectați nodurile de apucare a raftului

Condiții

Acum, trebuie să modificăm funcția pentru a detecta dacă un anumit nod poate fi folosit pentru capturarea în pervaz. Putem face acest lucru după ce am verificat dacă nodul este un nod "la sol" sau un nod "la tavan", deoarece în nici un caz nu poate fi utilizat pentru capturarea muchiilor.

dacă (onGround) newJumpLength = 0; altfel dacă (atCeiling) if (mNewLocationX! = locațiaX) newJumpLength = (scurt) Mathf.Max (maxCharacterJumpHeight * 2 + 1, jumpLength + 1); altfel newJumpLength = (scurt) Mathf.Max (maxCharacterJumpHeight * 2, jumpLength + 2);  altfel dacă (/ * verificați dacă există un nod care se prăbușește aici * /)  altceva dacă (mNewLocationY < mLocationY) 

În regulă: acum trebuie să ne dăm seama când un nod ar trebui să fie considerat un nod de apucare a bolțurilor. Pentru clienți, iată o diagramă care arată câteva exemple de poziții ale capcanelor:

... și iată cum s-ar putea ca acestea să pară în joc:

Spritele de caractere de sus sunt întinse pentru a arăta cum arată acest lucru cu caractere de dimensiuni diferite.

Celulele roșii reprezintă nodurile verificate; împreună cu celulele verzi, ele reprezintă caracterul din algoritmul nostru. Primele două situații arată o margine de apucare a caracterelor de 2x2 pe stânga și respectiv dreapta. În partea de jos două arată același lucru, dar dimensiunea personajului aici este 1x3 în loc de 2x2.

După cum puteți vedea, ar trebui să fie destul de ușor să detectați aceste cazuri în algoritm. Condițiile pentru nodul de apucare a cornierului vor fi următoarele:

  1. Există o placă solidă lângă placile de caractere de sus dreapta / sus.
  2. Există o țiglă goală deasupra plăcii solide găsite.
  3. Nu există o plăcuță solidă sub caracterul respectiv (nu este nevoie să apucați muchiile în cazul în care se află la sol).

Rețineți că cea de-a treia condiție este deja luată în considerare, deoarece verificăm nodul de apucare a perifericului numai dacă caracterul nu este pe pământ.

Mai întâi de toate, hai să verificăm dacă dorim să detectăm, de fapt,

altfel dacă (utilizeazăLedge)

Acum, să verificăm dacă există o placă din dreapta nodului de caractere de sus-dreapta:

altfel dacă (useLedges && mGrid [mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0)

Și apoi, dacă deasupra acelei dale există un spațiu gol:

altfel dacă (useLedges && mGrid [mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 && mGrid [mNewLocationX + caractere, mNewLocationY + characterHeight]! = 0)

Acum trebuie să facem același lucru și pentru partea stângă:

altfel daca (useLedges && ((mGrid [mNewLocationX + characterWidth, mNewLocationY + characterHeight - 1] == 0 && mGrid [mNewLocationX + caracterWidth, mNewLocationY + characterHeight]! = 0) || (mGrid [mNewLocationX - 1, mNewLocationY + characterHeight - 1] == 0 && mGrid [mNewLocationX - 1, mNewLocationY + characterHeight]! = 0)))

Există încă un lucru pe care îl putem opta în mod opțional, care este dezactivat găsirea nodurilor de apucare a cornișoarelor dacă viteza de cădere este prea mare, astfel încât traseul nu întoarce niște poziții extrem de grabante care ar fi greu de urmat de bot:

altfel dacă (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)))  

După toate acestea, putem fi siguri că nodul găsit este un nod de apucare.

Adăugarea unui nod special

Ce ne întâlnim când găsim un nod de apucare? Trebuie să setăm valoarea saltului. 

Amintiți-vă că valoarea saltului este numărul care reprezintă faza de salt a personajului, dacă acesta a atins această celulă. Dacă aveți nevoie de o recapitulare a modului în care funcționează algoritmul, aruncați o privire asupra articolului teoretic.

Se pare că tot ce trebuie să facem este să setăm valoarea saltului nodului 0, deoarece din punctul de capturare a pervazului personajul poate reseta efectiv un salt, ca și cum ar fi fost la sol - dar există câteva puncte pe care să le luați în considerare aici. 

  • În primul rând, ar fi drăguț dacă am putea spune dintr-o privire dacă nodul este un nod de apucare sau nu: aceasta va fi extrem de utilă atunci când se creează un comportament bot și de asemenea când se filtrează nodurile. 
  • În al doilea rând, de obicei, săriturile de la pământ pot fi executate de la orice punct ar fi cel mai potrivit pe o anumită piesă, dar când săriți de pe o apucare de pervaz, personajul este blocat într-o anumită poziție și nu poate să facă nimic decât să înceapă să coboare sau să sară în sus.

Având în vedere aceste avertismente, vom adăuga o valoare de salt specială pentru nodurile de apucare de pe margine. Nu contează cu adevărat ce este această valoare, dar este o idee bună să o faceți negativă, deoarece aceasta ne va reduce șansele de interpretare greșită a nodului.

const scurtă cLedgeGrabJumpValue = -9;

Acum, hai să atribuim această valoare atunci când detectăm un nod de apucare a muchiilor:

altfel dacă (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; 

Efectuarea cLedgeGrabJumpValue negativ va avea un efect asupra calculului costului nodului - va face ca algoritmul să folosească mai degrabă cornișe decât să le ignore. Există două lucruri de reținut aici:

  1. Punctele de prindere a ghearelor oferă o posibilitate mai mare de mișcare decât orice alte noduri în aer, deoarece personajul poate sări din nou prin folosirea lor; din acest punct de vedere, este bine ca aceste noduri să fie mai ieftine decât altele. 
  2. Prinde prea multe muchii duce deseori la o mișcare nenaturală, deoarece, de obicei, jucătorii nu folosesc gravescurile decât dacă sunt necesare pentru a ajunge undeva.

În animația de mai sus, puteți vedea diferența dintre deplasarea în sus atunci când sunt preferate marginile și când nu sunt.

Pentru moment, vom lăsa calculul costurilor ca atare, dar este destul de ușor să îl modificăm, pentru a face ca nodurile să fie mai scumpe.

Modificați valoarea jump când sare sau coborâți dintr-un fund

Acum trebuie să ajustăm valorile saltului pentru nodurile care pornesc de la punctul de prindere a muchiei. Trebuie să facem acest lucru pentru că săriturile de pe o poziție de apucare a pervazului sunt destul de diferite decât săriturile de la sol. Este foarte puțină libertate atunci când săriți de pe o margine, deoarece personajul este fixat la un anumit punct. 

Când se află la sol, personajul se poate mișca liber spre stânga sau spre dreapta și poate sări în momentul cel mai potrivit.

Mai întâi, hai să stabilim cazul când caracterul coboară dintr-o apucare de cornișe:

altfel dacă (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); 

După cum vedeți, lungimea noului salt este ceva mai mare dacă personajul a scăzut de pe o muchie: astfel compensăm lipsa de manevrabilitate în timp ce apucăm o muchie verticală, ceea ce va duce la o viteză verticală mai mare înainte ca jucătorul să ajungă la alte noduri.

Următorul este cazul în care personajul scade într-o parte din capturarea unei muchii:

altfel dacă (! onGround && mNewLocationX! = mLocationX) dacă (jumpLength == cLedgeGrabJumpValue) newJumpLength = (scurt) (maxCharacterJumpHeight * 2 + 3); altfel newJumpLength = (scurt) Mathf.Max (jumpLength + 1, 1); 

Tot ce trebuie să facem este să setăm valoarea saltului la valoarea care se încadrează.

Ignorați mai multe noduri

Trebuie să adăugăm câteva condiții suplimentare când trebuie să ignorăm nodurile. 

Mai intai, cand sarim de pe o pozitie de apucat, trebuie sa mergem in sus, nu in lateral. Aceasta funcționează în mod similar cu a sări de la sol. Viteza verticală este mult mai mare decât viteza orizontală posibilă în acest punct și trebuie să modelăm acest fapt în algoritm:

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

Dacă vrem să lăsăm să cadă de pe marginea de pe partea opusă ca aceasta:

Apoi trebuie să editați condiția care nu permite mișcarea orizontală atunci când valoarea saltului este ciudată. Asta pentru că, în prezent, valoarea noastră de apucătoare specială este egală cu -9, astfel că este necesar să excludem toate numerele negative din această condiție.

dacă (jumpLength> = 0 && jumpLength% 2! = 0 && mLocationX! = mNewLocationX) continuați;

Actualizați filtrul de noduri

În cele din urmă, hai să trecem la filtrarea nodurilor. Tot ceea ce trebuie să facem aici este să adăugăm o condiție pentru nodurile de apucare de la margini, astfel încât să nu le eliminăm. Trebuie doar să verificăm dacă valoarea de salt a nodului este egală cu cLedgeGrabJumpValue:

|| (fNodeTmp.JumpLength == cLedgeGrabJumpValue)

Întreaga filtrare arată astfel:

dacă (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) // marchează săriturile începe || (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength! = 0) // marchează debarcările || (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);

Asta este - acestea sunt toate modificările pe care trebuia să le facem pentru a actualiza algoritmul de căutare.

Modificări bot

Acum, că calea noastră arată punctele la care un personaj poate apuca o muchie, să modificăm comportamentul botului, astfel încât acesta să utilizeze aceste date.

Opriți recalcularea laX și ajungeți la Y

Mai întâi de toate, pentru a face lucrurile mai clare în bot, hai să actualizăm GetContext () funcţie. Problema actuală cu asta este asta reachedX și reachedY valorile sunt recalculate constant, ceea ce îndepărtează unele informații despre context. Aceste valori sunt folosite pentru a vedea dacă botul a atins deja nodul țintă pe axele x și y, respectiv. (Dacă aveți nevoie de o actualizare a modului în care funcționează, consultați tutorialul meu despre codarea botului.)

Să schimbați pur și simplu acest lucru astfel încât dacă un personaj ajunge la nodul de pe axa x sau y, atunci aceste valori rămân adevărate atâta timp cât nu mergem la următorul nod.

Pentru a face acest lucru posibil, trebuie să declarem reachedX și reachedY ca membri de clasă:

bool public mReachedNodeX; bool public mReachedNodeY;

Aceasta înseamnă că nu mai trebuie să le transmitem GetContext () funcţie:

public void GetContext (afară Vector2 prevDest, out Vector2 currentDest, out Vector2 nextDest, out bool destOnGround)

Cu aceste modificări, trebuie de asemenea să resetăm manual variabilele ori de câte ori începem să ne îndreptăm spre următorul nod. Prima întâmplare este atunci când tocmai am găsit calea și urmează să se mute către primul nod:

dacă (cale! = null && path.Count> 1) pentru (var i = path.Count - 1; i> = 0; --i) mPath.Add (cale [i]); mCurrentNodeId = 1; mReachedNodeX = false; mReachedNodeY = false;

A doua este când am ajuns la actualul nod țintă și vrem să ne îndreptăm spre următorul:

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

Pentru a opri recalcularea variabilelor, trebuie să înlocuim următoarele rânduri:

atinsX = ReachedNodeOnXAxis (pathPosition, prevDest, currentDest); a atinsY = ReachedNodeOnYAxis (pathPosition, prevDest, currentDest);

... cu acestea, care vor detecta dacă am ajuns la un nod pe o axă numai dacă nu am ajuns deja la ea:

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

Desigur, trebuie, de asemenea, să înlocuim orice altă apariție reachedX și reachedY cu versiunile recent declarate mReachedNodeX și mReachedNodeY.

A se vedea dacă caracterul trebuie să apuca o farfurie

Să spunem câteva variabile pe care le vom folosi pentru a determina dacă botul trebuie să apucă o margine și, dacă da, care dintre ele:

bool public mGrabsLedges = false; boolmustGrabLeftLedge; bool mMustGrabRightLedge;

mGrabsLedges este un steguleț pe care îl transmitem algoritmului pentru a-i da seama dacă ar trebui să găsească o cale incluzând capturile de la margine. mMustGrabLeftLedge și mMustGrabRightLedge va fi folosit pentru a determina dacă nodul următor este o margine de apucare și dacă botul ar trebui să apucă muchia spre stânga sau spre dreapta.

Ceea ce vrem să facem acum este să creeze o funcție care, dată de un nod, va fi capabilă să detecteze dacă caracterul din acel nod va putea să apuce o muchie. 

Vom avea nevoie de două funcții pentru aceasta: se va verifica dacă personajul poate apuca o margine în stânga, iar celălalt va verifica dacă personajul poate apuca o muchie în dreapta. Aceste funcții vor funcționa la fel ca și codul nostru de identificare a rândurilor:

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

După cum vedeți, verificăm dacă există o țiglă solidă lângă caracterul nostru, cu o placă goală deasupra ei.

Acum hai să mergem la GetContext () funcția și să atribuiți valorile corespunzătoare mMustGrabRightLedge și mMustGrabLeftLedge. Trebuie să le punem Adevărat în cazul în care caracterul ar trebui să apuca pervazuri la toate (adică, dacă mGrabsLedges este Adevărat) și dacă există o margine pentru ao prinde.

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

Rețineți că, de asemenea, nu vrem să luăm cornișe dacă nodul de destinație este la sol.

Actualizați valorile saltului

Așa cum puteți observa, poziția personajului atunci când apucați o muchie este puțin diferită de poziția sa când stați chiar sub ea:

Poziția de ridicare a muchiei este puțin mai mare decât poziția în picioare, chiar dacă aceste caractere ocupă același nod. Acest lucru înseamnă că apucarea unei muchii va necesita un salt ușor mai mare decât săriturați pe o platformă și trebuie să luăm acest lucru în considerare.

Să ne uităm la funcția care determină cât timp trebuie apăsat butonul de salt:

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

Mai întâi, vom schimba condiția inițială. Botezul ar trebui să poată să sară, nu doar de la sol, ci și atunci când apucă o muchie:

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

Acum trebuie să adăugăm încă câteva cadre dacă este sărituri pentru a apuca o muchie. Mai întâi de toate, trebuie să știm dacă poate face acest lucru, așa că hai să creăm o funcție care ne va spune dacă personajul poate apuca o muchie fie spre stânga sau spre dreapta:

bool public CanGrabLedge (int nodeId) întoarcere CanGrabLedgeOnLeft (nodeId) || CanGrabLedgeOnRight (nodeId); 

Acum, să adăugăm câteva cadre la salt atunci când botul trebuie să apucă o muchie:

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

După cum vedeți, prelungim saltul 4 cadre, care ar trebui să facă treaba bine în cazul nostru.

Dar mai este un lucru pe care trebuie să-l schimbăm aici, ceea ce nu are de-a face cu grija. Se stabilește un caz în care următorul nod are aceeași înălțime ca cel curent, dar nu este la sol, iar nodul după care este în sus, adică un salt este necesar:

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

Implementați logica mișcării pentru a obține grafurile în picioare și a cădea

Vom încerca să împărțim logica în două faze: una pentru când botul nu este suficient de aproape pentru a începe să se apuce, așa că pur și simplu dorim să continuăm mișcarea ca de obicei și una pentru că băiatul poate începe în siguranță se îndreaptă spre ea pentru al apuca.

Să începem prin a declara un boolean care va indica dacă am trecut deja la a doua fază. O vom numi mCanGrabLedge:

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

Acum trebuie să definim condițiile care vor lăsa personajul să treacă la a doua fază. Acestea sunt destul de simple:

  • Botul a ajuns deja la nodul gol pe axa X.
  • Bota trebuie să apuce fie la stânga, fie la dreapta.
  • Dacă botul se îndreaptă spre margine, se va ciocni într-un zid în loc să meargă mai departe.

În regulă, primele două condiții sunt foarte simple pentru a verifica acum, deoarece am făcut deja toate lucrările necesare deja:

dacă mCanGrabLedge && mReachedNodeX && (mMustGrabLeftLedge ||mustGrabRightLedge))  altfel dacă (mReachedNodeX && mReachedNodeY)

Acum, a treia condiție se poate separa în două părți. Primul va avea grijă de situația în care caracterul se mișcă spre marginea de jos, iar cel de-al doilea de sus. Condițiile pe care dorim să le stabilim pentru primul caz sunt:

  • Poziția curentă a botului este mai mică decât poziția țintă (se apropie de partea de jos).
  • Partea de sus a casetei de margine a personajului este mai mare decât înălțimea plăcuței.
(pathPosition.y < currentDest.y && (currentDest.y + Map.cTileSize*mHeight) < pathPosition.y + mAABB.HalfSizeY * 2)

Dacă botul se apropie de sus, condițiile sunt după cum urmează:

  • Poziția actuală a botului este mai mare decât poziția țintă (se apropie de partea de sus).
  • Diferența dintre poziția personajului și poziția țintă este mai mică decât înălțimea caracterului.
(pathPosition.y> currentDest.y && pathPosition.y - currentDest.y < mHeight * Map.cTileSize)

Acum, să combinăm toate aceste lucruri și să setăm steagul care indică faptul că putem trece în siguranță în direcția unei muchii:

 altfel dacă (! mCanGrabLedge && mReachedNodeX && (mMustGrabLeftLedge ||mustGrabRightLedge) && ((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; 

Mai este un lucru pe care dorim să-l facem aici, și anume să începem imediat să ne îndreptăm spre margine:

dacă (! mCanGrabLedge && mReachedNodeX && (mMustGrabLeftLedge ||mustGrabRightLedge) && ((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; 

Bine, acum, înainte de această condiție imensă, să creăm una mai mică. Aceasta va fi în esență o versiune simplificată a mișcării atunci când botul este pe punctul de a apuca o muchie:

dacă (mCanGrabLedge && mCurrentState! = CharacterState.GrabLedge) dacă (mMustGrabLeftLedge) mInputurile [(int) KeyInput.GoLeft] = true; altfel, dacă (mMustGrabRightLedge) mInputs [(int) KeyInput.GoRight] = true;  altfel dacă (! mCanGrabLedge && mReachedNodeX && (mMustGrabLeftLedge ||mustGrabRightLedge) &&

Aceasta este principala logică din spatele graiferului, dar mai sunt încă câteva lucruri de făcut. 

Trebuie să editați condiția în care verificăm dacă este OK să treceți la următorul nod. În prezent, starea arată astfel:

altfel dacă (mReachedNodeX && mReachedNodeY)

Acum trebuie să ne mutăm și la următorul nod dacă botul ar fi gata să prindă muchia și apoi ar face-o:

altfel dacă ((mReachedNodeX && mReachedNodeY) || (mCanGrabLedge && mCurrentState == CharacterState.GrabLedge))

Manevrați Sărituri și căderi de pe cadran

Odată ce botul se află pe margine, ar trebui să fie capabil să sară normal, deci să adăugăm o condiție suplimentară rutinei de sărituri:

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

Următorul lucru pe care botul trebuie să îl poată face este să cadă grațios pe margine. Cu implementarea actuală este foarte simplă: dacă luăm o muchie și nu saltăm, atunci trebuie să scăpăm din ea!

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

Asta e! Acum, personajul este capabil să părăsească foarte ușor poziția de apucare a corzilor, indiferent dacă are nevoie să sară în sus sau pur și simplu să coboare.

Opriți să ridicați margini tot timpul!

În acest moment, botul captează fiecare margine pe care o poate, indiferent dacă are sens să o facă. 

O soluție la acest lucru este să atribuiți un cost euristic mare gravelor, astfel încât algoritmul dă prioritate împotriva utilizării acestora, dacă nu trebuie să - dar acest lucru ar necesita botul nostru să aibă mai multe informații despre noduri. Deoarece tot trecem la bot este o listă de puncte, nu știm dacă algoritmul a însemnat un nod special de a fi orbit sau nu; botul presupune că, dacă ar putea fi prins un pervaz, cu siguranță ar trebui! 

Putem implementa o soluție rapidă pentru acest comportament: vom apela funcția de identificare a traseului de două ori. Prima dată când o vom apela cu useLedges parametrul setat la fals, și a doua oară cu asta Adevărat.

Să atribuim prima cale ca traseu găsit fără a utiliza orice capcană graves:

Listă calea1 = null; cale var = mMap.mPathFinder.FindPath (startTile, destinație, Mathf.CeilToInt (mAABB.HalfSizeX / 8.0f), Mathf.CeilToInt (mAABB.HalfSizeY / 8.0f), (scurt) mMaxJumpHeight, false);

Acum, dacă asta cale nu este nulă, trebuie să copia