Movimento del personaggio esagonale usando le coordinate assiali

Cosa starai creando

Nella prima parte della serie, abbiamo esplorato i diversi sistemi di coordinate per i giochi a mattonelle esagonali con l'aiuto di un gioco esagonale di Tetris. Una cosa che potresti aver notato è che stiamo ancora facendo affidamento sulle coordinate di offset per disegnare il livello sullo schermo usando il levelData schieramento. 

Potresti anche essere curioso di sapere come possiamo determinare le coordinate assiali di una tessera esagonale dalle coordinate dei pixel sullo schermo. Il metodo utilizzato nell'esercitazione del dragamine esagonale si basa sulle coordinate di offset e non è una soluzione semplice. Una volta capito, procederemo alla creazione di soluzioni per il movimento dei personaggi esagonali e il pathfinding.

1. Conversione di coordinate tra pixel e assiali

Ciò comporterà un po 'di matematica. Useremo il layout orizzontale per l'intero tutorial. Iniziamo trovando una relazione molto utile tra la larghezza e l'altezza dell'esagono regolare. Si prega di fare riferimento all'immagine qui sotto.

Considera l'esagono regolare blu a sinistra dell'immagine. Sappiamo già che tutti i lati sono di uguale lunghezza. Tutti gli angoli interni sono di 120 gradi ciascuno. Collegando ogni angolo al centro dell'esagono si ottengono sei triangoli, uno dei quali è mostrato utilizzando linee rosse. Questo triangolo ha tutti gli angoli interni uguali a 60 gradi. 

Mentre la linea rossa divide i due angoli nel mezzo, otteniamo 120/2 = 60. Il terzo angolo è 180- (60 + 60) = 60 come la somma di tutti gli angoli all'interno del triangolo dovrebbe essere di 180 gradi. Quindi essenzialmente il triangolo è un triangolo equilatero, che significa inoltre che ogni lato del triangolo ha la stessa lunghezza. Quindi nell'esagono blu le due linee rosse, la linea verde e ogni segmento di linea blu hanno la stessa lunghezza. Dall'immagine, è chiaro che la linea verde è hexTileHeight / 2.

Procedendo verso l'esagono a destra, possiamo vedere che la lunghezza del lato è uguale a hexTileHeight / 2, dovrebbe essere l'altezza della parte triangolare superiore hexTileHeight / 4 e l'altezza della parte triangolare inferiore dovrebbe essere hexTileHeight / 4, che raggiunge l'altezza totale dell'esagono, hexTileHeight

Considerare ora il piccolo triangolo rettangolo in alto a sinistra con un angolo verde e uno blu. L'angolo blu è di 60 gradi poiché è la metà dell'angolo, che a sua volta significa che l'angolo verde è di 30 gradi (180- (60 + 90)). Usando queste informazioni, arriviamo a una relazione tra l'altezza e la larghezza dell'esagono regolare.

tan 30 = lato opposto / lato adiacente; 1 / sqrt (3) = (hexTileHeight / 4) / (hexTileWidth / 2); hexTileWidth = sqrt (3) * hexTileHeight / 2; hexTileHeight = 2 * hexTileWidth / sqrt (3);

Conversione di coordinate assiali in pixel

Prima di avvicinarci alla conversione, rivisitiamo l'immagine del layout esagonale orizzontale in cui abbiamo evidenziato la riga e la colonna in cui una delle coordinate rimane la stessa.

Considerando il valore dello schermo y, possiamo vedere che ogni riga ha un offset di y 3 * hexTileHeight / 4, mentre si scende sulla linea verde, l'unico valore che cambia è io. Quindi, possiamo concludere che il valore del pixel y dipende solo dall'asse assiale io coordinata.

y = (3 * hexTileHeight / 4) * i; y = 3/2 * s * i;

Dove S è la lunghezza del lato, che è stata trovata per essere hexTileHeight / 2.

