A * Pathfinding per piattaforme 2D basate su griglia Ledge Grabbing

In questa parte della nostra serie sull'adattamento dell'algoritmo di pathfinder A * ai platform, introdurremo una nuova meccanica per il personaggio: il ledge grabbing. Apporteremo anche le modifiche appropriate sia all'algoritmo del pathfinder che all'IA artificiale, in modo che possano sfruttare la mobilità migliorata.

dimostrazione

Puoi giocare alla demo di Unity o alla versione WebGL (16 MB) per vedere il risultato finale in azione. Uso WASD spostare il personaggio, sinistro del mouse su un punto per trovare un percorso che puoi seguire per arrivarci, tasto destro del mouse una cella per alternare il terreno in quel punto, middle-click posizionare una piattaforma a senso unico, e clicca e trascina i cursori per cambiare i loro valori.

Ledge Grabbing Mechanics

Panoramica dei controlli

Diamo prima un'occhiata a come la meccanica di afferrare la sporgenza funziona nella demo per avere un'idea di come dovremmo cambiare il nostro algoritmo di path-path per tenere conto di questa nuova meccanica.

I controlli per l'accaparramento della sporgenza sono abbastanza semplici: se il personaggio è proprio accanto a una sporgenza mentre cade, e il giocatore preme il tasto direzionale sinistro o destro per spostarli verso quella sporgenza, allora quando il personaggio è nella posizione giusta, afferrerà la sporgenza.

Una volta che il personaggio ha afferrato una sporgenza, il giocatore ha due opzioni: possono saltare o scendere. Saltare funziona normalmente; il giocatore preme il tasto di salto e la forza del salto è identica alla forza applicata quando salta da terra. L'abbandono avviene premendo il pulsante Giù (S), o il tasto direzionale che punta lontano dalla sporgenza.

Implementazione dei controlli

Andiamo a vedere come funzionano i controlli della sporgenza della sporgenza nel codice. La prima cosa da fare è rilevare se la sporgenza si trova a sinistra oa destra del personaggio:

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

Possiamo usare queste informazioni per determinare se il personaggio debba lasciare la sporgenza. Come puoi vedere, per scendere, il giocatore deve:

  • premere il pulsante Giù,
  • premere il tasto sinistro quando stiamo afferrando una sporgenza sulla destra, o
  • premi il tasto destro quando afferriamo una sporgenza sulla sinistra.
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))  

C'è un piccolo avvertimento qui. Considera una situazione in cui stiamo tenendo premuto il pulsante Giù e il pulsante destro, quando il personaggio si trova su una sporgenza a destra. Risulterà nella seguente situazione:

Il problema qui è che il personaggio afferra la sporgenza immediatamente dopo averla lasciata andare. 

Una soluzione semplice a questo è bloccare il movimento verso la sporgenza per un paio di fotogrammi dopo che siamo caduti dalla sporgenza. Questo è ciò che segue il seguente frammento:

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; 

Dopo questo, cambiamo lo stato del personaggio in Saltare, che gestirà la fisica del salto:

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; 

Infine, se il personaggio non è caduto dalla sporgenza controlliamo se il tasto di salto è stato premuto; se è così, impostiamo la velocità verticale del salto e cambiamo lo stato:

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; 

Rilevazione di un punto di presa della sporgenza

Diamo un'occhiata a come determiniamo se una sporgenza può essere afferrata. Usiamo alcuni hotspot attorno al bordo del personaggio:

Il contorno giallo rappresenta i limiti del personaggio. I segmenti rossi rappresentano i sensori di parete; questi sono usati per gestire la fisica del personaggio. I segmenti blu rappresentano dove il nostro personaggio può afferrare una sporgenza.

Per determinare se il personaggio può afferrare una sporgenza, il nostro codice controlla costantemente il lato verso cui si sta muovendo. Sta cercando una tessera vuota nella parte superiore del segmento blu, quindi una tegola piena al di sotto della quale il personaggio può aggrapparsi. 

