Introduction aux coordonnées axiales pour les jeux hexagonaux basés sur des tuiles

Ce que vous allez créer

L'approche de base hexagonale basée sur les carreaux expliquée dans le didacticiel hexagonal du dragueur de mines permet de faire le travail, mais n'est pas très efficace. Il utilise une conversion directe à partir des données de niveau bidimensionnelles basées sur des tableaux et des coordonnées d'écran, ce qui complique inutilement la détermination des mosaïques taraudées.. 

En outre, la nécessité d'utiliser une logique différente en fonction de la rangée / colonne impaire ou paire d'une mosaïque n'est pas pratique. Cette série de didacticiels explore les systèmes de coordonnées d'écran alternatifs qui pourraient être utilisés pour alléger la logique et rendre les choses plus pratiques. Je vous suggère fortement de lire le didacticiel hexagonal de dragueur de mines avant de poursuivre avec ce didacticiel, car il explique le rendu de la grille basé sur un tableau à deux dimensions..

1. Coordonnées axiales

L'approche par défaut utilisée pour les coordonnées d'écran dans le didacticiel hexagonal du dragueur de mines s'appelle l'approche par coordonnées décalées. En effet, les lignes ou les colonnes alternatives sont décalées d'une valeur lors de l'alignement de la grille hexagonale.. 

Pour rafraîchir votre mémoire, veuillez vous référer à l'image ci-dessous, qui montre l'alignement horizontal avec les valeurs de coordonnées décalées affichées..

Dans l'image ci-dessus, une ligne avec le même je la valeur est surlignée en rouge et une colonne avec le même j la valeur est surlignée en vert. Pour simplifier les choses, nous ne discuterons pas des variantes impaires et paires, car ce ne sont que des façons différentes d’obtenir le même résultat.. 

Permettez-moi de vous présenter une meilleure alternative aux coordonnées d'écran, la coordonnée axiale. La conversion d'une coordonnée de décalage en une variante axiale est très simple. le je la valeur reste la même, mais le j la valeur est convertie en utilisant la formule axialJ = i - sol (j / 2). Une méthode simple peut être utilisée pour convertir un offset Phaser.Point à sa variante axiale, comme indiqué ci-dessous.

fonction offsetToAxial (offsetPoint) offsetPoint.y = (offsetPoint.y- (Math.floor (offsetPoint.x / 2))); return offsetPoint; 

La conversion inverse serait comme indiqué ci-dessous.

fonction axialToOffset (axialPoint) axialPoint.y = (axialPoint.y + (Math.floor (axialPoint.x / 2))); return axialPoint; 

Ici le X la valeur est la je valeur, et y la valeur est la j valeur pour le tableau à deux dimensions. Après la conversion, les nouvelles valeurs ressembleraient à l'image ci-dessous.

Notez que la ligne verte où le j la valeur reste la même ne zigzague plus, mais est maintenant une diagonale de notre grille hexagonale.

Pour la grille hexagonale alignée verticalement, les coordonnées de décalage sont affichées dans l'image ci-dessous..

La conversion en coordonnées axiales suit les mêmes équations, avec la différence que nous gardons le j valeur la même et modifier la je valeur. La méthode ci-dessous montre la conversion.

fonction offsetToAxial (offsetPoint) offsetPoint.x = (offsetPoint.x- (Math.floor (offsetPoint.y / 2))); return offsetPoint; 

Le résultat est comme indiqué ci-dessous.

Avant d’utiliser les nouvelles coordonnées pour résoudre des problèmes, laissez-moi vous présenter rapidement une autre alternative aux coordonnées à l’écran: les coordonnées de cube..

2. Cube ou coordonnées cubiques

Redresser le zigzag lui-même a potentiellement résolu la plupart des inconvénients du système de coordonnées offset. Un cube ou des coordonnées cubiques nous aideraient en outre à simplifier une logique compliquée telle que des heuristiques ou une rotation autour d'une cellule hexagonale.. 

Comme vous l'avez peut-être deviné, le système cubique a trois valeurs. Le troisième k ou z la valeur est dérivée de l'équation x + y + z = 0, où X et y sont les coordonnées axiales. Cela nous conduit à cette méthode simple pour calculer la z valeur.

function CalculateCubicZ (axialPoint) return -axialPoint.x-axialPoint.y; 