Il valore dello schermo x è un po 'più complicato di questo. Quando si considerano le tessere all'interno di una singola riga, ciascuna piastrella ha un offset x di hexTileWidth, che chiaramente dipende solo dall'assiale j coordinata. Ma ogni riga alternativa ha un offset aggiuntivo di hexTileWidth / 2 secondo l'assiale io coordinata.

Considerando di nuovo la linea verde, se immaginiamo che fosse una griglia quadrata, la linea sarebbe stata verticale, soddisfacendo l'equazione x = j * hexTileWidth. Come l'unica coordinata che cambia lungo la linea verde è io, l'offset dipenderà da questo. Questo ci porta alla seguente equazione.

x = j * hexTileWidth + (i * hexTileWidth / 2); = j * sqrt (3) * hexTileHeight / 2 + i * sqrt (3) * hexTileHeight / 4; = sqrt (3) * s * (j + (i / 2));

Quindi qui li abbiamo: le equazioni per convertire le coordinate assiali in coordinate dello schermo. La funzione di conversione corrispondente è la seguente.

var rootThree = Math.sqrt (3); var sideLength = hexTileHeight / 2; function axialToScreen (axialPoint) var tileX = rootThree * sideLength * (axialPoint.y + (axialPoint.x / 2)); var tileY = 3 * sideLength / 2 * axialPoint.x; axialPoint.x = Tilex; axialPoint.y = Tiley; return axialPoint; 

Il codice revisionato per disegnare la griglia esagonale è il seguente.

per (var i = 0; i < levelData.length; i++)  for (var j = 0; j < levelData[0].length; j++)  axialPoint.x=i; axialPoint.y=j; axialPoint=offsetToAxial(axialPoint); screenPoint=axialToScreen(axialPoint); if(levelData[i][j]!=-1) hexTile= new HexTileNode(game, screenPoint.x, screenPoint.y, 'hex', false,i,j,levelData[i][j]); hexGrid.add(hexTile);   

Conversione di pixel in coordinate assiali

Invertire queste equazioni con la semplice sostituzione di una variabile ci porterà allo schermo per le equazioni di conversione assiale.

i = y / (3/2 * s); j = (x- (y / sqrt (3))) / s * sqrt (3);

Sebbene le coordinate assiali richieste siano numeri interi, le equazioni genereranno numeri in virgola mobile. Quindi dovremo arrotondarli e applicare alcune correzioni, basandoci sulla nostra equazione principale x + y + z = 0. La funzione di conversione è la seguente.

function screenToAxial (screenPoint) var axialPoint = new Phaser.Point (); axialPoint.x = screenPoint.y / (1,5 * sideLength); axialPoint.y = (screenPoint.x- (screenPoint.y / rootThree)) / (rootThree * sideLength); var cubicZ = calculateCubicZ (axialPoint); var round_x = Math.round (axialPoint.x); var round_y = Math.round (axialPoint.y); var round_z = Math.round (cubicZ); if (round_x + round_y + round_z === 0) screenPoint.x = round_x; screenPoint.y = round_y;  else var delta_x = Math.abs (axialPoint.x-round_x); var delta_y = Math.abs (axialPoint.y-round_y); var delta_z = Math.abs (cubicZ-round_z); if (delta_x> delta_y && delta_x> delta_z) screenPoint.x = -round_y-round_z; screenPoint.y = round_y;  else if (delta_y> delta_x && delta_y> delta_z) screenPoint.x = round_x; screenPoint.y = -round_x-round_z;  else if (delta_z> delta_x && delta_z> delta_y) screenPoint.x = round_x screenPoint.y = round_y;  return screenPoint; 

Controlla l'elemento interattivo, che utilizza questi metodi per visualizzare le tessere e rilevare i tocchi.

2. Movimento del personaggio

Il concetto centrale del movimento dei personaggi in qualsiasi griglia è simile. Noi interrogiamo l'input dell'utente, determiniamo la direzione, troviamo la posizione risultante, controlliamo se la posizione risultante cade all'interno di un muro nella griglia, altrimenti spostiamo il personaggio in quella posizione. Si può fare riferimento al mio tutorial sul movimento dei caratteri isometrici per vederlo in azione rispetto alla conversione delle quote isometriche. 