Nota: la sporgenza della sporgenza è bloccata se il personaggio sta saltando su. Questo può essere facilmente notato nella demo e nell'animazione nella sezione Panoramica dei controlli.

Il problema principale con questo metodo è che se il nostro personaggio cade ad alta velocità, è facile perdere una finestra in cui può afferrare una sporgenza. Possiamo risolvere questo problema cercando tutte le tessere dalla posizione del frame precedente al frame corrente in cerca di qualsiasi tessera vuota sopra una solida. Se una di queste tessere viene trovata, allora può essere afferrata.

Ora abbiamo chiarito come funziona la sporgenza meccanica, vediamo come incorporarla nel nostro algoritmo di pathfinder.

Cambiamenti del Pathfinder

Rendere possibile accendere e spegnere la sporgenza della sporgenza

Prima di tutto, aggiungiamo un nuovo parametro al nostro FindPath funzione che indica se il pathfinder dovrebbe prendere in considerazione le sporgenze. Lo nomineremo useLedges:

lista pubblica FindPath (Vector2i start, Vector2i end, int characterWidth, int characterHeight, short maxCharacterJumpHeight, bool useLedges)

Rileva nodi di inclinazione di sporgenza

condizioni

Ora abbiamo bisogno di modificare la funzione per rilevare se un particolare nodo può essere usato per l'accostamento della sporgenza. Possiamo farlo dopo aver controllato se il nodo è un nodo "on ground" o un nodo "at ceiling", perché in entrambi i casi non può essere usato per l'accostamento della sporgenza.

if (onGround) newJumpLength = 0; else if (atCeiling) if (mNewLocationX! = mLocationX) newJumpLength = (short) Mathf.Max (maxCharacterJumpHeight * 2 + 1, jumpLength + 1); else newJumpLength = (short) Mathf.Max (maxCharacterJumpHeight * 2, jumpLength + 2);  else if (/ * controlla se c'è un nodo che cattura grappa qui * /)  else if (mNewLocationY < mLocationY) 

Va bene: ora dobbiamo capire quando un nodo deve essere considerato un nodo che cattura la sporgenza. Per la chiarezza, ecco un diagramma che mostra alcuni esempi di posizioni di presa della sporgenza:

... ed ecco come questi potrebbero apparire nel gioco:

Gli sprite dei personaggi principali sono allungati per mostrare come appaiono con personaggi di dimensioni diverse.

Le celle rosse rappresentano i nodi controllati; insieme con le celle verdi, rappresentano il carattere nel nostro algoritmo. Le prime due situazioni mostrano una sporgenza di 2x2 caratteri rispettivamente a sinistra e a destra. I due in basso mostrano la stessa cosa, ma la dimensione del personaggio qui è 1x3 invece di 2x2.

Come puoi vedere, dovrebbe essere abbastanza facile rilevare questi casi nell'algoritmo. Le condizioni per il nodo di afferramento della sporgenza saranno le seguenti:

  1. C'è una tessera solida accanto alla tessera personaggio in alto a destra / in alto a sinistra.
  2. C'è una tessera vuota sopra la tessera solida trovata.
  3. Non c'è una tessera solida sotto il personaggio (non è necessario afferrare sporgenze se sul terreno).

Nota che la terza condizione è già stata presa in considerazione, dal momento che controlliamo il nodo della sporgenza della sporgenza solo se il personaggio non è a terra.

Prima di tutto, controlliamo se effettivamente vogliamo rilevare le prese di sporgenza:

altrimenti se (useLedges)

Ora controlliamo se c'è un riquadro a destra del nodo dei caratteri in alto a destra:

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

E poi, se sopra quella tessera c'è uno spazio vuoto:

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

Ora dobbiamo fare la stessa cosa per il lato sinistro:

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

C'è ancora una cosa che possiamo opzionalmente fare, che è disabilitare trovare i nodi della sporgenza della sporgenza se la velocità di caduta è troppo alta, quindi il percorso non restituisce alcune sporgenze estreme che afferrano posizioni che sarebbero difficili da seguire dal bot:

altrimenti se (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)))  