L'équation x + y + z = 0 est en fait un plan 3D qui traverse la diagonale d’une grille de cube en trois dimensions. L'affichage des trois valeurs de la grille donnera les images suivantes pour les différents alignements hexagonaux..

La ligne bleue indique les carreaux où le z la valeur reste la même. 

3. Avantages du nouveau système de coordonnées

Vous vous demandez peut-être comment ces nouveaux systèmes de coordonnées nous aident avec la logique hexagonale. J'expliquerai quelques avantages avant de passer à la création d'un Tetris hexagonal en utilisant nos nouvelles connaissances..

Mouvement

Prenons le carreau du milieu dans l'image ci-dessus, qui a des coordonnées cubiques de 3,6, -9. Nous avons remarqué qu’une valeur de coordonnée reste la même pour les carreaux sur les lignes colorées. De plus, nous pouvons voir que les coordonnées restantes augmentent ou diminuent de 1 tout en traçant les lignes colorées. Par exemple, si le X la valeur reste la même et la y la valeur augmente de 1 dans une direction, la z la valeur diminue de 1 pour satisfaire notre équation de gouvernement x + y + z = 0. Cette fonctionnalité facilite beaucoup le contrôle des mouvements. Nous allons mettre cela à utiliser dans la deuxième partie de la série.

Voisins

Dans la même logique, il est simple de trouver les voisins pour les tuiles. x, y, z. En gardant X le même, nous avons deux voisins en diagonale, x, y-1, z + 1 et x, y + 1, z-1. En gardant y la même chose, on obtient deux voisins verticaux, x-1, y, z + 1 et x + 1, y, z-1. En gardant le même z, nous obtenons les deux voisins restants en diagonale, x + 1, y-1, z et x-1, y + 1, z. L'image ci-dessous illustre ceci pour une tuile à l'origine.

C'est tellement plus facile maintenant que nous n'avons plus besoin d'utiliser une logique différente basée sur des lignes / colonnes paires ou impaires..

Se déplacer dans une tuile

Une chose intéressante à noter dans l'image ci-dessus est une sorte de symétrie cyclique pour toutes les tuiles autour de la tuile rouge. Si nous prenons les coordonnées de toute tuile voisine, les coordonnées de la tuile voisine immédiate peuvent être obtenues en faisant un cycle des valeurs de coordonnées soit à gauche ou à droite, puis en multipliant par -1.. 

Par exemple, le voisin supérieur a une valeur de -1,0,1, qui tourne à droite devient une fois 1, -1,0 et après avoir multiplié par -1 devient -1,1,0, qui est la coordonnée du voisin de droite. Rotation à gauche et multiplication par -1 rendements 0, -1,1, qui est la coordonnée du voisin de gauche. En répétant cela, nous pouvons passer de toutes les tuiles voisines autour de la tuile centrale. C'est une fonctionnalité très intéressante qui pourrait aider dans la logique et les algorithmes. 

Notez que cela se produit uniquement parce que la vignette du milieu est considérée comme étant à l'origine. Nous pourrions facilement faire n'importe quelle tuile x, y, z être à l'origine en soustrayant les valeurs  X, y et z de lui et tous les autres carreaux.

Les heuristiques

Calculer des heuristiques efficaces est essentiel lorsqu'il s'agit de rechercher des algorithmes similaires ou similaires. Les coordonnées cubiques facilitent la recherche d'heuristiques simples pour les grilles hexagonales en raison des aspects mentionnés ci-dessus. Nous en discuterons en détail dans la deuxième partie de cette série..

Ce sont quelques-uns des avantages du nouveau système de coordonnées. Nous pourrions utiliser un mélange de différents systèmes de coordonnées dans nos mises en œuvre pratiques. Par exemple, le tableau à deux dimensions reste le meilleur moyen de sauvegarder les données de niveau, dont les coordonnées sont les coordonnées de décalage.. 

Essayons de créer une version hexagonale du célèbre jeu Tetris en utilisant cette nouvelle connaissance..

4. Créer un tetris hexagonal