Le uniche cose che sono diverse qui sono la conversione delle coordinate e le direzioni del movimento. Per una griglia esagonale allineata orizzontalmente, ci sono sei direzioni disponibili per il movimento. Potremmo usare i tasti della tastiera UN, W, E, D, X, e Z per controllare ogni direzione. Il layout di tastiera predefinito corrisponde perfettamente alle direzioni e le funzioni correlate sono le seguenti.

function moveLeft () movementVector.x = movementVector.y = 0; movementVector.x = -1 * velocità; CheckCollisionAndMove ();  function moveRight () movementVector.x = movementVector.y = 0; movementVector.x = velocità; CheckCollisionAndMove ();  function moveTopLeft () movementVector.x = -0.5 * speed; // Cos60 movementVector.y = -0.866 * speed; // sine60 CheckCollisionAndMove ();  function moveTopRight () movementVector.x = 0.5 * speed; // Cos60 movementVector.y = -0.866 * speed; // sine60 CheckCollisionAndMove ();  function moveBottomRight () movementVector.x = 0.5 * speed; // Cos60 movementVector.y = 0.866 * speed; // sine60 CheckCollisionAndMove ();  function moveBottomLeft () movementVector.x = -0.5 * speed; // Cos60 movementVector.y = 0.866 * speed; // sine60 CheckCollisionAndMove (); 

Le direzioni diagonali del movimento formano un angolo di 60 gradi con la direzione orizzontale. Quindi possiamo calcolare direttamente la nuova posizione usando la trigonometria usando Cos 60 e Sine 60. Da questa movementVector, scopriamo la nuova posizione risultante e controlliamo se cade all'interno di un muro nella griglia come di seguito.

function CheckCollisionAndMove () var tempPos = new Phaser.Point (); tempPos.x = hero.x + movementVector.x; tempPos.y = hero.y + movementVector.y; var corner = new Phaser.Point (); // controlla tl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner angolo) () return; // check tr corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner angolo) () return; // controlla bl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner angolo) () return; // controlla br corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner angolo) () return; hero.x = tempPos.x; hero.y = tempPos.y;  function checkCorner (corner) corner = screenToAxial (angolo); angolo = axialToOffset (angolo); if (checkForOccuppancy (corner.x, corner.y)) return true;  return false; 

Aggiungiamo il movementVector al vettore posizione eroe per ottenere la nuova posizione per il centro dello sprite dell'eroe. Quindi troviamo la posizione dei quattro angoli dell'eroe sprite e controlliamo se sono in collisione. Se non ci sono collisioni, impostiamo la nuova posizione sullo sprite dell'eroe. Vediamolo in azione.

Di solito, questo tipo di movimento a flusso libero non è consentito in un gioco basato sulla griglia. In genere, i personaggi si spostano da piastrella a piastrella, ovvero, dal centro della tessera al centro della tessera, in base ai comandi o toccare. Confido che tu possa capire la soluzione da solo.

3. Pathfinding

Quindi eccoci sul tema del pathfinding, un tema molto spaventoso per alcuni. Nelle mie precedenti esercitazioni non ho mai provato a creare nuove soluzioni di pathfinding, ma ho sempre preferito utilizzare soluzioni prontamente disponibili testate in battaglia. 

Questa volta, sto facendo un'eccezione e reinventerò la ruota, principalmente perché ci sono varie meccaniche di gioco possibili e nessuna singola soluzione sarebbe di beneficio a tutti. Quindi è utile sapere come è fatto il tutto al fine di sfornare le tue soluzioni personalizzate per la tua meccanica di gioco. 