Dopotutto, possiamo essere sicuri che il nodo trovato sia un nodo di presa di sporgenza.

Aggiunta di un nodo speciale

Cosa facciamo quando troviamo un nodo di afferrare una sporgenza? Abbiamo bisogno di impostare il suo valore di salto. 

Ricorda, il valore di salto è il numero che rappresenta quale fase del salto il personaggio sarebbe, se avesse raggiunto questa cella. Se avete bisogno di un riassunto su come funziona l'algoritmo, date un altro sguardo all'articolo teorico.

Sembra che tutto ciò che dovremmo fare è impostare il valore di salto del nodo su 0, perché dal punto di presa della sporgenza il personaggio può effettivamente resettare un salto, come se fosse a terra, ma ci sono un paio di punti da considerare qui. 

  • In primo luogo, sarebbe bello se potessimo dire a colpo d'occhio se il nodo è un nodo di presa o meno: questo sarà estremamente utile quando si crea un comportamento del bot e anche quando si filtrano i nodi. 
  • In secondo luogo, solitamente saltare da terra può essere eseguito da qualsiasi punto sarebbe più adatto su una particolare tessera, ma quando si salta da una sporgenza della sporgenza, il personaggio è bloccato in una posizione particolare e non può fare altro che iniziare a cadere o saltare verso l'alto.

Considerando tali avvertimenti, aggiungeremo un valore di salto speciale per i nodi di presa di sporgenza. Non importa cosa sia questo valore, ma è una buona idea renderlo negativo, poiché ciò ridurrà le possibilità di interpretare erroneamente il nodo.

const short cLedgeGrabJumpValue = -9;

Ora assegniamo questo valore quando rileviamo un nodo di presa di sporgenza:

altrimenti se (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; 

Fabbricazione cLedgeGrabJumpValue il negativo avrà un effetto sul calcolo del costo del nodo: farà sì che l'algoritmo preferisca utilizzare le sporgenze piuttosto che saltarle. Ci sono due cose da notare qui:

  1. I punti di presa della sporgenza offrono una maggiore possibilità di movimento rispetto a qualsiasi altro nodo in aria, perché il personaggio può saltare di nuovo usandoli; da questo punto di vista, è un bene che questi nodi siano più economici di altri. 
  2. Afferrare troppe sporgenze porta spesso a movimenti innaturali, perché in genere i giocatori non utilizzano le prese di sporgenza a meno che non siano necessarie per raggiungere un punto.

Nell'animazione sopra, puoi vedere la differenza tra salire quando le sporgenze sono preferite e quando non lo sono.

Per ora lasceremo il calcolo dei costi così com'è, ma è abbastanza facile modificarlo, per rendere i nodi di sporgenza più costosi.

Modificare il valore di salto quando si salta o si scende da una sporgenza

Ora dobbiamo regolare i valori di salto per i nodi che iniziano dal punto di presa della sporgenza. Dobbiamo farlo perché saltare da una posizione di presa è abbastanza diverso dal saltare da terra. Quando salti da una sporgenza c'è pochissima libertà, perché il personaggio è fissato su un punto particolare. 

Quando è a terra, il personaggio può muoversi liberamente a sinistra o a destra e saltare nel momento più adatto.

Per prima cosa, impostiamo il caso quando il personaggio scende da una sporgenza di sporgenza:

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

Come puoi vedere, la nuova lunghezza di salto è un po 'più grande se il personaggio è sceso da una sporgenza: in questo modo compensiamo la mancanza di manovrabilità afferrando una sporgenza, che si tradurrà in una maggiore velocità verticale prima che il giocatore possa raggiungere altri nodi.

Il prossimo è il caso in cui il personaggio cade da un lato afferrando una sporgenza:

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

Tutto ciò che dobbiamo fare è impostare il valore di salto sul valore di caduta.

Ignora altri nodi

Dobbiamo aggiungere un paio di condizioni aggiuntive per quando dobbiamo ignorare i nodi. 

Prima di tutto, quando saltiamo da una posizione di presa della sporgenza, dobbiamo salire, non di lato. Funziona allo stesso modo semplicemente saltando da terra. La velocità verticale è molto più alta della possibile velocità orizzontale a questo punto, e abbiamo bisogno di modellare questo fatto nell'algoritmo:

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

Se vogliamo lasciare cadere dalla sporgenza sul lato opposto in questo modo:

Quindi dobbiamo modificare la condizione che non consente il movimento orizzontale quando il valore di salto è dispari. Questo perché, al momento, il nostro valore di afferrare la sporgenza speciale è uguale a -9, quindi è opportuno escludere tutti i numeri negativi da questa condizione.

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

Aggiorna il filtro del nodo

Infine, passiamo al filtro dei nodi. Tutto quello che dobbiamo fare qui è aggiungere una condizione per i nodi di afferramento della sporgenza, in modo da non filtrarli. Abbiamo semplicemente bisogno di controllare se il valore di salto del nodo è uguale a cLedgeGrabJumpValue:

|| (fNodeTmp.JumpLength == cLedgeGrabJumpValue)

L'intero filtraggio appare ora:

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) // contrassegna salti inizia || (fNodeTmp.JumpLength == 0 && fPrevNodeTmp.JumpLength! = 0) // contrassegna atterraggi || (fNode.y> mChiudi [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);

