Hexagonal Character Movement Using Axial Coordinates
In the first part of the series, we explored the different coordinate systems for hexagonal tilebased games with the help of a hexagonal Tetris game. One thing you may have noticed is that we are still relying on the offset coordinates for drawing the level onto the screen using the levelData
array.
You may also be curious to know how we could determine the axial coordinates of a hexagonal tile from the pixel coordinates on the screen. The method used in the hexagonal minesweeper tutorial relies on the offset coordinates and is not a simple solution. Once we figure this out, we will proceed to create solutions for hexagonal character movement and pathfinding.
1. Converting Coordinates Between Pixel and Axial
This will involve some math. We will be using the horizontal layout for the entire tutorial. Let's start by finding a very helpful relationship between the regular hexagon's width and height. Please refer to the image below.
Consider the blue regular hexagon on the left of the image. We already know that all the sides are of equal length. All the interior angles are 120 degrees each. Connecting each corner to the centre of the hexagon will yield six triangles, one of which is shown using red lines. This triangle has all the internal angles equal to 60 degrees.
As the red line splits the two corner angles in the middle, we get 120/2=60
. The third angle is 180(60+60)=60
as the sum of all angles within the triangle should be 180 degrees. Thus essentially the triangle is an equilateral triangle, which further means that each side of the triangle has the same length. So in the blue hexagon the two red lines, the green line and each blue line segment are of the same length. From the image, it is clear that the green line is hexTileHeight/2
.
Proceeding to the hexagon on the right, we can see that as the side length is equal to hexTileHeight/2
, the height of the top triangular portion should be hexTileHeight/4
and the height of the bottom triangular portion should be hexTileHeight/4
, which totals to the full height of the hexagon, hexTileHeight
.
Now consider the small rightangled triangle in the top left with one green and one blue angle. The blue angle is 60 degrees as it is the half of the corner angle, which in turn means that the green angle is 30 degrees (180(60+90)
). Using this information, we arrive at a relationship between the height and width of the regular hexagon.
1 
tan 30 = opposite side/adjacent side; 
2 
1/sqrt(3) = (hexTileHeight/4)/(hexTileWidth/2); 
3 
hexTileWidth = sqrt(3)*hexTileHeight/2; 
4 
hexTileHeight = 2*hexTileWidth/sqrt(3); 
Converting Axial to Pixel Coordinates
Before we approach the conversion, let's revisit the image of the horizontal hexagonal layout where we have highlighted the row and column in which one of the coordinates remains the same.
Considering the screen y value, we can see that each row has a y offset of 3*hexTileHeight/4
, while going down on the green line, the only value that changes is i
. Hence, we can conclude that the y pixel value only depends on the axial i
coordinate.
1 
y= (3*hexTileHeight/4)*i; 
2 
y = 3/2*s*i; 
Where s
is the side length, which was found to be hexTileHeight/2
.
The screen x value is a bit more complicated than this. When considering the tiles within a single row, each tile has an x offset of hexTileWidth
, which clearly depends only on the axial j
coordinate. But each alternative row has an additional offset of hexTileWidth/2
depending on the axial i
coordinate.
Again considering the green line, if we imagine it was a square grid then the line would have been vertical, satisfying the equation x=j*hexTileWidth
. As the only coordinate that changes along the green line is i
, the offset will depend on it. This leads us to the following equation.
1 
x=j*hexTileWidth+(i*hexTileWidth/2); 
2 
= j* sqrt(3)*hexTileHeight/2 + i* sqrt(3)*hexTileHeight/4; 
3 
= sqrt(3)*s*(j+ (i/2)); 
So here we have them: the equations to convert axial coordinates to screen coordinates. The corresponding conversion function is as below.
1 
var rootThree=Math.sqrt(3); 
2 
var sideLength=hexTileHeight/2; 
3 
function axialToScreen(axialPoint){ 
4 
var tileX=rootThree*sideLength*(axialPoint.y+(axialPoint.x/2)); 
5 
var tileY=3*sideLength/2*axialPoint.x; 
6 
axialPoint.x=tileX; 
7 
axialPoint.y=tileY; 
8 
return axialPoint; 
9 
}