L'algoritmo più semplice che viene utilizzato per il rilevamento dei percorsi nelle griglie è Dijkstra's Algorithm. Iniziamo dal primo nodo e calcoliamo i costi necessari per passare a tutti i possibili nodi vicini. Chiudiamo il primo nodo e passiamo al nodo adiacente con il costo più basso coinvolto. Questo viene ripetuto per tutti i nodi non chiusi finché non raggiungiamo la destinazione. Una variante di questo è il Un algoritmo *, dove usiamo anche un euristico oltre al costo. 

Un euristico viene utilizzato per calcolare la distanza approssimativa dal nodo corrente al nodo di destinazione. Poiché non conosciamo veramente il percorso, questo calcolo della distanza è sempre un'approssimazione. Quindi una migliore euristica produrrà sempre un percorso migliore. Ora, detto questo, la soluzione migliore non deve essere quella che fornisce il percorso migliore in quanto dobbiamo considerare l'utilizzo delle risorse e le prestazioni dell'algoritmo, quando tutti i calcoli devono essere fatti in tempo reale o una volta per aggiornamento ciclo continuo. 

L'euristica più semplice e più semplice è la Manhattan euristico o Distanza di Manhattan. In una griglia 2D, questa è in realtà la distanza tra il nodo iniziale e il nodo finale in linea d'aria, o il numero di blocchi che dobbiamo percorrere.

Variante esagonale di Manhattan

Per la nostra griglia esagonale, dobbiamo trovare una variante per l'euristica di Manhattan per approssimare la distanza. Mentre camminiamo sulle tessere esagonali, l'idea è di trovare il numero di tessere che dobbiamo percorrere per raggiungere la destinazione. Lascia che ti mostri prima la soluzione. Si prega di spostare il mouse sopra l'elemento interattivo qui sotto per vedere quanto sono lontane le altre tessere dalla tessera sotto il mouse.

Nell'esempio sopra, troviamo la tessera sotto il mouse e troviamo la distanza di tutte le altre tessere da essa. La logica è trovare la differenza di io e j le coordinate assiali di entrambe le tessere, per prima cosa di e dj. Trova i valori assoluti di queste differenze, Absi e absj, poiché le distanze sono sempre positive. 

Lo notiamo quando entrambi di e dj sono positivi e quando entrambi di e dj sono negativi, la distanza è Absi + absj. quando di e dj sono di segno opposto, la distanza è il valore più grande tra Absi e absj. Questo porta alla funzione di calcolo euristico getHeuristic come sotto.

getHeuristic = function (i, j) j = (j- (Math.floor (i / 2))); var di = i-this.originali; var dj = j-this.convertedj; var si = Math.sign (di); var sj = Math.sign (dj); var absi = di * si; var absj = dj * sj; se (si! = sj) this.heuristic = Math.max (absi, absj);  else this.heuristic = (absi + absj); 

Una cosa da notare è che non stiamo considerando se il percorso sia realmente percorribile o meno; assumiamo semplicemente che sia percorribile e imposta il valore della distanza. 

Trovare il percorso esagonale

Procediamo con il rilevamento dei percorsi per la nostra griglia esagonale con il metodo euristico appena trovato. Dato che useremo la ricorsione, sarà più facile capire una volta interrotta la logica fondamentale del nostro approccio. Ogni tessera esagonale avrà una distanza euristica e un valore di costo associato ad essa.

  • Abbiamo una funzione ricorsiva, diciamo findPath (piastrelle), che contiene una tessera esagonale, che è la tessera corrente. Inizialmente questa sarà la tessera iniziale.
  • Se la tessera è uguale alla tessera finale, la ricorsione termina e abbiamo trovato il percorso. Altrimenti procediamo con il calcolo.
  • Troviamo tutti i vicini percorribili della tessera. Effettueremo un ciclo di tutte le tessere vicine e applicheremo ulteriore logica a ciascuna di esse, a meno che non lo siano chiuso.
  • Se un vicino non è in precedenza visitato e non chiuso, troviamo la distanza della tessera vicina alla tessera finale usando la nostra euristica. Impostiamo le tessere del vicino costo a costo del piastrellista corrente + 10. Impostiamo la tessera del vicino come visitato. Impostiamo le tessere del vicino tessera precedente come il riquadro corrente. Lo facciamo anche per un vicino visitato in precedenza se il costo del riquadro corrente + 10 è inferiore al costo di quel vicino.
  • Calcoliamo il costo totale come somma del valore di costo della piastrella adiacente e del valore di distanza euristica. Tra tutti i vicini, selezioniamo il vicino che dà il costo totale più basso e chiama findPath su quella tessera vicina.
  • Impostiamo il riquadro corrente su chiuso in modo che non venga più considerato.
  • In alcuni casi, non riusciamo a trovare nessuna tessera che soddisfi le condizioni, quindi chiudiamo la tessera corrente, apriamo la tessera precedente e ripetiamo.

