In questo tutorial, cercherò di introdurre il mondo interessante dei giochi a mattonelle esagonali usando l'approccio più semplice. Imparerai come convertire i dati di una matrice bidimensionale in un layout di livello esagonale corrispondente sullo schermo e viceversa. Utilizzando le informazioni ottenute, creeremo un gioco di dragamine esagonale in due diversi layout esagonali.
Questo ti consentirà di iniziare ad esplorare semplici giochi da tavolo esagonali e giochi di puzzle e sarà un buon punto di partenza per apprendere approcci più complessi come i sistemi di coordinate esagonali assiali o cubici.
Nell'attuale generazione di giochi casuali, non vediamo molti giochi che utilizzano un approccio basato su tessere esagonali. Quelle che incontriamo sono solitamente giochi di puzzle, giochi da tavolo o giochi di strategia. Inoltre, molti dei nostri requisiti sono soddisfatti dall'approccio a griglia quadrata o approccio isometrico. Questo porta alla domanda naturale: "Perché abbiamo bisogno di un approccio esagonale diverso e ovviamente complicato?" Scopriamolo.
Quindi, che cosa rende rilevante l'approccio esagonale basato su tile, dal momento che abbiamo già appropriatamente appresi e perfezionati altri approcci? Lasciatemi elencare alcune delle ragioni.
Direi che l'ultima ragione dovrebbe essere sufficiente per padroneggiare questo nuovo approccio. Aggiungere quell'elemento di gioco unico alla logica del gioco potrebbe fare la differenza e permettervi di fare un grande gioco.
Gli altri motivi sono puramente tecnici e entrerebbero in vigore solo dopo aver affrontato complicati algoritmi o set di tessere più grandi. Ci sono molti altri aspetti che possono essere elencati come vantaggi dell'approccio esagonale, ma la maggior parte di essi dipenderà dall'interesse personale del giocatore.
Un esagono è un poligono con sei lati e un esagono con tutti i lati della stessa lunghezza è chiamato esagono regolare. A fini teorici, considereremo le nostre tessere esagonali come esagoni regolari, ma potrebbero essere schiacciati o allungati nella pratica.
La cosa interessante è che un esagono può essere posizionato in due modi diversi: gli angoli appuntiti potrebbero essere allineati verticalmente o orizzontalmente. Quando le cime appuntite sono allineate verticalmente, si chiama a orizzontale layout, e quando sono allineati orizzontalmente, è chiamato a verticale disposizione. Potresti pensare che i nomi siano fuorvianti rispetto alla spiegazione fornita. Questo non è il caso in quanto la denominazione non viene eseguita in base agli angoli appuntiti, ma come viene presentata una griglia di tessere. L'immagine sotto mostra i diversi allineamenti delle tessere e i layout corrispondenti.
La scelta del layout dipende interamente dalla grafica e dal gameplay del tuo gioco. Tuttavia la tua scelta non finisce qui, poiché ciascuno di questi layout può essere implementato in due modi diversi.
Consideriamo un layout di griglia esagonale orizzontale. Le righe alternative della griglia dovrebbero essere compensate orizzontalmente da hexTileWidth / 2
. Ciò significa che potremmo scegliere di compensare le righe dispari o le righe pari. Se mostriamo anche il corrispondente riga, colonna valori, queste varianti apparirebbero come l'immagine qui sotto.
Allo stesso modo, il layout verticale potrebbe essere implementato in due varianti, mentre compensando le colonne alternative di hexTileHeight / 2
come mostrato di seguito.
Da qui in poi, inizia a fare riferimento al codice sorgente fornito insieme a questo tutorial per una migliore comprensione.
Le immagini sopra, con le righe e le colonne visualizzate, rendono più facile visualizzare una correlazione diretta con una matrice bidimensionale che memorizza i dati di livello. Diciamo che abbiamo un semplice array bidimensionale levelData
come sotto.
var levelData = [[0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0 ], [0,0,0,0,0]]
Per rendere più semplice la visualizzazione, mostrerò qui il risultato desiderato in entrambe le varianti verticale e orizzontale.
Iniziamo con il layout orizzontale, che è l'immagine sul lato sinistro. In ogni riga, se prese singolarmente, le tessere vicine sono sfalsate orizzontalmente di hexTileWidth
. Le righe alternative sono orizzontalmente sfalsate di un valore di hexTileWidth / 2
. La differenza di altezza verticale tra ogni riga è hexTileHeight * 3/4
.
Per capire come siamo arrivati a un tale valore per l'offset in altezza, dobbiamo considerare il fatto che le porzioni triangolari superiore e inferiore di un esagono orizzontale sono esattamente hexTileHeight / 4
.
Ciò significa che l'esagono ha un rettangolo hexTileHeight / 2
porzione nel mezzo, un triangolare hexTileHeight / 4
porzione in alto, e un triangolare invertito hexTileHeight / 4
porzione sul fondo. Questa informazione è sufficiente per creare il codice necessario per disporre la griglia esagonale sullo schermo.
var verticalOffset = hexTileHeight * 3/4; var horizontalOffset = hexTileWidth; var startX; var startY; var startXInit = hexTileWidth / 2; var startYInit = hexTileHeight / 2; var hexTile; per (var i = 0; i < levelData.length; i++) if(i%2!==0) startX=2*startXInit; else startX=startXInit; startY=startYInit+(i*verticalOffset); for (var j = 0; j < levelData[0].length; j++) if(levelData[i][j]!=-1) hexTile= new HexTile(game, startX, startY, 'hex',false,i,j,levelData[i][j]); hexGrid.add(hexTile); startX+=horizontalOffset;
Con il HexTile
prototipo, ho aggiunto alcune funzionalità aggiuntive al Phaser.Sprite
prototipo che consente di visualizzare il io
e j
valori. Il codice pone essenzialmente una nuova tessera esagonale folletto
a startx
e starty
. Questo codice può essere modificato per visualizzare la variante di offset uniforme semplicemente rimuovendo un operatore nel Se
condizione come questa: if (i% 2 === 0)
.
Per un layout verticale (l'immagine sulla metà destra), i riquadri adiacenti in ogni colonna sono spostati verticalmente di hexTileHeight
. Ogni colonna alternativa viene spostata verticalmente di hexTileHeight / 2
. Applicando la logica che abbiamo applicato per l'offset verticale per il layout orizzontale, possiamo vedere che l'offset orizzontale per il layout verticale tra le tessere vicine in una riga è hexTileWidth * 3/4
. Il codice corrispondente è sotto.
var verticalOffset = hexTileHeight; var horizontalOffset = hexTileWidth * 3/4; var startX; var startY; var startXInit = hexTileWidth / 2; var startYInit = hexTileHeight / 2; var hexTile; per (var i = 0; i < levelData.length; i++) startX=startXInit; startY=2*startYInit+(i*verticalOffset); for (var j = 0; j < levelData[0].length; j++) if(j%2!==0) startY=startY+startYInit; else startY=startY-startYInit; if(levelData[i][j]!=-1) hexTile= new HexTile(game, startX, startY, 'hex', true,i,j,levelData[i][j]); hexGrid.add(hexTile); startX+=horizontalOffset;
Allo stesso modo con il layout orizzontale, possiamo passare alla variante di offset pari semplicemente rimuovendo il !
operatore nella parte superiore Se
condizione. Sto usando un phaser Gruppo
per raccogliere tutto il hexTiles
di nome hexGrid
. Per semplicità, sto usando il punto centrale dell'immagine della piastrella esagonale come un'ancora, altrimenti dovremmo considerare anche le correzioni dell'immagine.
Una cosa da notare è che i valori di larghezza della piastrella e altezza della piastrella nel layout orizzontale non sono uguali ai valori di larghezza della piastrella e altezza della piastrella nel layout verticale. Ma quando si utilizza la stessa immagine per entrambi i layout, potremmo semplicemente ruotare l'immagine della piastrella di 90 gradi e scambiare i valori della larghezza della piastrella e dell'altezza delle piastrelle.
La matrice per schermare la logica di posizionamento era interessante, ma il contrario non è così facile. Considera che dobbiamo trovare l'indice della matrice della tessera esagonale su cui abbiamo toccato. Il codice per raggiungere questo obiettivo non è bello, e in genere viene fornito con prove ed errori.
Se consideriamo il layout orizzontale, può sembrare che la porzione rettangolare centrale della tessera esagonale possa facilmente aiutarci a capire j
valore in quanto è solo questione di dividere il X
valore di hexTileWidth
e prendendo il valore intero. Ma a meno che non conosciamo il io
valore, non sappiamo se siamo su una fila dispari o pari. Un valore approssimativo di io
può essere trovato dividendo il valore y di hexTileHeight * 3/4
.
Ora vengono le parti complicate della tessera esagonale: le parti triangolari superiore e inferiore. L'immagine qui sotto ci aiuterà a capire il problema in questione.
Le regioni 2, 3, 5, 6, 8 e 9 formano insieme una tessera. La parte più complicata è scoprire se la posizione TAPpata è in 1/2 o 3/4 o 7/8 o 9/10. Per questo, dobbiamo considerare tutte le singole regioni triangolari e controllarle contro usando la pendenza del bordo inclinato.
Questa pendenza può essere trovata dall'altezza e dalla larghezza di ciascuna regione triangolare, che sono rispettivamente hexTileHeight / 4
e hexTileWidth / 2
. Lascia che ti mostri la funzione che fa questo.
function findHexTile () var pos = game.input.activePointer.position; pos.x- = hexGrid.x; pos.y- = hexGrid.y; var xVal = Math.floor ((pos.x) / hexTileWidth); var yVal = Math.floor ((pos.y) / (hexTileHeight * 3/4)); var dX = (pos.x)% hexTileWidth; var dY = (pos.y)% (hexTileHeight * 3/4); var slope = (hexTileHeight / 4) / (hexTileWidth / 2); var caldY = dX * slope; var delta = hexTileHeight / 4-caldY; if (yVal% 2 === 0) // la correzione deve avvenire in porzioni triangolari e le righe sfalsate if (Math.abs (delta)> dY) if (delta> 0) // dispari riga in basso a destra xVal--; yVal--; else // dispari riga in basso a sinistra yVal--; else if (dX> hexTileWidth / 2) // i valori disponibili non funzionano per la riga pari in basso a destra se (dY<((hexTileHeight/2)-caldY))//even row bottom right half yVal--; else if(dY>caldY) // dispari riga in alto a destra e metà destra xVal--; else // even row in basso a sinistra half yVal--; pos.x = yVal; pos.y = xVal; ritorno pos;
Per prima cosa, troviamo xVal
e yval
allo stesso modo che faremmo per una griglia quadrata. Quindi troviamo il restante orizzontale (dX
) e verticale (dY
) valori dopo aver rimosso l'offset del moltiplicatore di tile. Usando questi valori, cerchiamo di capire se il punto si trova all'interno di una qualsiasi delle complicate regioni triangolari.
Se trovato, apportiamo le modifiche corrispondenti ai valori iniziali di xVal
e yval
. Come ho detto prima, il codice non è bello e non semplice. Il modo più semplice per capire questo sarebbe chiamare findHexTile
muoviti con il mouse e poi metti console.log
all'interno di ciascuna di queste condizioni e sposta il mouse su varie regioni all'interno di una tessera esagonale. In questo modo, puoi vedere come viene gestita ogni regione intra-esagonale.
Le modifiche al codice per il layout verticale sono mostrate di seguito.
function findHexTile () var pos = game.input.activePointer.position; pos.x- = hexGrid.x; pos.y- = hexGrid.y; var xVal = Math.floor ((pos.x) / (hexTileWidth * 3/4)); var yVal = Math.floor ((pos.y) / (hexTileHeight)); var dX = (pos.x)% (hexTileWidth * 3/4); var dY = (pos.y)% (hexTileHeight); var slope = (hexTileHeight / 2) / (hexTileWidth / 4); var caldX = dY / slope; var delta = hexTileWidth / 4-caldX; if (xVal% 2 === 0) if (dX> Math.abs (delta)) // left left else // odd right if (delta> 0) // dispari right bottom bottom xVal--; yVal--; else // odd right top xVal--; else if (delta> 0) if (dX4. Trovare i vicini
Ora che abbiamo trovato la tessera su cui abbiamo tappato, troviamo tutte e sei le tessere vicine. Questo è un problema molto facile da risolvere una volta che analizziamo visivamente la griglia. Consideriamo il layout orizzontale.
L'immagine sopra mostra il dispari e pari righe di una griglia esagonale disposta orizzontalmente quando una tessera intermedia ha il valore di
0
per entrambiio
ej
. Dall'immagine, diventa chiaro che se la riga è dispari, quindi per una tessera ai, j
i vicini sonoio, j-1
,i-1, j-1
,i-1, j
,i, j + 1
,i + 1, j
, ei + 1, j-1
. Quando la riga è pari, quindi per una tessera ai, j
i vicini sonoio, j-1
,i-1, j
,i-1, j + 1
,i, j + 1
,i + 1, j + 1
, ei + 1, j
. Questo potrebbe essere calcolato manualmente facilmente.Analizziamo un'immagine simile per i dispari e pari colonne di una griglia esagonale allineata verticalmente.
Quando abbiamo una colonna dispari, una tessera a
i, j
avrài, j-1
,i-1, j-1
,i-1, j
,i-1, j + 1
,i, j + 1
, ei + 1, j
come vicini. Allo stesso modo, per una colonna pari, i vicini sonoi + 1, j-1
,i, j-1
,i-1, j
,i, j + 1
,i + 1, j + 1
, ei + 1, j
.5. Minesweeper esagonale
Con la conoscenza di cui sopra, possiamo provare a realizzare un gioco di dragamine esagonale nei due diversi layout. Analizziamo le caratteristiche di un gioco di dragamine.
- Ci sarà il numero N di mine nascoste all'interno della griglia.
- Se tocchiamo una tessera con una mina, il gioco è finito.
- Se tocchiamo una tessera che ha una miniera vicina, mostrerà immediatamente il numero di mine attorno ad essa.
- Se tocchiamo una miniera senza mine vicine, porterei alla rivelazione di tutte le tessere collegate che non hanno mine.
- Possiamo toccare e tenere premuto per contrassegnare una tessera come una mina.
- Il gioco è finito quando riveliamo tutte le tessere senza mine.
Possiamo facilmente memorizzare un valore nel
levelData
array per indicare una miniera. Lo stesso metodo può essere usato per popolare il valore delle mine vicine sull'indice dell'array delle tessere vicine.All'inizio del gioco, compileremo a caso il file
levelData
array con numero N di mine. Dopo questo, aggiorneremo i valori per tutte le tessere vicine. Useremo un metodo ricorsivo per mettere in sequenza tutte le tessere bianche collegate quando il giocatore tocca una tessera che non ha una mina come vicino.Dati di livello
Dobbiamo creare una griglia esagonale dall'aspetto piacevole, come mostrato nell'immagine qui sotto.
Questo può essere fatto visualizzando solo una parte del
levelData
array. Se usiamo-1
come valore per una tessera non utilizzabile e0
come valore per una piastrella utilizzabile, quindi la nostralevelData
per ottenere il risultato sopra sembrerà così.// livello a forma di piastrella orizzontale var levelData = [[-1, -1, -1,0,0,0,0,0,0,0, -1, -1, -1], [-1, -1 , 0,0,0,0,0,0,0,0,0, -1, -1, -1], [-1, -1,0,0,0,0,0,0,0,0,0, 0, -1, -1], [-1,0,0,0,0,0,0,0,0,0,0, -1, -1], [-1,0,0,0, 0,0,0,0,0,0,0,0, -1], [0,0,0,0,0,0,0,0,0,0,0,0,0, -1], [ 0,0,0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0,0, 0, -1], [-1,0,0,0,0,0,0,0,0,0,0,0, -1], [-1,0,0,0,0,0, 0,0,0,0,0, -1, -1], [-1, -1,0,0,0,0,0,0,0,0,0,0, -1, -1], [ -1, -1,0,0,0,0,0,0,0,0,0, -1, -1, -1], [-1, -1, -1,0,0,0,0, 0,0,0, -1, -1, -1]];Durante il looping della matrice, aggiungeremo solo tessere esagonali quando
levelData
ha un valore di0
. Per l'allineamento verticale, lo stessolevelData
può essere usato, ma avremmo bisogno di trasporre l'array. Ecco un metodo elegante che può farlo per te.levelData = trasposizione (levelData); // ... function transpose (a) return Object.keys (a [0]). Map (function (c) return a.map (function (r) return r [c];););Aggiunta di mine e aggiornamento dei vicini
Di default, il nostro
levelData
ha solo due valori,-1
e0
, di cui useremmo solo l'area con0
. Per indicare che una tessera contiene una mina, possiamo usare il valore di10
.Una tessera esagonale vuota può avere un massimo di sei mine vicino perché ha sei tessere vicine. Possiamo memorizzare queste informazioni anche nel
levelData
una volta che abbiamo aggiunto tutte le miniere. In sostanza, alevelData
indice con un valore di10
ha una mina e se contiene valori da0
a6
, che indica il numero di miniere vicine. Dopo aver popolato le mine e aggiornato i vicini, se un elemento dell'array è ancora0
, indica che è una tessera vuota senza mine vicine.Possiamo usare i seguenti metodi per i nostri scopi.
function addMines () var tileType = 0; var tempArray = []; var newPt = new Phaser.Point (); per (var i = 0; i < levelData.length; i++) for (var j = 0; j < levelData[0].length; j++) tileType=levelData[i][j]; if(tileType===0) newPt=new Phaser.Point(); newPt.x=i; newPt.y=j; tempArray.push(newPt); for (var i = 0; i < numMines; i++) newPt=Phaser.ArrayUtils.removeRandomItem(tempArray); levelData[newPt.x][newPt.y]=10;//10 is mine updateNeighbors(newPt.x,newPt.y); function updateNeighbors(i,j)//update neighbors around this mine var tileType=0; var tempArray=getNeighbors(i,j); var tmpPt; for (var k = 0; k < tempArray.length; k++) tmpPt=tempArray[k]; tileType=levelData[tmpPt.x][tmpPt.y]; levelData[tmpPt.x][tmpPt.y]=tileType+1;Per ogni miniera aggiunta
addMines
, stiamo incrementando il valore dell'array memorizzato in tutti i suoi vicini. IlgetNeighbors
il metodo non restituirà una tessera che si trova al di fuori della nostra area effettiva o se contiene una mina.Tocca logica
Quando il giocatore tocca una tessera, dobbiamo trovare l'elemento dell'array corrispondente usando il
findHexTile
metodo spiegato in precedenza. Se l'indice delle tessere si trova all'interno della nostra area effettiva, allora confrontiamo semplicemente il valore nell'indice dell'array per trovare se è una tessera miniera o vuota.function onTap () var tile = findHexTile (); if (! checkforBoundary (tile.x, tile.y)) if (checkForOccuppancy (tile.x, tile.y)) if (levelData [tile.x] [tile.y] == 10) // console log ( 'boom'); var hexTile = hexGrid.getByName ("tile" + tile.x + "_" + tile.y); if (! hexTile.revealed) hexTile.reveal (); // game over else var hexTile = hexGrid.getByName ("tile" + tile.x + "_" + tile.y); if (! hexTile.revealed) if (levelData [tile.x] [tile.y] === 0) //console.log('recursive reveal '); recursiveReveal (tile.x, tile.y); else //console.log('reveal '); hexTile.reveal (); revealedTiles ++; infoTxt.text = 'found' + revealTiles + 'of' + blankTiles;Teniamo traccia del numero totale di tessere vuote usando la variabile
blankTiles
e il numero di tessere rivelato usandorevealedTiles
. Una volta che sono uguali, abbiamo vinto la partita.Quando tocchiamo una tessera con un valore di matrice di
0
, dobbiamo rivelare ricorsivamente la regione con tutte le tessere bianche collegate. Questo è fatto dalla funzionerecursiveReveal
, che riceve gli indici delle tessere della tessera tappata.function recursiveReveal (i, j) var newPt = new Phaser.Point (i, j); var hexTile; var tempArray = [newPt]; vari vicini; while (tempArray.length) newPt = tempArray [0]; var neighbors = getNeighbors (newPt.x, newPt.y); while (neighbors.length) newPt = neighbors.shift (); hexTile = hexGrid.getByName ( "tile" + newPt.x + "_" + newPt.y); if (! hexTile.revealed) hexTile.reveal (); revealedTiles ++; if (levelData [newPt.x] [newPt.y] === 0) tempArray.push (newPt); newPt = tempArray.shift (); // sembrava che un punto senza un vicino a volte sfuggisse all'iterazione senza essere rivelato, catturalo qui hexTile = hexGrid.getByName ("tile" + newPt.x + "_" + newPt.y ); if (! hexTile.revealed) hexTile.reveal (); revealedTiles ++;In questa funzione, troviamo i vicini di ciascuna tessera e rivelano il valore della tessera, nel frattempo aggiungendo le tessere vicine ad una matrice. Continuiamo a ripeterlo con il prossimo elemento dell'array finché l'array non sarà vuoto. La ricorsione si interrompe quando incontriamo elementi di matrice contenenti una mina, il che è garantito dal fatto che
getNeighbors
non restituirà una tessera con una mina.Piastrelle marcanti e rivelatrici
Devi aver notato che sto usando
hexTile.reveal ()
, che è reso possibile dalla creazione di unHexTile
prototipo che mantiene la maggior parte degli attributi relativi alla nostra tessera esagonale. Io uso ilsvelare
funzione per visualizzare il testo del valore della tessera e impostare il colore della piastrella. Allo stesso modo, iltoggleMark
la funzione è usata per contrassegnare la tessera come una mina quando tocchiamo e ti tocchiamo.HexTile
ha anche unrivelato
attributo che tiene traccia se è TAPpato e rivelato o meno.HexTile.prototype.reveal = function () this.tileTag.visible = true; this.revealed = true; if (this.type == 10) this.tint = '0xcc0000'; else this.tint = '0x00cc00'; HexTile.prototype.toggleMark = function () if (this.marked) this.marked = false; this.tint = '0xffffff'; else this.marked = true; this.tint = '0x0000cc';Controlla il dragamine esagonale con l'orientamento orizzontale sotto. Tocca per rivelare le tessere e tocca e tieni premuto per contrassegnare le mine. Non c'è gioco fin qui, ma se ne rivela un valore
10
, allora lo è hasta la vista baby!Modifiche per la versione verticale
Mentre sto usando la stessa immagine della tessera esagonale per entrambi gli orientamenti, ruoto la Sprite per l'allineamento verticale. Il codice qui sotto nel
HexTile
il prototipo fa questo.if (isVertical) this.rotation = Math.PI / 2;La logica del dragamine rimane la stessa per la griglia esagonale allineata verticalmente con la differenza per
findHextile
egetNeighbors
logica che ora deve adeguarsi alla differenza di allineamento. Come accennato in precedenza, dobbiamo anche utilizzare la trasposizione dell'array di livelli con il loop di layout corrispondente.Controlla la versione verticale qui sotto.
Il resto del codice nella fonte è semplice e diretto. Vorrei provare a aggiungere il riavvio mancante, la vittoria del gioco e la funzionalità di game over.
Conclusione
Questo approccio di un gioco a mattonelle esagonali che utilizza un array bidimensionale è più un approccio di un laico. Approcci più interessanti e funzionali implicano l'alterazione del sistema di coordinate con tipi diversi usando equazioni.
I più importanti sono le coordinate assiali e le coordinate cubiche. Ci sarà una serie di tutorial di follow-up che discuteranno questi approcci. Nel frattempo, consiglierei di leggere l'articolo incredibilmente completo di Amit sulle griglie esagonali.