Dans la première partie de la série, nous avons exploré les différents systèmes de coordonnées pour les jeux hexagonaux basés sur des tuiles à l’aide d’un jeu de Tetris hexagonal. Une chose que vous avez peut-être remarquée, c'est que nous nous appuyons toujours sur les coordonnées du décalage pour afficher le niveau sur l'écran à l'aide de la levelData
tableau.
Vous voudrez peut-être aussi savoir comment nous pourrions déterminer les coordonnées axiales d’une mosaïque hexagonale à partir des coordonnées en pixels de l’écran. La méthode utilisée dans le didacticiel hexagonal de dragueur de mines repose sur les coordonnées de décalage et n’est pas une solution simple. Une fois que nous aurons compris cela, nous procéderons à la création de solutions pour le déplacement de personnage hexagonal et la recherche de trajectoire..
Cela impliquera des maths. Nous allons utiliser la mise en page horizontale pour l'ensemble du tutoriel. Commençons par trouver une relation très utile entre la largeur et la hauteur de l'hexagone régulier. S'il vous plaît se référer à l'image ci-dessous.
Considérez l'hexagone régulier bleu à gauche de l'image. Nous savons déjà que tous les côtés ont la même longueur. Tous les angles intérieurs sont de 120 degrés chacun. En reliant chaque coin au centre de l'hexagone, vous obtiendrez six triangles, dont l'un est représenté par des lignes rouges. Ce triangle a tous les angles internes égaux à 60 degrés.
Lorsque la ligne rouge divise les deux angles au centre, nous obtenons 120/2 = 60
. Le troisième angle est 180- (60 + 60) = 60
comme la somme de tous les angles dans le triangle devrait être 180 degrés. Ainsi, le triangle est essentiellement un triangle équilatéral, ce qui signifie en outre que chaque côté du triangle a la même longueur. Ainsi, dans l'hexagone bleu, les deux lignes rouges, la ligne verte et chaque segment de ligne bleue ont la même longueur. De l'image, il est clair que la ligne verte est hexTileHeight / 2
.
En se dirigeant vers l'hexagone à droite, on peut voir que la longueur du côté est égale à hexTileHeight / 2
, la hauteur de la partie triangulaire supérieure devrait être hexTileHeight / 4
et la hauteur de la partie triangulaire inférieure devrait être hexTileHeight / 4
, qui totalise toute la hauteur de l'hexagone, hexTileHeight
.
Maintenant, considérons le petit triangle rectangle en haut à gauche avec un angle vert et un angle bleu. L'angle bleu est de 60 degrés car il s'agit de la moitié de l'angle du coin, ce qui signifie que l'angle vert est de 30 degrés (180- (60 + 90)
). En utilisant cette information, nous arrivons à une relation entre la hauteur et la largeur de l'hexagone régulier.
tan 30 = côté opposé / côté adjacent; 1 / sqrt (3) = (hexTileHeight / 4) / (hexTileWidth / 2); hexTileWidth = sqrt (3) * hexTileHeight / 2; hexTileHeight = 2 * hexTileWidth / sqrt (3);
Avant d’aborder la conversion, revenons à l’image de la mise en page hexagonale horizontale où nous avons mis en surbrillance la ligne et la colonne dans lesquelles l’une des coordonnées reste la même..
En considérant la valeur d'écran y, nous pouvons voir que chaque ligne a un décalage y de 3 * hexTileHeight / 4
, en descendant sur la ligne verte, la seule valeur qui change est je
. Par conséquent, nous pouvons conclure que la valeur de pixel y ne dépend que de la valeur axiale je
coordonner.
y = (3 * hexTileHeight / 4) * i; y = 3/2 * s * i;
Où s
est la longueur du côté, qui s'est avéré être hexTileHeight / 2
.
La valeur de l'écran x est un peu plus compliquée que cela. Lorsqu’on considère les carreaux d’une même rangée, chaque carreau a un décalage x de hexTileWidth
, qui dépend clairement que de l'axial j
coordonner. Mais chaque ligne alternative a un décalage supplémentaire de hexTileWidth / 2
en fonction de l'axial je
coordonner.
Toujours en considérant la ligne verte, si nous imaginions qu’il s’agissait d’une grille carrée, la ligne aurait été verticale, satisfaisant ainsi l’équation. x = j * hexTileWidth
. Comme la seule coordonnée qui change le long de la ligne verte est je
, le décalage en dépendra. Cela nous amène à l'équation suivante.
x = j * hexTileWidth + (i * hexTileWidth / 2); = j * sqrt (3) * hexTileHeight / 2 + i * sqrt (3) * hexTileHeight / 4; = sqrt (3) * s * (j + (i / 2));
Nous les avons donc ici: les équations pour convertir les coordonnées axiales en coordonnées d'écran. La fonction de conversion correspondante est comme ci-dessous.
var rootThree = Math.sqrt (3); var sideLength = hexTileHeight / 2; fonction 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;
Le code révisé pour dessiner la grille hexagonale est comme suit.
pour (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);
Inverser ces équations avec la simple substitution d'une variable nous mènera à l'écran pour les équations de conversion axiales.
i = y / (3/2 * s); j = (x- (y / sqrt (3))) / s * sqrt (3);
Bien que les coordonnées axiales requises soient des entiers, les équations donneront des nombres en virgule flottante. Nous devrons donc les arrondir et appliquer des corrections, en nous basant sur notre équation principale x + y + z = 0
. La fonction de conversion est comme ci-dessous.
fonction 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;
Découvrez l'élément interactif, qui utilise ces méthodes pour afficher des tuiles et détecter des taps.
Le concept de base du mouvement des personnages dans n'importe quelle grille est similaire. Nous interrogeons l'utilisateur, déterminons la direction, trouvons la position résultante, vérifions si la position résultante tombe à l'intérieur d'un mur de la grille, sinon nous déplaçons le personnage à cette position. Vous pouvez vous référer à mon tutoriel sur les mouvements de caractères isométriques pour voir cela en action en ce qui concerne la conversion de coordonnées isométriques..
Les seules choses qui diffèrent ici sont la conversion de coordonnées et les directions de mouvement. Pour une grille hexagonale alignée horizontalement, six directions sont disponibles pour le mouvement. Nous pourrions utiliser les touches du clavier UNE
, W
, E
, ré
, X
, et Z
pour contrôler chaque direction. La disposition du clavier par défaut correspond parfaitement aux directions et les fonctions associées sont les suivantes..
function moveLeft () movementVector.x = movementVector.y = 0; movementVector.x = -1 * vitesse; CheckCollisionAndMove (); function moveRight () movementVector.x = movementVector.y = 0; movementVector.x = speed; CheckCollisionAndMove (); function moveTopLeft () movementVector.x = -0,5 * vitesse; // Cos60 movementVector.y = -0,866 * vitesse; // 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 * vitesse; // Cos60 movementVector.y = 0.866 * vitesse; // sine60 CheckCollisionAndMove ();
Les directions de déplacement en diagonale forment un angle de 60 degrés avec la direction horizontale. Donc, nous pouvons calculer directement la nouvelle position en utilisant la trigonométrie en utilisant Cos 60
et Sine 60
. À partir de cela mouvementVecteur
, nous découvrons la nouvelle position résultante et vérifions si elle tombe à l'intérieur d'un mur dans la grille comme ci-dessous.
fonction CheckCollisionAndMove () var tempPos = new Phaser.Point (); tempPos.x = hero.x + movementVector.x; tempPos.y = hero.y + movementVector.y; var corner = new Phaser.Point (); // vérifie tl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner (corner)) retourne; // vérifie tr corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner (corner)) retourne; // check bl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner (corner)) retourne; // vérifie br corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner (corner)) retourne; hero.x = tempPos.x; hero.y = tempPos.y; function checkCorner (coin) coin = screenToAxial (coin); corner = axialToOffset (corner); if (checkForOccuppancy (corner.x, corner.y)) return true; return false;
Nous ajoutons le mouvementVecteur
au vecteur de position du héros pour obtenir la nouvelle position du centre de l’image-objet du héros. Ensuite, nous trouvons la position des quatre coins du sprite héros et vérifions si ceux-ci entrent en collision. S'il n'y a pas de collision, nous définissons la nouvelle position sur le sprite du héros. Voyons cela en action.
Habituellement, ce type de mouvement fluide n'est pas autorisé dans un jeu basé sur une grille. En règle générale, les caractères se déplacent d'une mosaïque à l'autre, c'est-à-dire d'un centre à l'autre, en fonction des commandes ou de la frappe. J'espère que vous pourrez trouver la solution par vous-même.
Nous sommes donc ici sur le thème de la recherche de chemins, un sujet très effrayant pour certains. Dans mes précédents tutoriels, je n'ai jamais essayé de créer de nouvelles solutions de découverte, mais j'ai toujours préféré utiliser des solutions disponibles qui sont testées au combat..
Cette fois-ci, je fais une exception et je vais réinventer la roue, principalement parce qu’il existe différents mécanismes de jeu et qu’aucune solution unique ne profiterait à tous. Il est donc pratique de savoir comment tout cela se passe afin de créer vos propres solutions personnalisées pour votre mécanicien de jeu..
L’algorithme le plus fondamental utilisé pour la recherche de chemin dans les grilles est Algorithme de Dijkstra. Nous commençons au premier nœud et calculons les coûts nécessaires pour passer à tous les nœuds voisins possibles. Nous fermons le premier noeud et passons au noeud voisin au coût le plus bas. Ceci est répété pour tous les nœuds non fermés jusqu'à ce que nous atteignions la destination. Une variante de ceci est la Algorithme A *, où nous utilisons également une heuristique en plus du coût.
Une heuristique est utilisée pour calculer la distance approximative entre le nœud actuel et le nœud de destination. Comme nous ne connaissons pas vraiment le chemin, ce calcul de distance est toujours approximatif. Donc, une meilleure heuristique donnera toujours un meilleur chemin. Cela dit, la meilleure solution ne doit pas nécessairement être celle qui donne le meilleur chemin, car nous devons également prendre en compte l'utilisation des ressources et les performances de l'algorithme, lorsque tous les calculs doivent être effectués en temps réel ou une fois par mise à jour. boucle.
L'heuristique la plus simple et la plus simple est la Heuristique de Manhattan
ou Distance de Manhattan
. Dans une grille 2D, il s’agit en fait de la distance entre le noeud de départ et le noeud de fin à vol d'oiseau, ou du nombre de blocs que nous devons parcourir..
Pour notre grille hexagonale, nous devons trouver une variante de l'heuristique de Manhattan pour approximer la distance. Alors que nous marchons sur les tuiles hexagonales, l’idée est de trouver le nombre de tuiles sur lesquelles nous devons marcher pour atteindre la destination. Laissez-moi vous montrer la solution en premier. Passez la souris sur l'élément interactif ci-dessous pour voir à quelle distance se trouvent les autres tuiles de la tuile sous la souris..
Dans l'exemple ci-dessus, nous trouvons la vignette sous la souris et la distance de toutes les autres. La logique est de trouver la différence de je
et j
les coordonnées axiales des deux tuiles d'abord, disons di
et dj
. Trouver les valeurs absolues de ces différences, absi
et absj
, comme les distances sont toujours positives.
Nous remarquons que lorsque les deux di
et dj
sont positifs et quand les deux di
et dj
sont négatifs, la distance est absi + absj
. Quand di
et dj
sont de signes opposés, la distance est la plus grande valeur parmi absi
et absj
. Cela conduit à la fonction de calcul heuristique GetHeuristic
comme ci-dessous.
getHeuristic = fonction (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; si (si! = sj) this.heuristic = Math.max (absi, absj); else this.heuristic = (absi + absj);
Une chose à noter est que nous ne considérons pas si le chemin est vraiment praticable ou non; nous supposons simplement qu'il est praticable et définissons la valeur de la distance.
Continuons avec le cheminement de notre grille hexagonale avec la méthode heuristique récemment trouvée. Comme nous allons utiliser la récursivité, il sera plus facile de comprendre une fois que nous aurons décomposé la logique de base de notre approche. Chaque tuile hexagonale aura une distance heuristique et une valeur de coût qui lui est associée.
findPath (mosaïque)
, qui prend dans une tuile hexagonale, qui est la tuile actuelle. Au départ, ce sera la tuile de départ.fermé
.Coût
à coût actuel de la tuile + 10. Nous plaçons la tuile voisine comme a visité. Nous posons la tuile du voisin tuile précédente
comme la tuile actuelle. Nous le faisons également pour un voisin déjà visité si le coût de la tuile actuelle + 10 est inférieur au coût du voisin..findPath
sur cette tuile voisine.Il y a une défaillance évidente dans la logique lorsque plusieurs carreaux remplissent les conditions. Un meilleur algorithme trouvera tous les chemins différents et sélectionnera celui qui a la longueur la plus courte, mais nous ne le ferons pas ici. Vérifiez le cheminement en action ci-dessous.
Pour cet exemple, je calcule les voisins différemment de l'exemple Tetris. Lorsque vous utilisez des coordonnées axiales, les tuiles voisines ont des coordonnées plus hautes ou plus basses d'une valeur de 1.
function getNe voisins (i, j) // les coordonnées sont en ax var 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); renvoyer tempArray;
le findPath
fonction récursive est comme ci-dessous.
function findPath (tile) // passe dans un hexTileNode if (Phaser.Point.equals (tile, endTile)) // succès, destination atteinte console.log ('succès'); // peint maintenant le chemin. paintPath (mosaïque); else // trouver tous les voisins var voisins = getNeothers (tile.originali, tile.convertedj); var newPt = new Phaser.Point (); var hexTile; var totalCost = 0; var currentLowestCost = 100000; var nextTile; // trouve des heuristiques et des coûts pour tous les voisins while (neighbours.length) newPt = voisins.shift (); hexTile = hexGrid.getByName ("mosaïque" + newPt.x + "_" + newPt.y); if (! hexTile.nodeClosed) // si le noeud n'a pas déjà été calculé if ((hexTile.nodeVisited && (tile.cost + 10)Il faudra peut-être plusieurs lectures supplémentaires pour bien comprendre ce qui se passe, mais croyez-moi, cela en vaut la peine. Ceci n'est qu'une solution très basique et pourrait être considérablement améliorée. Pour déplacer le caractère le long du chemin calculé, vous pouvez vous référer à mon tutoriel sur le chemin isométrique.
Le marquage du chemin est fait en utilisant une autre fonction récursive simple,
paintPath (mosaïque)
, qui s'appelle d'abord avec la tuile de fin. Nous venons de marquer lepreviousNode
de la tuile si présente.fonction paintPath (tile) tile.markDirty (); if (tile.previousNode! == null) paintPath (tile.previousNode);Conclusion
Avec l'aide des trois didacticiels hexagonaux que j'ai partagés, vous devriez être en mesure de commencer votre prochain jeu génial hexagonal basé sur des tuiles..
Veuillez noter qu'il existe également d'autres approches et qu'il y a beaucoup de lectures supplémentaires à lire si vous le souhaitez. Faites-le moi savoir à travers les commentaires si vous avez besoin d'explorer quelque chose de plus par rapport aux jeux hexagonaux basés sur des dalles.