Nous avons tous joué à Tetris, et si vous êtes développeur de jeux, vous avez peut-être aussi créé votre propre version. Tetris est l’un des jeux les plus simples à base de tuiles, à part le tic tac toe ou les dames, qui utilise un simple tableau à deux dimensions. Commençons par lister les fonctionnalités de Tetris.

  • Il commence par une grille bidimensionnelle vierge.
  • Différents blocs apparaissent en haut et descendent d'une tuile à la fois jusqu'à atteindre le bas..
  • Une fois en bas, ils y sont cimentés ou deviennent non interactifs. Fondamentalement, ils font partie de la grille.
  • Lors de la descente, le bloc peut être déplacé latéralement, pivoté dans le sens horaire / anti-horaire et baissé.
  • L’objectif est de remplir toutes les tuiles d’une ligne, sur lesquelles toute la ligne disparaît, en effaçant le reste de la grille remplie..
  • Le jeu se termine lorsqu'il n'y a plus de tuiles libres au-dessus pour qu'un nouveau bloc entre dans la grille.

Représenter les différents blocs

Comme le jeu a des blocs qui tombent verticalement, nous utiliserons une grille hexagonale alignée verticalement. Cela signifie que les déplacer latéralement les fera se déplacer en zigzag. Une ligne complète de la grille est constituée d’un ensemble de tuiles en ordre zigzagique. À partir de ce moment, vous pouvez commencer à vous référer au code source fourni avec ce tutoriel.. 

Les données de niveau sont stockées dans un tableau à deux dimensions nommé levelData, et le rendu est effectué en utilisant les coordonnées de décalage, comme expliqué dans le didacticiel hexagonal du dragueur de mines. Veuillez vous y référer si vous rencontrez des difficultés pour suivre le code.. 

L'élément interactif dans la section suivante montre les différents blocs que nous allons utiliser. Il y a un autre bloc supplémentaire, qui consiste en trois tuiles remplies alignées verticalement comme un pilier. BlockData est utilisé pour créer les différents blocs. 

fonction BlockData (topB, topRightB, bottomRightB, bottomB, bottomLeftB, topLeftB) this.tBlock = topB; this.trBlock = topRightB; this.brBlock = bottomRightB; this.bBlock = bottomB; this.blBlock = bottomLeftB; this.tlBlock = topLeftB; this.mBlock = 1; 

Un modèle de bloc vierge est un ensemble de sept tuiles consistant en une tuile du milieu entourée de ses six voisins. Pour tout bloc Tetris, la tuile du milieu est toujours remplie, indiquée par une valeur de 1, alors qu'une tuile vide serait désignée par une valeur de 0. Les différents blocs sont créés en remplissant les tuiles de BlockData comme ci-dessous.

var block1 = new BlockData (1,1,0,0,0,1); var block2 = new BlockData (0,1,0,0,0,1); var block3 = new BlockData (1,1,0,0,0,0); var block4 = new BlockData (1,1,0,1,0,0); var block5 = new BlockData (1,0,0,1,0,1); var block6 = new BlockData (0,1,1,0,1,1); var block7 = new BlockData (1,0,0,1,0,0);

Nous avons un total de sept blocs différents.

Rotation des blocs

Laissez-moi vous montrer comment les blocs tournent en utilisant l'élément interactif ci-dessous. Tapez et maintenez pour faire pivoter les blocs, puis tapez sur X changer le sens de rotation.

Pour faire pivoter le bloc, nous devons trouver toutes les tuiles qui ont une valeur de 1, définir la valeur à 0, faites pivoter une fois autour de la tuile du milieu pour trouver la tuile voisine et définissez sa valeur 1. Pour faire pivoter une tuile autour d’une autre tuile, nous pouvons utiliser la logique expliquée dans se déplacer autour d'une tuile section ci-dessus. Nous arrivons à la méthode ci-dessous à cet effet.

function apply anchorTile); // trouver la valeur z tileToRotate.x = tileToRotate.x-anchorTile.x; // trouver x différence trouver la différence z var pointArr = [tileToRotate.x, tileToRotate.y, tileToRotateZ]; // peupler un tableau pour faire pivoter pointArr = arrayRotate (pointArr, clockWise); // faire pivoter un tableau, true pour clockwise [0]) + anchorTile.x; // multipliez par -1 et supprimez la mosaïque de différence xToRotate.y = (- 1 * pointArr [1]) + ancreTile.y; // multipliez par -1 et supprimez la mosaïque de différence yToRotate = axialToOffset (tileToRotate); // converti en décalage retourné tileToRotate;  //… function arrayRotate (arr, reverse) // Méthode astucieuse pour faire pivoter les éléments d'un tableau if (reverse) arr.unshift (arr.pop ()) else arr.push (arr.shift ()) return arr 

