Hey again!
This time I’ll talk about something different, coordinates in hexagon based maps!
First of all let me tell you this , hexagons are much more cooler than square tiles can ever be, period. And I won’t even go into stuff like diagonal movement problem on square tiles and stuff like that. It’s just that square tiles used way too much while it feels like hexagons are under used and under appreciated.
But hexagons comes with a price, small but still there it is. It just a bit harder to calculate coordinates on a hexagon based maps. You know how easy it is to calculate pixel/tile coordinates on square based maps? You just divide/multiply everything by square edge size? Well it’s just a bit more complex on hexagonal maps as you can guess but still all it takes is a few helper functions and then you’re set.
Hexagons – Billions of bees cannot be wrong
You probably already know that there are 2 different type of hexagonal structure ; vertically aligned and horizontally aligned.
Well technically it’s just like rotating 90 degrees right? But it’s actually much more than that. Not only that rotation changes math behind all together, it even effect how many different sprites you need to texture etc etc. So when you choose on of these styles , you probably won’t have an easy time to switch to another so be careful.
I personally prefer horizontally aligned hexagons ( top ). It totally personal for me but horizontally aligned hexagons feels better for grand strategy games like Civilization while vertically aligned hexagons feel better for small size tactical strategy games. But as I said it’s totally personal. Also I heard some artists saying that horizontally aligned requires less sprite variations but don’t quote my on that.
Anyway as I said I prefer horizontally aligned hexagons so all my math and samples will be using that. Still I’m sure you can work math out for vertically aligned one easily.
And one more thing before we start the calculations, obviously we’ll need some constants for hexagons like edge length. Have a look at this ;
We’ll calculate all those variables from just one thing , radius. And actually I guess that picture is a little misleading, where it says radius is actually just an edge, radius is the distance from center to any corner but considering the angles between edges of a hexagon, radius is equal to edge. Hope it is clear.
As I said we’ll calculate all those from radius so let’s have a look at that ;
{
Radius = radius;
Height =2* Radius;
RowHeight =1.5f* Radius;
HalfWidth = (float)Math.Sqrt((Radius * Radius) – ((Radius /2) * (Radius /2)));
Width =2*this.HalfWidth;
ExtraHeight = Height – RowHeight;
}
We’ll do this once at the beginning and use those variable then so we will be able to change hex sizes easily whenever we want to.
Then we need 2 main functions.
1) Calculating pixel coordinates from hex coordinates ( We’ll use hex coordinates mainly since it’s way easier. If we assume Width is 64 for example , (1,0) will become (64,0) )
2) Calculating hex coordinates from pixel coordinates ( We’ll need this for a few different things , for example converting mouse coordinates to hex coordinates so we can detect which hexagon player selected etc. )
Calculating pixel coordinates from hex coordinates
First one is much easier so let’s start with that ; pseudo code for that should be something like this ;
PixelX = (HexX * Width) + ( ( HexY AND 1 ) * ( Width / 2 ) )
PixelY = HexY * RowHeight;
Or in C# ;
(
(position.X * _values.Width) + (((int)position.Y&1) * _values.Width /2),
position.Y* _values.RowHeight
);
Let me explain that a bit ; if you go back and check the hex coordinates image again , you’ll notice the horizontal placement is just increasing by Width every hex but it starts with an offset every other row. So;
PixelX = (HexX * Width)
will draw hexagons one by one as they are in first row ( top row is 0 ) and then
+ ( ( HexY AND 1 ) * ( Width / 2 ) )
will add (Width / 2) offset to every other row. ( 1st, 3rd, 5th etc)
Y coordinate is much simpler, if you check it out carefully you’ll actually see Y increases by Row Height , every row. So it’s just HexY * RowHeight. Easy peasy.
With this function we’ll be able to say “Put a hex to (5,6)” and it’ll calculate the actually pixel coordinates and we won’t need to bother with those calculations again.
Calculating hex coordinates from pixel coordinates
Now this is harder, much harder. Now have a look at this and notice that our hexagonal map actually can be divided into two type of rectangles/squares.
And as you can see those rectangles are “Width” pixel wide and “RowHeight” pixel long. So actually it’s pretty easy to calculate which square you’re in ;
int GridY = (int)py / (int)type.HexValues.RowHeight;
var GridModX = (int)px % (int)type.HexValues.Width;
var GridModY = (int)py % (int)type.HexValues.RowHeight;
gridX and gridY will tell us which tile we’re in and then gridModX and gridModY will tell us the position in that tile.
Now if you check that image again, you’ll see both A and B tiles contains areas belong 3 different hexagons. (2,1) tile for example is type B and contains areas which belongs to green ( top ), blue ( left ), orange ( right ) hexagons. and (2,2) is type A which contains areas belong to blue ( top left ) , orange ( top right ) and green ( bottom )
Now that we already know which tile we’re in and the exact coordinate in that tile , we’ll just find which hexagons contains the point in that tile.
I’m sure you understand the concept so I won’t bother to explain all lines one by one. Here is the first block ;
{
// middle
resultY = gridY;
resultX = gridX;
// left
if (gridModY < (type.HexValues.ExtraHeight – gridModX * m))
{
resultY = gridY –1;
resultX = gridX –1;
}
// right
if (gridModY < (–type.HexValues.ExtraHeight + gridModX * m))
{
resultY = gridY –1;
resultX = gridX;
}
}
Should be pretty straight forward. 3 different results as Type A tiles contains 3 different zones. Middle , left and right.
And if it’s Type B ;
{
if (gridModX >= type.HexValues.HalfWidth)
{
if (gridModY < (2* type.HexValues.ExtraHeight – gridModX * m))
{
// Top
resultY = gridY –1;
resultX = gridX;
}
else
{
// Right
resultY = gridY;
resultX = gridX;
}
}
if (gridModX < type.HexValues.HalfWidth)
{
if (gridModY < (gridModX * m))
{
// Top
resultY = gridY –1;
resultX = gridX;
}
else
{
// Left
resultY = gridY;
resultX = gridX –1;
}
}
}
Actually I guess I missed some extra lines here and there so I’ll just put the whole function too. ( my function works in 3D space though , it’s all same just don’t mind me using Vector3 )
{
var px = pos.X + type.HexValues.HalfWidth;
var py = pos.Z + type.HexValues.Height /2;
int gridX = (int)px / (int)(type.HexValues.Width);
int gridY = (int)py / (int)type.HexValues.RowHeight;
var gridModX = (int)px % (int)type.HexValues.Width;
var gridModY = (int)py % (int)type.HexValues.RowHeight;
bool gridTypeA =false;
if (((int)gridY &1) ==0)
gridTypeA =true;
var resultY = gridY;
var resultX = gridX;
var m = type.HexValues.ExtraHeight / type.HexValues.HalfWidth;
if (gridTypeA)
{
// middle
resultY = gridY;
resultX = gridX;
// left
if (gridModY < (type.HexValues.ExtraHeight – gridModX * m))
{
resultY = gridY –1;
resultX = gridX –1;
}
// right
if (gridModY < (–type.HexValues.ExtraHeight + gridModX * m))
{
resultY = gridY –1;
resultX = gridX;
}
}
else
{
if (gridModX >= type.HexValues.HalfWidth)
{
if (gridModY < (2* type.HexValues.ExtraHeight – gridModX * m))
{
// Top
resultY = gridY –1;
resultX = gridX;
}
else
{
// Right
resultY = gridY;
resultX = gridX;
}
}
if (gridModX < type.HexValues.HalfWidth)
{
if (gridModY < (gridModX * m))
{
// Top
resultY = gridY –1;
resultX = gridX;
}
else
{
// Left
resultY = gridY;
resultX = gridX –1;
}
}
}
returnnewVector3(resultX, 0, resultY);
}
With this function, we’ll be able to say “Mouse is on (150,250) coordinates, go highlight that hexagon” and it’ll convert (150,250) to the hexagon coordinates you need.
Hmm I guess we’re pretty much done right? Oh well I’ll give you just one more function as bonus, the one I use to draw hexagons in 3D world. I won’t explain it though as it’s pretty straight forward. 1 Hexagon = 4 Triangles = 12 vertices
{
var zero =newVertexPositionTexture();
var m = (_values.Height – _values.RowHeight) / _values.Height;
var p = (_values.RowHeight) / _values.Height;
zero.Position =newVector3(MapCoordination.X – _values.HalfWidth, MapCoordination.Y, MapCoordination.Z – _values.Radius /2);
zero.TextureCoordinate.X =0;
zero.TextureCoordinate.Y = m;
_vertices.Add(zero);
zero.Position =newVector3(MapCoordination.X + _values.HalfWidth, MapCoordination.Y, MapCoordination.Z – _values.Radius /2);
zero.TextureCoordinate.X =1;
zero.TextureCoordinate.Y = m;
_vertices.Add(zero);
zero.Position =newVector3(MapCoordination.X – _values.HalfWidth, MapCoordination.Y, MapCoordination.Z + _values.Radius /2);
zero.TextureCoordinate.X =0;
zero.TextureCoordinate.Y = p;
_vertices.Add(zero);
//——————————————
zero.Position =newVector3(MapCoordination.X + _values.HalfWidth, MapCoordination.Y, MapCoordination.Z + _values.Radius /2);
zero.TextureCoordinate.X =1;
zero.TextureCoordinate.Y = p;
_vertices.Add(zero);
zero.Position =newVector3(MapCoordination.X – _values.HalfWidth, MapCoordination.Y, MapCoordination.Z + _values.Radius /2);
zero.TextureCoordinate.X =0;
zero.TextureCoordinate.Y = p;
_vertices.Add(zero);
zero.Position =newVector3(MapCoordination.X + _values.HalfWidth, MapCoordination.Y, MapCoordination.Z – _values.Radius /2);
zero.TextureCoordinate.X =1;
zero.TextureCoordinate.Y = m;
_vertices.Add(zero);
//——————————————
zero.Position =newVector3(MapCoordination.X + _values.HalfWidth, MapCoordination.Y, MapCoordination.Z + _values.Radius /2);
zero.TextureCoordinate.X =1;
zero.TextureCoordinate.Y = p;
_vertices.Add(zero);
zero.Position =newVector3(MapCoordination.X, MapCoordination.Y, MapCoordination.Z + _values.Height /2);
zero.TextureCoordinate.X =0.5f;
zero.TextureCoordinate.Y =1;
_vertices.Add(zero);
zero.Position =newVector3(MapCoordination.X – _values.HalfWidth, MapCoordination.Y, MapCoordination.Z + _values.Radius /2);
zero.TextureCoordinate.X =0;
zero.TextureCoordinate.Y = p;
_vertices.Add(zero);
//——————————————
zero.Position =newVector3(MapCoordination.X – _values.HalfWidth, MapCoordination.Y, MapCoordination.Z – _values.Radius /2);
zero.TextureCoordinate.X =0;
zero.TextureCoordinate.Y = m;
_vertices.Add(zero);
zero.Position =newVector3(MapCoordination.X, MapCoordination.Y, MapCoordination.Z – _values.Height /2);
zero.TextureCoordinate.X =0.5f;
zero.TextureCoordinate.Y =0;
_vertices.Add(zero);
zero.Position =newVector3(MapCoordination.X + _values.HalfWidth, MapCoordination.Y, MapCoordination.Z – _values.Radius /2);
zero.TextureCoordinate.X =1;
zero.TextureCoordinate.Y = m;
_vertices.Add(zero);
}
Guess it may help some of you guys , it’s boring to calculate that vertex position after all. It works pretty well for me but you may need to tweak it a little.
It should be easy to create a hexagonal map structure with all those functions. I hope you guys like it!
Let me know if there is anything you don’t understand or if you have any improvement ideas. See you next time!
Thanks, quite helpfull. Though, it is a bit hard to understand because the code works for both types of hexagons. In this thread I published a simplified version that only works for horizontally alligned hexagons: https://forum.unity.com/threads/mousepos-to-hexagon.226490/.