The revised code for drawing the hexagonal grid is as follows.
1 
for (var i = 0; i < levelData.length; i++) 
2 
{

3 
for (var j = 0; j < levelData[0].length; j++) 
4 
{

5 
axialPoint.x=i; 
6 
axialPoint.y=j; 
7 
axialPoint=offsetToAxial(axialPoint); 
8 
screenPoint=axialToScreen(axialPoint); 
9 
if(levelData[i][j]!=1){ 
10 
hexTile= new HexTileNode(game, screenPoint.x, screenPoint.y, 'hex', false,i,j,levelData[i][j]); 
11 
hexGrid.add(hexTile); 
12 
}

13 
}

14 
}

Converting Pixel to Axial Coordinates
Reversing those equations with the simple substitution of one variable will lead us to the screen to axial conversion equations.
1 
i=y/(3/2*s); 
2 
j=(x(y/sqrt(3)))/s*sqrt(3); 
Although the required axial coordinates are integers, the equations will result in floating point numbers. So we will need to round them off and apply some corrections, relying on our main equation x+y+z=0
. The conversion function is as below.
1 
function screenToAxial(screenPoint){ 
2 
var axialPoint=new Phaser.Point(); 
3 
axialPoint.x=screenPoint.y/(1.5*sideLength); 
4 
axialPoint.y=(screenPoint.x(screenPoint.y/rootThree))/(rootThree*sideLength); 
5 
var cubicZ=calculateCubicZ(axialPoint); 
6 
var round_x=Math.round(axialPoint.x); 
7 
var round_y=Math.round(axialPoint.y); 
8 
var round_z=Math.round(cubicZ); 
9 
if(round_x+round_y+round_z===0){ 
10 
screenPoint.x=round_x; 
11 
screenPoint.y=round_y; 
12 
}else{ 
13 
var delta_x=Math.abs(axialPoint.xround_x); 
14 
var delta_y=Math.abs(axialPoint.yround_y); 
15 
var delta_z=Math.abs(cubicZround_z); 
16 
if(delta_x>delta_y && delta_x>delta_z){ 
17 
screenPoint.x=round_yround_z; 
18 
screenPoint.y=round_y; 
19 
}else if(delta_y>delta_x && delta_y>delta_z){ 
20 
screenPoint.x=round_x; 
21 
screenPoint.y=round_xround_z; 
22 
}else if(delta_z>delta_x && delta_z>delta_y){ 
23 
screenPoint.x=round_x 
24 
screenPoint.y=round_y; 
25 
}

26 
}

27 
return screenPoint; 
28 
}

Check out the interactive element, which uses these methods to display tiles and detect taps.
<2. Character Movement
The core concept of character movement in any grid is similar. We poll for user input, determine the direction, find the resulting position, check if the resulting position falls inside a wall in the grid, else move the character to that position. You may refer to my isometric character movement tutorial to see this in action with respect to isometric coordinate conversion.
The only things that are different here are the coordinate conversion and the directions of motion. For a horizontally aligned hexagonal grid, there are six available directions for motion. We could use the keyboard keys A
, W
, E
, D
, X
, and Z
for controlling each direction. The default keyboard layout matches the directions perfectly, and the related functions are as below.
1 
function moveLeft(){ 
2 
movementVector.x=movementVector.y=0; 
3 
movementVector.x=1*speed; 
4 
CheckCollisionAndMove(); 
5 
}

6 
function moveRight(){ 
7 
movementVector.x=movementVector.y=0; 
8 
movementVector.x=speed; 
9 
CheckCollisionAndMove(); 
10 
}