La variable horloge est utilisé pour tourner dans le sens horaire ou anti-horaire, ce qui est accompli en déplaçant les valeurs de la matrice dans des directions opposées dans arrayRotate.

Déplacer le bloc

Nous gardons une trace de la je et j coordonnées de décalage pour la tuile du milieu du bloc à l'aide des variables blockMidRowValue et blockMidColumnValue respectivement. Afin de déplacer le bloc, nous incrémentons ou décrémentons ces valeurs. Nous mettons à jour les valeurs correspondantes dans levelData avec les valeurs de bloc en utilisant le paintBlock méthode. La mise à jour levelData est utilisé pour rendre la scène après chaque changement d'état.

var blockMidRowValue; var blockMidColumnValue; //… function moveLeft () blockMidColumnValue--;  function moveRight () blockMidColumnValue ++;  function dropDown () paintBlock (true); blockMidRowValue ++;  function paintBlock () clockWise = true; var val = 1; changeLevelData (blockMidRowValue, blockMidColumnValue, val); var rotationTile = new Phaser.Point (blockMidRowValue-1, blockMidColumnValue); if (currentBlock.tBlock == 1) changeLevelData (rotationTile.x, rotationTile.y, valeur * currentBlock.tBlock);  var midPoint = new Phaser.Point (blockMidRowValue, blockMidColumnValue); rotationTile = rotationTileAroundTile (rotationTile, midPoint); if (currentBlock.trBlock == 1) changeLevelData (rotationTile.x, rotationTile.y, valeur * currentBlock.trBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotationTile = rotationTileAroundTile (rotationTile, midPoint); if (currentBlock.brBlock == 1) changeLevelData (rotationTile.x, rotationTile.y, valeur * currentBlock.brBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotationTile = rotationTileAroundTile (rotationTile, midPoint); if (currentBlock.bBlock == 1) changeLevelData (rotationTile.x, rotationTile.y, valeur * currentBlock.bBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotationTile = rotationTileAroundTile (rotationTile, midPoint); if (currentBlock.blBlock == 1) changeLevelData (rotationTile.x, rotationTile.y, valeur * currentBlock.blBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotationTile = rotationTileAroundTile (rotationTile, midPoint); if (currentBlock.tlBlock == 1) changeLevelData (rotationTile.x, rotationTile.y, valeur * currentBlock.tlBlock);  function changeLevelData (iVal, jVal, newValue, erase) if (! validIndexes (iVal, jVal)) return; if (effacer) if (levelData [iVal] [jVal] == 1) levelData [iVal] [jVal] = 0;  else levelData [iVal] [jVal] = newValue;  function validIndexes (iVal, jVal) if (iVal<0 || jVal<0 || iVal>= levelData.length || jVal> = levelData [0] .length) return false;  return true;  

Ici, currentBlock pointe vers le blockData dans la scène. Dans paintBlock, d'abord nous avons mis la levelData valeur pour la tuile du milieu du bloc à 1 comme toujours 1 pour tous les blocs. L'indice du milieu est blockMidRowValueblockMidColumnValue

Puis nous passons au levelData index de la tuile au dessus de la tuile du milieu  blockMidRowValue-1,  blockMidColumnValue, et le mettre à 1 si le bloc a cette tuile comme 1. Ensuite, nous faisons pivoter une fois autour de la tuile du milieu dans le sens des aiguilles d'une montre pour obtenir la suivante et répétons le même processus. Ceci est fait pour toutes les tuiles autour de la tuile du milieu pour le bloc.

Vérification des opérations valides

Lors du déplacement ou de la rotation du bloc, nous devons vérifier si cette opération est valide. Par exemple, nous ne pouvons pas déplacer ou faire pivoter le bloc si les tuiles qu’il doit occuper sont déjà occupées. De plus, nous ne pouvons pas déplacer le bloc en dehors de notre grille bidimensionnelle. Nous devons également vérifier si le bloc peut continuer à chuter, ce qui déterminerait si nous devons le cimenter ou non.. 

Pour tout cela, j'utilise une méthode canMove (i, j), qui retourne un booléen indiquant si le bloc est placé à je, j est un coup valide. Pour chaque opération, avant de changer réellement le levelData valeurs, nous vérifions si la nouvelle position du bloc est une position valide en utilisant cette méthode.