C'è una evidente condizione di fallimento nella logica quando più di una tessera soddisfa le condizioni. Un algoritmo migliore troverà tutti i diversi percorsi e selezionerà quello con la lunghezza più breve, ma non lo faremo qui. Controlla il path-finding in azione qui sotto.

Per questo esempio, sto calcolando i vicini in modo diverso rispetto all'esempio Tetris. Quando si usano le coordinate assiali, le tessere vicine hanno coordinate che sono superiori o inferiori di un valore di 1.

function getNeighbors (i, j) // coordinate sono in axial var tempArray = []; var axialPoint = new Phaser.Point (i, j); var neighbourPoint = new Phaser.Point (); neighbourPoint.x = axialPoint.x-1; // tr neighbourPoint.y = axialPoint.y; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x + 1; // bl neighbourPoint.y = axialPoint.y; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x; // l neighbourPoint.y = axialPoint.y-1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x; // r neighbourPoint.y = axialPoint.y + 1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x-1; // tr neighbourPoint.y = axialPoint.y + 1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x + 1; // bl neighbourPoint.y = axialPoint.y-1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); return tempArray; 

Il findPath la funzione ricorsiva è come sotto.

function findPath (tile) // passa in un hexTileNode if (Phaser.Point.equals (tile, endTile)) // success, destination ha raggiunto console.log ('success'); // ora dipingi il percorso. paintPath (piastrelle);  else // trova tutti i vicini vicini var = getNeighbors (tile.originali, tile.convertedj); var newPt = new Phaser.Point (); var hexTile; var totalCost = 0; var currentLowestCost = 100000; var nextTile; // trova l'euristica e il costo per tutti i vicini mentre (neighbors.length) newPt = neighbors.shift (); hexTile = hexGrid.getByName ( "tile" + newPt.x + "_" + newPt.y); if (! hexTile.nodeClosed) // se il nodo non era già calcolato se ((hexTile.nodeVisited && (tile.cost + 10)

Potrebbe richiedere ulteriori e molteplici letture per capire correttamente cosa sta succedendo, ma credimi, ne vale la pena. Questa è solo una soluzione molto semplice e potrebbe essere migliorata molto. Per spostare il personaggio lungo il percorso calcolato, puoi fare riferimento al mio percorso isometrico seguendo il tutorial. 

La marcatura del percorso viene eseguita utilizzando un'altra semplice funzione ricorsiva, paintPath (piastrelle), che viene prima chiamato con la tessera finale. Abbiamo appena segnato il previousNode della piastrella se presente.

function paintPath (tile) tile.markDirty (); if (tile.previousNode! == null) paintPath (tile.previousNode); 

Conclusione

Con l'aiuto di tutti e tre i tutorial esagonali che ho condiviso, dovresti essere in grado di iniziare il tuo prossimo fantastico gioco a mattonelle esagonali. 

Tieni presente che ci sono anche altri approcci e ci sono molte altre letture là fuori, se sei pronto per questo. Per favore fatemi sapere attraverso i commenti se avete bisogno di qualcosa di più da esplorare in relazione ai giochi a mattonelle esagonali.