11 
function moveTopLeft(){ 
12 
movementVector.x=0.5*speed;//Cos60 
13 
movementVector.y=0.866*speed;//sine60 
14 
CheckCollisionAndMove(); 
15 
}

16 
function moveTopRight(){ 
17 
movementVector.x=0.5*speed;//Cos60 
18 
movementVector.y=0.866*speed;//sine60 
19 
CheckCollisionAndMove(); 
20 
}

21 
function moveBottomRight(){ 
22 
movementVector.x=0.5*speed;//Cos60 
23 
movementVector.y=0.866*speed;//sine60 
24 
CheckCollisionAndMove(); 
25 
}

26 
function moveBottomLeft(){ 
27 
movementVector.x=0.5*speed;//Cos60 
28 
movementVector.y=0.866*speed;//sine60 
29 
CheckCollisionAndMove(); 
30 
}

The diagonal directions of motion make an angle of 60 degrees with the horizontal direction. So we can directly calculate the new position using trigonometry by using Cos 60
and Sine 60
. From this movementVector
, we find out the new resulting position and check if it falls inside a wall in the grid as below.
1 
function CheckCollisionAndMove(){ 
2 
var tempPos=new Phaser.Point(); 
3 
tempPos.x=hero.x+movementVector.x; 
4 
tempPos.y=hero.y+movementVector.y; 
5 
var corner=new Phaser.Point(); 
6 
//check tl

7 
corner.x=tempPos.xheroSize/2; 
8 
corner.y=tempPos.yheroSize/2; 
9 
if(checkCorner(corner))return; 
10 
//check tr

11 
corner.x=tempPos.x+heroSize/2; 
12 
corner.y=tempPos.yheroSize/2; 
13 
if(checkCorner(corner))return; 
14 
//check bl

15 
corner.x=tempPos.xheroSize/2; 
16 
corner.y=tempPos.y+heroSize/2; 
17 
if(checkCorner(corner))return; 
18 
//check br

19 
corner.x=tempPos.x+heroSize/2; 
20 
corner.y=tempPos.y+heroSize/2; 
21 
if(checkCorner(corner))return; 
22 

23 
hero.x=tempPos.x; 
24 
hero.y=tempPos.y; 
25 
}

26 
function checkCorner(corner){ 
27 
corner=screenToAxial(corner); 
28 
corner=axialToOffset(corner); 
29 
if(checkForOccuppancy(corner.x,corner.y)){ 
30 
return true; 
31 
}

32 
return false; 
33 
}