Ecco, questi sono tutti i cambiamenti che dovevamo apportare per aggiornare l'algoritmo del pathfinder.

Modifiche ai Bot

Ora che il nostro percorso mostra i punti in cui un personaggio può afferrare una sporgenza, modifichiamo il comportamento del bot in modo che faccia uso di questi dati.

Arresta il ricalcolo raggiuntoX e raggiuntoY

Prima di tutto, per rendere le cose più chiare nel bot, aggiorniamo il GetContext () funzione. Il problema attuale è quello reachedX e reachedY i valori vengono costantemente ricalcolati, il che rimuove alcune informazioni sul contesto. Questi valori sono usati per vedere se il bot ha già raggiunto il nodo di destinazione sui suoi assi xey rispettivamente. (Se hai bisogno di un aggiornamento su come funziona, dai un'occhiata al mio tutorial sulla codifica del bot.)

Cambiamo semplicemente questo in modo che se un personaggio raggiunge il nodo sull'asse x o y, questi valori rimangono veri finché non passiamo al nodo successivo.

Per rendere possibile ciò, dobbiamo dichiararlo reachedX e reachedY come membri della classe:

public bool mReachedNodeX; public bool mReachedNodeY;

Questo significa che non abbiamo più bisogno di passarli a GetContext () funzione:

pubblico vuoto GetContext (out Vector2 prevDest, out Vector2 currentDest, out Vector2 nextDest, out bool destOnGround)

Con questi cambiamenti, abbiamo anche bisogno di resettare le variabili manualmente ogni volta che iniziamo a spostarci verso il prossimo nodo. La prima occorrenza è quando abbiamo appena trovato il percorso e ci stiamo spostando verso il primo nodo:

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

Il secondo è quando abbiamo raggiunto il nodo di destinazione corrente e vogliamo spostarci verso il prossimo:

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

Per interrompere il ricalcolo delle variabili, è necessario sostituire le seguenti righe:

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

... con questi, che rileveranno se abbiamo raggiunto un nodo su un asse solo se non lo abbiamo già raggiunto:

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

Naturalmente, dobbiamo anche sostituire ogni altra occorrenza di reachedX e reachedY con le versioni appena dichiarate mReachedNodeX e mReachedNodeY.

Vedi se il personaggio ha bisogno di afferrare una sporgenza

Dichiariamo una coppia di variabili che useremo per determinare se il bot ha bisogno di afferrare una sporgenza e, in tal caso, quale:

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

mGrabsLedges è una bandiera che passiamo all'algoritmo per fargli sapere se dovrebbe trovare un percorso che includa le prese della sporgenza. mMustGrabLeftLedge e mMustGrabRightLedge verrà utilizzato per determinare se il nodo successivo è una sporgenza e se il bot deve afferrare la sporgenza a sinistra oa destra.

Quello che vogliamo fare ora è creare una funzione che, dato un nodo, sarà in grado di rilevare se il personaggio in quel nodo sarà in grado di afferrare una sporgenza. 

Avremo bisogno di due funzioni: una controllerà se il personaggio può afferrare una sporgenza sulla sinistra e l'altra controllerà se il personaggio può afferrare una sporgenza sulla destra. Queste funzioni funzioneranno allo stesso modo del nostro codice di tracciamento per il rilevamento di sporgenze:

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

Come puoi vedere, controlliamo se c'è una tessera solida accanto al nostro personaggio con una tessera vuota sopra di essa.

Ora andiamo al GetContext () funzione e assegnare i valori appropriati a mMustGrabRightLedge e mMustGrabLeftLedge. Dobbiamo metterli a vero se il personaggio dovrebbe afferrare le sporgenze (cioè, se mGrabsLedges è vero) e se c'è una sporgenza da afferrare.

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

Si noti inoltre che non vogliamo prendere sporgenze se il nodo di destinazione è sul terreno.

Aggiorna i valori di salto

Come puoi notare, la posizione del personaggio quando si afferra una sporgenza è leggermente diversa dalla sua posizione quando si trova proprio sotto di essa:

La posizione di presa della sporgenza è leggermente superiore alla posizione in piedi, anche se questi personaggi occupano lo stesso nodo. Ciò significa che afferrare una sporgenza richiederà un salto leggermente più alto di un semplice salto su una piattaforma, e dobbiamo tenerne conto.

Diamo un'occhiata alla funzione che determina per quanto tempo deve essere premuto il pulsante di salto:

public int GetJumpFramesForNode (int prevNodeId) int currentNodeId = prevNodeId + 1; if (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; if (mPath [i] .y - mPath [prevNodeId] .y < jumpHeight || mMap.IsGround(mPath[i].x, mPath[i].y - 1)) return GetJumpFrameCount(jumpHeight);   return 0; 

Prima di tutto, cambieremo la condizione iniziale. Il robot dovrebbe essere in grado di saltare, non solo da terra, ma anche quando sta afferrando una sporgenza:

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

Ora dobbiamo aggiungere qualche altro fotogramma se salta per afferrare una sporgenza. Prima di tutto, dobbiamo sapere se può effettivamente farlo, quindi creiamo una funzione che ci dirà se il personaggio può afferrare una sporgenza a sinistra oa destra:

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

Ora aggiungiamo un paio di frame al salto quando il bot ha bisogno di afferrare una sporgenza:

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

Come puoi vedere, prolunghiamo il salto 4 cornici, che dovrebbero fare il lavoro bene nel nostro caso.

Ma c'è un'altra cosa che dobbiamo cambiare qui, che in realtà non ha molto a che fare con l'accaparramento della sporgenza. Risolve un caso in cui il nodo successivo ha la stessa altezza di quello corrente, ma non è sul terreno, e il nodo dopo è in alto, il che significa che è necessario un salto:

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

Implementa la logica di movimento per afferrare e abbassare sporgenze

Vogliamo dividere la sporgenza logica della presa in due fasi: una per quando il bot non è ancora abbastanza vicino alla sporgenza per iniziare ad afferrare, quindi vogliamo semplicemente continuare il movimento come al solito, e uno per quando il ragazzo può iniziare in sicurezza avanzando verso di esso per afferrarlo.

Iniziamo dichiarando un booleano che indicherà se siamo già passati alla seconda fase. Lo nomineremo mCanGrabLedge:

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

Ora dobbiamo definire le condizioni che consentiranno al personaggio di passare alla seconda fase. Questi sono piuttosto semplici:

  • Il bot ha già raggiunto il nodo obiettivo sull'asse X..
  • Il robot ha bisogno di afferrare la sporgenza sinistra o destra.
  • Se il robot si sposta verso la sporgenza, si imbatterà in un muro invece di andare oltre.

Va bene, le prime due condizioni sono molto semplici da verificare ora perché abbiamo già fatto tutto il lavoro necessario:

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

Ora, la terza condizione possiamo separare in due parti. Il primo si prenderà cura della situazione in cui il personaggio si sposta verso la sporgenza dal basso, e il secondo dall'alto. Le condizioni che vogliamo impostare per il primo caso sono:

  • La posizione corrente del bot è inferiore alla posizione target (si avvicina dal basso).
  • La parte superiore del riquadro di delimitazione del personaggio è più alta dell'altezza della tessera di sporgenza.
(pathPosition.y < currentDest.y && (currentDest.y + Map.cTileSize*mHeight) < pathPosition.y + mAABB.HalfSizeY * 2)

Se il bot si sta avvicinando dall'alto, le condizioni sono le seguenti:

  • La posizione attuale del bot è superiore alla posizione target (si sta avvicinando dall'alto).
  • La differenza tra la posizione del personaggio e la posizione di destinazione è inferiore all'altezza del personaggio.
(pathPosition.y> currentDest.y && pathPosition.y - currentDest.y < mHeight * Map.cTileSize)

Ora combiniamo tutti questi elementi e impostiamo la bandiera che indica che possiamo spostarci in modo sicuro verso una sporgenza:

 altrimenti se (! 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; 

C'è un'altra cosa che vogliamo fare qui, e cioè iniziare immediatamente a muoverci verso la sporgenza:

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, ora prima di questa enorme condizione creiamo uno più piccolo. Questa sarà fondamentalmente una versione semplificata per il movimento quando il robot sta per afferrare una sporgenza:

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) &&

Questa è la logica principale alla base della presa, ma ci sono ancora un paio di cose da fare. 

Dobbiamo modificare la condizione in cui controlliamo se è OK passare al nodo successivo. Attualmente, la condizione è simile a questa:

altrimenti se (mReachedNodeX && mReachedNodeY)

Ora dobbiamo anche passare al nodo successivo se il bot era pronto per afferrare la sporgenza e poi effettivamente lo ha fatto:

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

Gestisci il salto e il rilascio dalla sporgenza

Una volta che il bot è sulla sporgenza, dovrebbe essere in grado di saltare normalmente, quindi aggiungiamo una condizione aggiuntiva alla routine di salto:

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

La prossima cosa che il bot deve essere in grado di fare è abbandonare con grazia la sporgenza. Con l'attuale implementazione è molto semplice: se stiamo afferrando una sporgenza e non stiamo saltando, ovviamente dobbiamo lasciarlo cadere!

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

Questo è tutto! Ora il personaggio è in grado di lasciare senza problemi la posizione di presa della sporgenza, indipendentemente dal fatto che debba saltare o semplicemente scendere.

Smettila di afferrare sporgenze tutto il tempo!

Al momento, il bot afferra ogni sporgenza che può, indipendentemente dal fatto che abbia senso farlo. 

Una soluzione a questo è assegnare un grande costo euristico alle prese di sporgenza, quindi l'algoritmo dà la priorità sull'utilizzo di esse se non è necessario, ma ciò richiederebbe al nostro bot di avere un po 'più di informazioni sui nodi. Dal momento che tutto ciò che passiamo al bot è un elenco di punti, non sappiamo se l'algoritmo abbia significato che un nodo particolare debba essere scavalc