Dans ce tutoriel, je vais essayer d’introduire le monde intéressant des jeux hexagonaux basés sur des tuiles en utilisant la plus simple des approches. Vous apprendrez à convertir les données d'un tableau bidimensionnel en une disposition de niveau hexagonal correspondante à l'écran et inversement. En utilisant les informations obtenues, nous allons créer un jeu de dragueur de mines hexagonal dans deux configurations hexagonales différentes..
Cela vous permettra de commencer à explorer de simples jeux de société hexagonaux et de jeux de réflexion et constituera un bon point de départ pour apprendre des approches plus complexes, telles que les systèmes de coordonnées hexagonaux axiaux ou cubiques..
Dans la génération actuelle de jeux occasionnels, nous ne voyons pas beaucoup de jeux utilisant une approche hexagonale basée sur les tuiles. Ceux que nous rencontrons sont généralement des jeux de réflexion, des jeux de société ou des jeux de stratégie. En outre, la plupart de nos besoins sont satisfaits par l’approche par grille carrée ou par approche isométrique. Cela conduit à la question naturelle: "Pourquoi avons-nous besoin d'une approche hexagonale différente et évidemment compliquée?" Découvrons-le.
En quoi l’approche hexagonale basée sur les carreaux est-elle pertinente, puisque d’autres approches ont déjà été apprises et perfectionnées? Laissez-moi énumérer certaines des raisons.
Je dirais que la dernière raison devrait être suffisante pour que vous maîtrisiez cette nouvelle approche. Ajouter cet élément de jeu unique à votre logique de jeu peut faire toute la différence et vous permettre de faire un grand jeu..
Les autres raisons sont purement techniques et n'entrent en vigueur que lorsque vous utilisez des algorithmes complexes ou des ensembles de pavés plus volumineux. Il existe de nombreux autres aspects qui peuvent également être énumérés comme des avantages de l'approche hexagonale, mais la plupart dépendront de l'intérêt personnel du joueur..
Un hexagone est un polygone à six côtés et un hexagone dont tous les côtés ont la même longueur est appelé un hexagone régulier. Pour des raisons théoriques, nous considérerons nos carreaux hexagonaux comme des hexagones réguliers, mais ils pourraient être écrasés ou allongés dans la pratique..
La chose intéressante est qu’un hexagone peut être placé de deux manières différentes: les coins pointus peuvent être alignés verticalement ou horizontalement. Lorsque les pointes sont alignées verticalement, on parle de horizontal mise en page, et quand ils sont alignés horizontalement, on l'appelle un verticale disposition. Vous pensez peut-être que les noms sont des noms erronés par rapport à l'explication fournie. Ce n'est pas le cas car la dénomination n'est pas basée sur les coins pointus mais sur la manière dont une grille de tuiles est disposée. L'image ci-dessous montre les différents alignements de tuiles et les dispositions correspondantes.
Le choix de la disposition dépend entièrement des visuels et du gameplay de votre jeu. Cependant, votre choix ne s'arrête pas là, car chacune de ces dispositions peut être mise en œuvre de deux manières différentes..
Considérons une disposition de grille hexagonale horizontale. Les lignes alternatives de la grille doivent être décalées horizontalement de hexTileWidth / 2
. Cela signifie que nous pourrions choisir de décaler les lignes impaires ou les lignes paires. Si nous affichons également le correspondant rangée, colonne valeurs, ces variantes ressembleraient à l'image ci-dessous.
De même, la disposition verticale pourrait être mise en œuvre en deux variantes tout en compensant les colonnes alternatives par hexTileHeight / 2
comme indiqué ci-dessous.
A partir de là, commencez à vous référer au code source fourni avec ce tutoriel pour une meilleure compréhension..
Les images ci-dessus, avec les lignes et les colonnes affichées, facilitent la visualisation d'une corrélation directe avec un tableau à deux dimensions qui stocke les données de niveau. Disons que nous avons un tableau simple à deux dimensions levelData
comme ci-dessous.
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]]
Pour faciliter la visualisation, je vais montrer ici le résultat souhaité sous forme de variations verticales et horizontales..
Commençons par la disposition horizontale, qui est l'image sur le côté gauche. Dans chaque ligne, pris individuellement, les tuiles voisines sont décalées horizontalement de hexTileWidth
. Les lignes alternatives sont décalées horizontalement d’une valeur de hexTileWidth / 2
. La différence de hauteur verticale entre chaque rangée est hexTileHeight * 3/4
.
Pour comprendre comment nous sommes arrivés à une telle valeur pour le décalage de hauteur, nous devons prendre en compte le fait que les parties triangulaires supérieure et inférieure d'un hexagone disposé horizontalement sont exactement hexTileHeight / 4
.
Cela signifie que l'hexagone a un rectangle hexTileHeight / 2
partie au milieu, un triangle hexTileHeight / 4
partie sur le dessus, et un triangle inversé hexTileHeight / 4
partie sur le fond. Cette information est suffisante pour créer le code nécessaire pour tracer la grille hexagonale à l'écran.
var verticalOffset = hexTileHeight * 3/4; var horizontalOffset = hexTileWidth; var startX; var startY; var startXInit = hexTileWidth / 2; var startYInit = hexTileHeight / 2; var hexTile; pour (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;
Avec le HexTile
prototype, j’ai ajouté quelques fonctionnalités supplémentaires au Phaser.Sprite
prototype qui lui permet d'afficher la je
et j
valeurs. Le code place essentiellement une nouvelle tuile hexagonale Lutin
à startX
et début
. Ce code peut être modifié pour afficher la variante même offset en supprimant simplement un opérateur dans la liste. si
condition comme ceci: si (i% 2 === 0)
.
Pour une disposition verticale (l’image sur la moitié droite), les tuiles voisines de chaque colonne sont décalées verticalement de hexTileHeight
. Chaque colonne de remplacement est décalée verticalement de hexTileHeight / 2
. En appliquant la logique que nous avons appliquée pour le décalage vertical pour la disposition horizontale, nous pouvons voir que le décalage horizontal pour la disposition verticale entre les tuiles voisines d'une ligne est hexTileWidth * 3/4
. Le code correspondant est ci-dessous.
var verticalOffset = hexTileHeight; var horizontalOffset = hexTileWidth * 3/4; var startX; var startY; var startXInit = hexTileWidth / 2; var startYInit = hexTileHeight / 2; var hexTile; pour (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;
De la même manière que pour la disposition horizontale, nous pouvons passer à la variante même offset en supprimant simplement la !
opérateur en haut si
état. J'utilise un Phaser Groupe
pour collecter tous les hexTiles
nommé hexGrid
. Pour des raisons de simplicité, j'utilise le point central de l'image hexagonale en mosaïque comme point d'ancrage. Sinon, nous devrons également tenir compte des décalages d'image..
Une chose à noter est que les valeurs de largeur et de hauteur de mosaïque dans la disposition horizontale ne sont pas égales à celles de largeur et de hauteur de mosaïque dans la disposition verticale. Mais si vous utilisez la même image pour les deux mises en page, il vous suffit de faire pivoter l'image de mosaïque de 90 degrés et d'échanger les valeurs de largeur et de hauteur de mosaïque..
La logique de placement de l'écran à l'écran était simple, mais l'inverse n'est pas si facile. Considérez que nous devons trouver l’indice de tableau de la tuile hexagonale sur laquelle nous avons tapé. Le code pour y parvenir n’est pas beau et on y parvient généralement par essais et erreurs..
Si nous considérons la disposition horizontale, il peut sembler que la partie rectangulaire centrale de la tuile hexagonale peut facilement nous aider à comprendre la j
comme il s’agit simplement de diviser le X
valeur par hexTileWidth
et en prenant la valeur entière. Mais à moins de connaître le je
valeur, nous ne savons pas si nous sommes sur une ligne impaire ou paire. Une valeur approximative de je
peut être trouvé en divisant la valeur y par hexTileHeight * 3/4
.
Viennent maintenant les parties compliquées du carreau hexagonal: les parties triangulaires supérieure et inférieure. L'image ci-dessous nous aidera à comprendre le problème.
Les régions 2, 3, 5, 6, 8 et 9 forment ensemble une tuile. La partie la plus compliquée consiste à déterminer si la position taraudée est en 1/2 ou 3/4 ou 7/8 ou 9/10. Pour cela, nous devons examiner toutes les régions triangulaires individuelles et les comparer en utilisant la pente du bord incliné..
Cette pente peut être trouvée à partir de la hauteur et de la largeur de chaque région triangulaire, qui sont respectivement hexTileHeight / 4
et hexTileWidth / 2
. Laissez-moi vous montrer la fonction qui fait cela.
fonction 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 pente = (hexTileHeight / 4) / (hexTileWidth / 2); var caldY = dX * pente; var delta = hexTileHeight / 4-caldY; if (yVal% 2 === 0) // la correction doit être effectuée par portions triangulaires et les lignes de décalage if (Math.abs (delta)> dY) if (delta> 0) // rangée impaire en bas à droite moitié xVal--; yVal--; else // rang impair en bas à gauche moitié yVal--; else if (dX> hexTileWidth / 2) // les valeurs disponibles ne fonctionnent pas même rangée en bas à droite si (dY<((hexTileHeight/2)-caldY))//even row bottom right half yVal--; else if(dY>caldY) // rangs impairs en haut à droite et au milieu à droite moitiés xVal--; else // même ligne en bas à gauche moitié yVal--; pos.x = yVal; pos.y = xVal; retour pos;
Tout d'abord, nous trouvons xVal
et yVal
de la même manière que nous le ferions pour une grille carrée. On trouve ensuite l’horizontale restante (dX
) et vertical (dY
) les valeurs après la suppression du décalage du multiplicateur de tuiles. En utilisant ces valeurs, nous essayons de déterminer si le point se trouve dans l'une des régions triangulaires compliquées.
Si trouvé, nous apportons les modifications correspondantes aux valeurs initiales de xVal
et yVal
. Comme je l’ai dit plus tôt, le code n’est ni beau ni simple. La meilleure façon de comprendre cela serait d’appeler findHexTile
avec la souris, puis mettez console.log
dans chacune de ces conditions et déplacez la souris sur différentes régions dans une tuile hexagonale. De cette façon, vous pouvez voir comment chaque région intra-hexagonale est traitée.
Les changements de code pour la disposition verticale sont indiqués ci-dessous.
fonction 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 pente = (hexTileHeight / 2) / (hexTileWidth / 4); var caldX = dY / pente; var delta = hexTileWidth / 4-caldX; if (xVal% 2 === 0) if (dX> Math.abs (delta)) // même à gauche else // impair impair si (delta> 0) // impair inférieur droit xVal--; yVal--; else // impair droite en haut xVal--; else if (delta> 0) if (dX4. Trouver des voisins
Maintenant que nous avons trouvé la tuile sur laquelle nous avons tapé, trouvons les six tuiles voisines. C'est un problème très facile à résoudre une fois que nous avons analysé visuellement la grille. Considérons la disposition horizontale.
L'image ci-dessus montre l'intrus et le pair rangées d'une grille hexagonale disposée horizontalement quand une tuile du milieu a la valeur de
0
pour les deuxje
etj
. À partir de l'image, il devient clair que si la ligne est impaire, alors pour une tuile àje, j
les voisins sontje, j-1
,i-1, j-1
,i-1, j
,je, j + 1
,i + 1, j
, eti + 1, j-1
. Quand la ligne est paire, alors pour une tuile àje, j
les voisins sontje, j-1
,i-1, j
,i-1, j + 1
,je, j + 1
,i + 1, j + 1
, eti + 1, j
. Cela pourrait être calculé manuellement facilement.Analysons une image similaire pour les impairs et les pairs des colonnes d'une grille hexagonale alignée verticalement.
Lorsque nous avons une colonne impaire, une tuile à
je, j
auraje, j-1
,i-1, j-1
,i-1, j
,i-1, j + 1
,je, j + 1
, eti + 1, j
en tant que voisins. De même, pour une colonne paire, les voisins sonti + 1, j-1
,je, j-1
,i-1, j
,je, j + 1
,i + 1, j + 1
, eti + 1, j
.5. Démineur hexagonal
Avec les connaissances ci-dessus, nous pouvons essayer de créer un jeu de dragueur de mines hexagonal dans deux configurations différentes. Décomposons les fonctionnalités d'un jeu de démineur.
- Il y aura un nombre N de mines cachées dans la grille.
- Si nous tapons sur une tuile avec une mine, le jeu est terminé.
- Si nous tapons sur une tuile qui a une mine voisine, elle affichera le nombre de mines tout autour.
- Si nous exploitons une mine sans mines voisines, cela conduirait à la révélation de toutes les tuiles connectées qui n'ont pas de mines.
- Nous pouvons appuyer et tenir pour marquer une tuile comme une mine.
- Le jeu est terminé lorsque nous révélons toutes les tuiles sans mines.
Nous pouvons facilement stocker une valeur dans le
levelData
tableau pour indiquer une mine. La même méthode peut être utilisée pour renseigner la valeur des mines voisines sur l'index de la matrice des tuiles voisines.Au début du jeu, nous allons peupler au hasard les
levelData
tableau avec N nombre de mines. Après cela, nous mettrons à jour les valeurs pour toutes les tuiles voisines. Nous allons utiliser une méthode récursive pour révéler en chaîne toutes les tuiles vierges connectées lorsque le joueur tape sur une tuile qui n'a pas de mine en tant que voisin.Niveau de données
Nous devons créer une jolie grille hexagonale, comme le montre l'image ci-dessous..
Cela peut être fait en affichant seulement une partie de la
levelData
tableau. Si on utilise-1
comme valeur pour une tuile non utilisable et0
comme valeur pour une tuile utilisable, notrelevelData
pour atteindre le résultat ci-dessus ressemblera à ceci.// niveau de la dalle horizontale var levelData = [[-1, -1, -1,0,0,0,0,0,0,0,0, -1, -1, -1], [-1, -1 , 0,0,0,0,0,0,0,0, -1, -1, -1], [-1, -1,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, -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, -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, -1, -1, -1], [-1, -1, -1,0,0,0,0, 0,0,0, -1, -1, -1]];En boucle dans le tableau, nous n’ajouterions que des carreaux hexagonaux lorsque le
levelData
a une valeur de0
. Pour l'alignement vertical, le mêmelevelData
peut être utilisé, mais nous aurions besoin de transposer le tableau. Voici une méthode astucieuse qui peut le faire pour vous.levelData = transpose (levelData); //… fonction transpose (a) return Object.keys (a [0]). Map (fonction (c) return a.map (fonction (r) return r [c];););Ajouter des mines et mettre à jour les voisins
Par défaut, notre
levelData
a seulement deux valeurs,-1
et0
, dont nous n'utiliserions que la zone avec0
. Pour indiquer qu'une tuile contient une mine, on peut utiliser la valeur dedix
.Une tuile hexagonale vierge peut avoir un maximum de six mines à proximité, car elle a six tuiles voisines. Nous pouvons également stocker ces informations dans la
levelData
une fois que nous avons ajouté toutes les mines. Essentiellement, unlevelData
indice ayant une valeur dedix
a une mine, et si elle contient des valeurs de0
à6
, cela indique le nombre de mines voisines. Après avoir rempli les mines et mis à jour les voisins, si un élément de tableau est toujours0
, il indique qu'il s'agit d'une tuile vierge sans mines voisines.Nous pouvons utiliser les méthodes suivantes pour nos besoins.
fonction addMines () var tileType = 0; var tempArray = []; var newPt = new Phaser.Point (); pour (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;Pour chaque mine ajoutée dans
addMines
, nous incrémentons la valeur du tableau stocké dans tous ses voisins. leobtenir les voisins
la méthode ne retournera pas une tuile qui est en dehors de notre zone effective ou si elle contient une mine.Appuyez sur la logique
Lorsque le joueur tape sur une tuile, nous devons trouver l’élément de tableau correspondant à l’aide du bouton
findHexTile
méthode expliquée plus tôt. Si l'indice de mosaïque se trouve dans notre zone effective, nous comparons simplement la valeur de l'index de tableau pour déterminer s'il s'agit d'une mosaïque vierge ou vierge..fonction 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); si (! 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( 'révélation récursive'); recursiveReveal (tile.x, tile.y); else //console.log('reveal '); hexTile.reveal (); révéléeTiles ++; infoTxt.text = 'trouvé' + révélerTiles + 'sur' + blankTiles;Nous gardons une trace du nombre total de carreaux vierges en utilisant la variable
blankTiles
et le nombre de carreaux révélés à l'aide deTuiles révélées
. Une fois égaux, nous avons gagné le jeu.Lorsque nous tapons sur une tuile avec une valeur de tableau de
0
, nous devons révéler récursivement la région avec toutes les tuiles vierges connectées. Ceci est fait par la fonctionRévéler récursif
, qui reçoit les indices de tuile de la tuile taraudée.fonction recursiveReveal (i, j) var newPt = new Phaser.Point (i, j); var hexTile; var tempArray = [newPt]; voisins var; while (tempArray.length) newPt = tempArray [0]; var neighbours = getNeothers (newPt.x, newPt.y); while (voisins.length) newPt = voisins.shift (); hexTile = hexGrid.getByName ("mosaïque" + newPt.x + "_" + newPt.y); si (! hexTile.revealed) hexTile.reveal (); révéléeTiles ++; if (levelData [newPt.x] [newPt.y] === 0) tempArray.push (newPt); newPt = tempArray.shift (); // il semblait qu'un point sans voisin échappe parfois à l'itération sans être révélé, saisissez-le ici hexTile = hexGrid.getByName ("tile" + newPt.x + "_" + newPt.y ) si (! hexTile.revealed) hexTile.reveal (); révéléeTiles ++;Dans cette fonction, nous trouvons les voisins de chaque tuile et révélons la valeur de cette tuile tout en ajoutant des tuiles voisines à un tableau. Nous continuons à répéter cela avec l'élément suivant du tableau jusqu'à ce que le tableau soit vide. La récursivité s’arrête lorsque nous rencontrons des éléments de tableau contenant une mine, ce qui est assuré par le fait que
obtenir les voisins
ne reviendra pas une tuile avec une mine.Tuiles de marquage et de révélation
Vous devez avoir remarqué que j'utilise
hexTile.reveal ()
, qui est rendu possible en créant unHexTile
prototype qui conserve la plupart des attributs liés à notre tuile hexagonale. Je utilise lrévéler
fonction pour afficher le texte de la valeur de la mosaïque et définir sa couleur. De même, letoggleMark
la fonction est utilisée pour marquer la tuile comme une mine quand on appuie et maintenez.HexTile
a aussi unrévélé
attribut qui trace s'il est exploité et révélé ou non.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';Découvrez le dragueur de mines hexagonal à orientation horizontale ci-dessous. Appuyez sur pour afficher les carreaux et maintenez-le enfoncé pour marquer les mines. Il n'y a pas de jeu fini à partir de maintenant, mais si vous révélez une valeur de
dix
, alors c'est hasta la vista baby!Changements pour la version verticale
Comme j'utilise la même image de pavé hexagonal pour les deux orientations, je fais pivoter le Sprite pour l'alignement vertical. Le code ci-dessous dans le
HexTile
prototype fait cela.if (isVertical) this.rotation = Math.PI / 2;La logique du dragueur de mines reste la même pour la grille hexagonale alignée verticalement avec la différence pour
findHextile
etobtenir les voisins
logique qui doit maintenant prendre en compte la différence d’alignement. Comme mentionné précédemment, nous devons également utiliser la transposition du tableau de niveaux avec la boucle de présentation correspondante.Découvrez la version verticale ci-dessous.
Le reste du code dans le code source est simple et direct. Je voudrais que vous essayiez d'ajouter les fonctionnalités manquantes de redémarrage, de victoire et de fin de partie.
Conclusion
Cette approche d'un jeu hexagonal basé sur des tuiles utilisant un tableau à deux dimensions est plutôt celle d'un profane. Des approches plus intéressantes et fonctionnelles impliquent de modifier le système de coordonnées en différents types à l'aide d'équations.
Les plus importants sont les coordonnées axiales et les coordonnées cubiques. Une série de tutoriels de suivi traitera de ces approches. En attendant, je vous recommande de lire l'article incroyablement détaillé d'Amit sur les grilles hexagonales.