We add the movementVector
to the hero position vector to get the new position for the hero sprite's centre. Then we find the position of the four corners of the hero sprite and check if those are colliding. If there are no collisions, then we set the new position to the hero sprite. Let's see that in action.
Usually, this kind of freeflowing motion is not allowed in a gridbased game. Typically, characters move from tile to tile, that is, tile centre to tile centre, based on commands or tap. I trust that you can figure the solution out by yourself.
3. Pathfinding
So here we are on the topic of pathfinding, a very scary topic for some. In my previous tutorials I never tried to create new pathfinding solutions but always preferred to use readily available solutions which are battle tested.
This time, I am making an exception and will be reinventing the wheel, mainly because there are various game mechanics possible and no single solution would benefit all. So it is handy to know how the whole thing is done in order to churn out your own custom solutions for your game mechanic.
The most basic algorithm that is used for pathfinding in grids is Dijkstra's Algorithm. We start at the first node and calculate the costs involved in moving to all the possible neighbour nodes. We close the first node and move to the neighbour node with the lowest cost involved. This is repeated for all the nonclosed nodes till we reach the destination. A variant of this is the A* algorithm, where we also use a heuristic in addition to the cost.
A heuristic is used to calculate the approximate distance from the current node to the destination node. As we do not really know the path, this distance calculation is always an approximation. So a better heuristic will always yield a better path. Now, that being said, the best solution need not be the one which yields the best path as we need to consider the resource usage and performance of the algorithm as well, when all the calculations need to be done in real time or once per update loop.
The easiest and simplest heuristic is the Manhattan heuristic
or Manhattan distance
. In a 2D grid, this is actually the distance between the start node and end node as the crow flies, or the number of blocks we need to walk.
Hexagonal Manhattan Variant
For our hexagonal grid, we need to find a variant for the Manhattan heuristic to approximate the distance. As we are walking on the hexagonal tiles, the idea is to find the number of tiles we need to walk over to reach the destination. Let me show you the solution first. Please move the mouse over the interactive element below to see how far away the other tiles are from the tile under the mouse.
In the example above, we find the tile under the mouse and find the distance of all other tiles from it. The logic is to find the difference of i
and j
axial coordinates of both tiles first, say di
and dj
. Find the absolute values of these differences, absi
and absj
, as distances are always positive.
We notice that when both di
and dj
are positive and when both di
and dj
are negative, the distance is absi+absj
. When di
and dj
are of opposite signs, the distance is the bigger value among absi
and absj
. This leads to the heuristic calculation function getHeuristic
as below.
1 
getHeuristic=function(i,j){ 
2 
j=(j(Math.floor(i/2))); 
3 
var di=ithis.originali; 
4 
var dj=jthis.convertedj; 
5 
var si=Math.sign(di); 
6 
var sj=Math.sign(dj); 
7 
var absi=di*si; 
8 
var absj=dj*sj; 
9 
if(si!=sj){ 
10 
this.heuristic= Math.max(absi,absj); 
11 
}else{ 
12 
this.heuristic= (absi+absj); 
13 
}

14 
}

One thing to notice is that we are not considering if the path is really walkable or not; we just assume that it is walkable and set the distance value.
Finding the Hexagonal Path
Let's proceed with pathfinding for our hexagonal grid with the newly found heuristic method. As we will be using recursion, it will be easier to understand once we breakdown the core logic of our approach. Each hexagonal tile will have a heuristic distance and a cost value associated with it.
 We have a recursive function, say
findPath(tile)
, which takes in one hexagonal tile, which is the current tile. Initially this will be the starting tile.  If the tile is equal to the end tile, the recursion ends and we have found the path. Else we proceed with the calculation.
 We find all the walkable neighbours of the tile. We will loop through all the neighbour tiles and apply further logic to each of them unless they are
closed
.  If a neighbour is not previously visited and not closed, we find the distance of the neighbour tile to the end tile using our heuristic. We set the neighbour tile's
cost
to current tile's cost + 10. We set the neighbour tile as visited. We set the neighbour tile'sprevious tile
as the current tile. We do this for a previously visited neighbour as well if the current tile's cost + 10 is less than that neighbour's cost.  We calculate the total cost as the sum of the neighbour tile's cost value and the heuristic distance value. Among all the neighbours, we select the neighbour which gives the lowest total cost and call
findPath
on that neighbour tile.  We set the current tile to closed so that it won't be considered anymore.
 In some cases, we'll fail to find any tile which satisfies the conditions, and then we close the current tile, open the previous tile, and redo.
There is an obvious failure condition in the logic when more than one tile satisfies the conditions. A better algorithm will find all the different paths and select the one with the shortest length, but we won't be doing that here. Check the pathfinding in action below.
For this example, I am calculating neighbours differently than in the Tetris example. When using axial coordinates, the neighbour tiles have coordinates which are higher or lower by a value of 1.
1 
function getNeighbors(i,j){ 
2 
//coordinates are in axial

3 
var tempArray=[]; 
4 
var axialPoint=new Phaser.Point(i,j); 
5 
var neighbourPoint=new Phaser.Point(); 
6 
neighbourPoint.x=axialPoint.x1;//tr 
7 
neighbourPoint.y=axialPoint.y; 
8 
populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); 
9 
neighbourPoint.x=axialPoint.x+1;//bl 
10 
neighbourPoint.y=axialPoint.y; 
11 
populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); 
12 
neighbourPoint.x=axialPoint.x;//l 
13 
neighbourPoint.y=axialPoint.y1; 
14 
populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); 
15 
neighbourPoint.x=axialPoint.x;//r 
16 
neighbourPoint.y=axialPoint.y+1; 
17 
populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); 
18 
neighbourPoint.x=axialPoint.x1;//tr 
19 
neighbourPoint.y=axialPoint.y+1; 
20 
populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); 
21 
neighbourPoint.x=axialPoint.x+1;//bl 
22 
neighbourPoint.y=axialPoint.y1; 
23 
populateNeighbor(neighbourPoint.x,neighbourPoint.y,tempArray); 
24 
return tempArray; 
25 
}

