A * Pathfinding per platformer 2D basati su griglia diverse dimensioni dei caratteri

In questo tutorial, estenderemo il nostro pathfinder platform basato su griglia in modo che possa affrontare i personaggi che occupano più di una cella della griglia.

Se non hai ancora aggiunto il supporto per la piattaforma unidirezionale al tuo codice, ti consiglio di farlo, ma non è necessario per seguire questo tutorial.

dimostrazione

Puoi giocare alla demo di Unity o alla versione WebGL (100 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, e clicca e trascina i cursori per cambiare i loro valori.

I personaggi di dimensioni diverse devono seguire percorsi diversi, l'algoritmo aggiornato lo riconosce.

Posizione del personaggio

Il Pathfinder accetta la posizione, la larghezza e l'altezza del personaggio come input. Mentre la larghezza e l'altezza sono facili da interpretare, dobbiamo chiarire a quale blocco si riferiscono le coordinate di posizione.

La posizione che superiamo deve essere in termini di coordinate della mappa, il che significa che, ancora una volta, abbiamo bisogno di accettare qualche inesattezza. Ho deciso che sarebbe stato sensato fare in modo che la posizione si riferisse al riquadro del personaggio in basso a sinistra, poiché questo corrisponde al sistema di coordinate della mappa.

Posizioni delle tessere del personaggio per un personaggio 3x3. 

Con quello chiarito, possiamo aggiornare il pathfinder.

Verifica che l'obiettivo sia realistico

Innanzitutto, dobbiamo assicurarci che il nostro carattere di dimensioni personalizzate possa rientrare nel percorso di destinazione. Fino a questo punto, abbiamo controllato solo un blocco per farlo, poiché si trattava della dimensione massima (e unica) del personaggio:

if (mGrid [end.x, end.y] == 0) restituisce null;

Ora, tuttavia, abbiamo bisogno di scorrere tutte le celle che il personaggio occuperebbe se si trovasse nella posizione finale, e controllare se qualcuno di loro è un blocco solido. Se lo sono, ovviamente il personaggio non può stare lì, quindi l'obiettivo non può essere raggiunto.

Per fare questo, dichiariamo innanzitutto un booleano a cui ci concentreremo falso se il personaggio è in una tessera solida e vero altrimenti:

var inSolidTile = false;

Successivamente, faremo scorrere ogni blocco del personaggio:

per (var w = 0; w < characterWidth; ++w)  for (int h = 0; h < characterHeight; ++h)   

All'interno di questo ciclo, dobbiamo verificare se un determinato blocco è solido; se è così, abbiamo impostato inSolidTile a vero, e uscire dal ciclo:

per (var w = 0; w < characterWidth; ++w)  for (int h = 0; h < characterHeight; ++h)  if (mGrid[end.x + w, end.y + h] == 0 || mGrid[end.x + w, end.y + h] == 0)  inSolidTile = true; break;   if (inSolidTile) break; 

Ma questo non è abbastanza. Si consideri la seguente situazione:

Blocchi verdi: carattere; blocco blu: obiettivo.

Se dovessimo spostare il personaggio in modo che il suo fondo-sinistra blocco occupato l'obiettivo, poi il fondo-destra blocco sarebbe bloccato in un blocco solido, quindi l'algoritmo penserebbe che, poiché il personaggio non si adatta alla posizione obiettivo, è impossibile raggiungere il punto finale. Certo, non è vero; non ci interessa quale parte del personaggio raggiunga l'obiettivo. 

Per risolvere questo problema, sposteremo il punto finale a sinistra, passo dopo passo, fino al punto in cui la posizione dell'obiettivo originale corrisponderebbe al fondo-destra blocco di caratteri:

per (var i = 0; i < characterWidth; ++i)  inSolidTile = false; for (var w = 0; w < characterWidth; ++w)  for (var h = 0; h < characterHeight; ++h)  if (mGrid[end.x + w, end.y + h] == 0 || mGrid[end.x + w, end.y + h] == 0)  inSolidTile = true; break;   if (inSolidTile) break;  if (inSolidTile) end.x -= 1; else break; 

Nota che non dovremmo semplicemente controllare gli angoli inferiore sinistro e destro, perché potrebbe verificarsi il seguente caso:

Di nuovo, blocchi verdi: carattere; blocco blu: obiettivo.

Qui, puoi vedere che se uno degli angoli inferiori occupa la posizione obiettivo, il personaggio sarebbe comunque in una zona solida sull'altro lato. In questo caso, dobbiamo abbinare il fondo-centro bloccare con l'obiettivo.

Infine, se non riusciamo a trovare un posto dove il personaggio si adatterebbe, potremmo anche uscire presto dall'algoritmo:

if (inSolidTile == true) restituisce null;

Determinazione della posizione di partenza

Per vedere se il nostro personaggio è a terra, dobbiamo verificare se qualunque delle celle più in basso del personaggio si trovano direttamente sopra una tessera solida.

Diamo un'occhiata al codice che abbiamo usato per un carattere 1x1:

if (mMap.IsGround (start.x, start.y - 1)) firstNode.JumpLength = 0; else firstNode.JumpLength = (short) (maxCharacterJumpHeight * 2);

Determiniamo se il punto di partenza è a terra controllando se la tessera immediatamente sotto il punto di partenza è una tessera terreno. Per aggiornare il codice, faremo semplicemente controllare sotto tutti i blocchi più in basso del personaggio. 

Per prima cosa, dichiariamo un booleano che ci dirà se il personaggio inizia a terra. Inizialmente, assumiamo che non lo faccia:

bool startsOnGround = false;

Successivamente, eseguiremo l'iterazione di tutti i blocchi di caratteri più in basso e controlleremo se qualcuno di essi si trova direttamente sopra una tessera terreno. Se è così, allora abbiamo impostato startsOnGround a vero e uscire dal ciclo:

for (int x = start.x; x < start.x + characterWidth; ++x)  if (mMap.IsGround(x, start.y - 1))  startsOnGround = true; break;  

Infine, impostiamo il valore di salto a seconda che il personaggio sia iniziato sul terreno:

 if (startsOnGround) firstNode.JumpLength = 0; else firstNode.JumpLength = (short) (maxCharacterJumpHeight * 2);

Controllo dei limiti del successore

Dobbiamo anche cambiare i limiti dei nostri successori, ma qui non abbiamo bisogno di controllare ogni tessera. Va abbastanza bene per controllare il contorno del personaggio - i blocchi attorno al bordo - perché sappiamo che la posizione del genitore andava bene.

Diamo un'occhiata a come abbiamo controllato i limiti del successore in precedenza:

if (mGrid [mNewLocationX, mNewLocationY] == 0) continua; if (mMap.IsGround (mNewLocationX, mNewLocationY - 1)) onGround = true; else if (mGrid [mNewLocationX, mNewLocationY + characterHeight] == ​​0) atCeiling = true;

Aggiorneremo questo controllando se uno qualsiasi dei blocchi di profilo si trova all'interno di un blocco solido. Se qualcuno di loro lo fa, allora il personaggio non può entrare nella posizione e il successore dovrebbe essere saltato.

Controllo dei blocchi superiore e inferiore

Per prima cosa, ripetiamo tutti i blocchi più in alto e più in basso del personaggio e controlliamo se si sovrappongono a una tessera solida nella nostra griglia:

per (var w = 0; w < characterWidth; ++w)  if (mGrid[mNewLocationX + w, mNewLocationY] == 0 || mGrid[mNewLocationX + w, mNewLocationY + characterHeight - 1] == 0) goto CHILDREN_LOOP_END; 

Il CHILDREN_LOOP_END l'etichetta conduce alla fine del ciclo successore; usandolo, saltiamo il bisogno di prima rompere fuori dal giro e poi Continua al prossimo successore nel ciclo successore. 

Quando una tessera in aria può essere considerata "OnGround"

Se uno dei blocchi in basso si trova sopra una tessera solida, il successore deve essere a terra. Ciò significa che, anche se non c'è una tessera solida direttamente sotto la cella successiva, il successore sarà comunque considerato un A terra nodo, se il personaggio è abbastanza largo.

Il nodo rosso è un nodo "OnGround", anche se non è effettivamente a terra.
per (var w = 0; w < characterWidth; ++w)  if (mGrid[mNewLocationX + w, mNewLocationY] == 0 || mGrid[mNewLocationX + w, mNewLocationY + characterHeight - 1] == 0) goto CHILDREN_LOOP_END; if (mMap.IsGround(mNewLocationX + w, mNewLocationY - 1)) onGround = true; 

Verifica se il personaggio è al soffitto

Se una delle tessere sopra il personaggio è solida, il personaggio è al soffitto.

per (var w = 0; w < characterWidth; ++w)  if (mGrid[mNewLocationX + w, mNewLocationY] == 0 || mGrid[mNewLocationX + w, mNewLocationY + characterHeight - 1] == 0) goto CHILDREN_LOOP_END; if (mMap.IsGround(mNewLocationX + w, mNewLocationY - 1)) onGround = true; if (mGrid[mNewLocationX + w, mNewLocationY + characterHeight] == 0) atCeiling = true; 

Controllo dei blocchi ai lati del personaggio

Ora dobbiamo solo controllare che non ci siano blocchi solidi nelle celle sinistra e destra del personaggio. Se ci sono, allora possiamo tranquillamente saltare il successore, perché il nostro personaggio non si adatta a quella particolare posizione:

 per (var h = 1; h < characterHeight - 1; ++h)  if (mGrid[mNewLocationX, mNewLocationY + h] == 0 || mGrid[mNewLocationX + characterWidth - 1, mNewLocationY + h] == 0) goto CHILDREN_LOOP_END; 

Conclusione

Abbiamo rimosso una restrizione piuttosto significativa dall'algoritmo; ora hai molta più libertà in termini di dimensioni dei personaggi del tuo gioco.

Nel prossimo tutorial della serie, useremo il nostro algoritmo di path-path per alimentare un bot in grado di seguire il percorso stesso; basta cliccare su una posizione e funzionerà e salterà per arrivarci. Questo è molto utile per gli NPC!