fonction canMove (iVal, jVal) var validMove = true; var store = clockWise; var newBlockMidPoint = new Phaser.Point (blockMidRowValue + iVal, blockMidColumnValue + jVal); clockWise = true; if (! validAndEmpty (newBlockMidPoint.x, newBlockMidPoint.y)) // cochez mid, always 1 validMove = false;  var rotationTile = new Phaser.Point (newBlockMidPoint.x-1, newBlockMidPoint.y); if (currentBlock.tBlock == 1) if (! validAndEmpty (rotationTile.x, rotationTile.y)) // check top validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotationTile = rotationTileAroundTile (rotationTile, newBlockMidPoint); if (currentBlock.trBlock == 1) if (! validAndEmpty (rotationTile.x, rotationTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotationTile = rotationTileAroundTile (rotationTile, newBlockMidPoint); if (currentBlock.brBlock == 1) if (! validAndEmpty (rotationTile.x, rotationTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotationTile = rotationTileAroundTile (rotationTile, newBlockMidPoint); if (currentBlock.bBlock == 1) if (! validAndEmpty (rotationTile.x, rotationTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotationTile = rotationTileAroundTile (rotationTile, newBlockMidPoint); if (currentBlock.blBlock == 1) if (! validAndEmpty (rotationTile.x, rotationTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotationTile = rotationTileAroundTile (rotationTile, newBlockMidPoint); if (currentBlock.tlBlock == 1) if (! validAndEmpty (rotationTile.x, rotationTile.y)) validMove = false;  clockWise = store; renvoyer validMove;  function validAndEmpty (iVal, jVal) if (! validIndexes (iVal, jVal)) retour false;  else if (levelData [iVal] [jVal]> 1) // occuppied return false;  return true; 

Le processus est le même que paintBlock, mais au lieu de modifier les valeurs, cela retourne simplement un booléen indiquant un déplacement valide. Bien que j'utilise le rotation autour d'une tuile du milieu logique pour trouver les voisins, l’alternative la plus simple et la plus efficace consiste à utiliser les valeurs de coordonnées directes des voisins, qui peuvent être facilement déterminées à partir des coordonnées de carreaux du milieu.

Rendre le jeu

Le niveau de jeu est représenté visuellement par un RenderTexture nommé jeuScene. Dans le tableau levelData, une tuile inoccupée aurait une valeur de 0, et une tuile occupée aurait une valeur de 2 ou plus. 

Un bloc cimenté est désigné par une valeur de 2, et une valeur de 5 indique une tuile qui doit être supprimée car elle fait partie d'une ligne terminée. Une valeur de 1 signifie que la tuile fait partie du bloc. Après chaque changement d'état du jeu, nous rendons le niveau en utilisant les informations de levelData, comme indiqué ci-dessous.

//… hexSprite.tint = '0xffffff'; if (levelData [i] [j]> -1) axialPoint = offsetToAxial (axialPoint); cubicZ = CalculateCubicZ (axialPoint); if (levelData [i] [j] == 1) hexSprite.tint = '0xff0000';  else if (levelData [i] [j] == 2) hexSprite.tint = '0x0000ff';  else if (levelData [i] [j]> 2) hexSprite.tint = '0x00ff00';  gameScene.renderXY (hexSprite, startX, startY, false);  //… 

D'où une valeur de 0 est rendu sans aucune teinte, une valeur de 1 est rendu avec la teinte rouge, une valeur de 2 est rendu avec la teinte bleue, et une valeur de 5 est rendu avec la teinte verte.

5. Le jeu terminé

En réunissant le tout, nous obtenons le jeu hexagonal terminé de Tetris. Veuillez passer par le code source pour comprendre la mise en œuvre complète. Vous remarquerez que nous utilisons à la fois des coordonnées de décalage et des coordonnées cubiques à des fins différentes. Par exemple, pour rechercher si une ligne est terminée, nous utilisons des coordonnées de décalage et vérifions la levelData rangées.

Conclusion

Ceci conclut la première partie de la série. Nous avons réussi à créer un jeu Tetris hexagonal utilisant une combinaison de coordonnées de décalage, de coordonnées axiales et de coordonnées de cube.. 

Dans la dernière partie de la série, nous étudierons le mouvement des personnages en utilisant les nouvelles coordonnées sur une grille hexagonale alignée horizontalement..