The findPath
recursive function is as below.
1 
function findPath(tile){//passes in a hexTileNode 
2 
if(Phaser.Point.equals(tile,endTile)){ 
3 
//success, destination reached

4 
console.log('success'); 
5 
//now paint the path.

6 
paintPath(tile); 
7 
}else{//find all neighbors 
8 
var neighbors=getNeighbors(tile.originali,tile.convertedj); 
9 
var newPt=new Phaser.Point(); 
10 
var hexTile; 
11 
var totalCost=0; 
12 
var currentLowestCost=100000; 
13 
var nextTile; 
14 

15 
//find heuristics & cost for all neighbors

16 
while(neighbors.length){ 
17 
newPt=neighbors.shift(); 
18 
hexTile=hexGrid.getByName("tile"+newPt.x+"_"+newPt.y); 
19 
if(!hexTile.nodeClosed){//if node was not already calculated 
20 
if((hexTile.nodeVisited && (tile.cost+10)<hexTile.cost)  
21 
!hexTile.nodeVisited){//if node was already visited, compare cost 
22 
hexTile.getHeuristic(endTile.originali,endTile.originalj); 
23 
hexTile.cost=tile.cost+10; 
24 
hexTile.previousNode=tile;//point to previous node 
25 
hexTile.nodeVisited=true; 
26 
hexTile.showDifference();//display heuristic & cost 
27 
}else continue; 
28 
totalCost=hexTile.cost+hexTile.heuristic; 
29 
if(totalCost<currentLowestCost){//select the next neighbour with lowest total cost 
30 
nextTile=hexTile; 
31 
currentLowestCost=totalCost; 
32 
}

33 
}else{ 
34 
console.log('node closed'); 
35 
}

36 
}

37 
tile.nodeClosed=true; 
38 
if(nextTile!==null){ 
39 
findPath(nextTile);//call algo on the new tile 
40 
nextTileToCall=nextTile; 
41 
}else{ 
42 
if(tile.previousNode!==null){ 
43 
//current tile is now closed, open previous tile and redo.

44 
tile.previousNode.cost=10; 
45 
tile.previousNode.nodeClosed=false; 
46 
findPath(tile.previousNode);//call algo on the previous tile 
47 
nextTileToCall=tile.previousNode; 
48 
}else{ 
49 
//no path

50 
nextTileToCall=null; 
51 
}

52 
}

53 
}

54 
}

It may require further and multiple reading to properly understand what is going on, but believe me, it is worth the effort. This is only a very basic solution and could be improved a lot. For moving the character along the calculated path, you can refer to my isometric path following tutorial.
Marking the path is done using another simple recursive function, paintPath(tile)
, which is first called with the end tile. We just mark the previousNode
of the tile if present.
1 
function paintPath(tile){ 
2 
tile.markDirty(); 
3 
if(tile.previousNode!==null){ 
4 
paintPath(tile.previousNode); 
5 
}

6 
}

Conclusion
With the help of all the three hexagonal tutorials I have shared, you should be able to get started with your next awesome hexagonal tilebased game.
Please be advised that there are other approaches as well, and there's a lot of further reading out there if you are up for it. Please do let me know through the comments if you need anything more to be explored in relation to hexagonal tilebased games.