Article: Hexagonal Coordinates
by Blecki
This article describes how to use hexagonal coordinates and is provided with a sample project.
Hexagon Dimensions

First, we will calculate the dimensions of various parts of a hexagon. To calculate the width and halfWidth, we use the pythagorean theroem.
class Hex
{
private float radius;
private float width;
private float halfWidth;
private float height;
private float rowHeight;
public Hex(float radius)
{
this.radius = radius;
this.height = 2 * radius;
this.rowHeight = 1.5f * radius;
this.halfWidth = (float)Math.Sqrt((radius * radius) - ((radius / 2) * (radius / 2)));
this.width = 2 * this.halfWidth;
}

Coordinate System
Origin and Center
We will use Vector2s as our hex tile map coordinates. Tile map coordinates increase to the right and up.
We will need to turn tile map coordinates into world coordinates. There are two points that make logical tile origins. There is the center of the tile, and there is the point outside the hex, as in the diagram.
Whichever you use, the value is the same. All you're changing is which of these you position at 0,0. Be consistent in your drawing code, and these two values will be the same. This does make a different later, in the conversion from world coordinates back to tile coordinates. The origin point is the easier to work with there, so that's what we'll use here.

We must account for every other row being offset half a hex width.
Notice that we use the rowHeight value, not the height value. This accounts for the way Hexagons interlock.
public Vector2 TileOrigin(Vector2 tileCoordinate)
{
return new Vector2(
(tileCoordinate.X * width) + ((tileCoordinate.Y % 2 == 1) ? halfWidth : 0),
tileCoordinate.Y * rowHeight);
}
We'll go ahead and calculate the center based on the origin.
public Vector2 TileCenter(Vector2 tileCoordinate)
{
return TileOrigin(tileCoordinate) + new Vector2(halfWidth, height/2);
}
Directions

The chief advantage of a hex map over a square map is that there are six neighbors all the same distance away, rather than only four. We will first create an enumeration to represent our six directions, then we will define some functions for manipulating them.
public enum Direction
{
NorthEast,
East,
SouthEast,
SouthWest,
West,
NorthWest,
NumberOfDirections,
}
Now we will add a few functions to manipulate directions.
public static Direction RotateDirection(Direction direction, int amount)
{
//Let's make sure our directions stay within the enumerated values.
if (direction < Direction.NorthEast ||
direction > Direction.NorthWest ||
Math.Abs(amount) > (int)Direction.NorthWest)
{
throw new InvalidOperationException("Directions out of range.");
}
direction += amount;
//Now we need to make sure direction stays within the proper range.
//C# does not allow modulus operations on enums, so we have to convert to and from int.
int n_dir = (int)direction % (int)Direction.NumberOfDirections;
if (n_dir < 0) n_dir = (int)Direction.NumberOfDirections + n_dir;
direction = (Direction)n_dir;
return direction;
}
Finding the opposite is probably the simplest.
public static Direction Opposite(Direction direction)
{
return RotateDirection(direction, 3);
}
Now that we have directions, we can figure out who our neighbors are. We must again account for odd numbered rows being different. The algorithm is pretty simple. We must simply apply a different set of offsets per direction depending on whether this is an odd or even row.
public static Vector2 Neighbor(Vector2 tile, Direction direction)
{
if (tile.Y % 2 == 0) //Is this row even?
{
Even Neighbors
switch(direction)
{
case Direction.NorthEast : tile.Y += 1; break;
case Direction.East : tile.X += 1; break;
case Direction.SouthEast: tile.Y -= 1; break;
case Direction.SouthWest: tile.Y -= 1; tile.X -= 1; break;
case Direction.West: tile.X -= 1; break;
case Direction.NorthWest: tile.X -= 1; tile.Y += 1; break;
default: throw new InvalidOperationException("Invalid direction");
}
}
else //This is an odd row.
{
Odd Neighbors
switch (direction)
{
case Direction.NorthEast: tile.X += 1; tile.Y += 1; break;
case Direction.East: tile.X += 1; break;
case Direction.SouthEast: tile.X += 1; tile.Y -= 1; break;
case Direction.SouthWest: tile.Y -= 1;; break;
case Direction.West: tile.X -= 1; break;
case Direction.NorthWest: tile.Y += 1; break;
default: throw new InvalidOperationException("Invalid direction");
}
}
return tile;
}
Unfortunately, as you can see, iterating over tiles in a specific direction is not as simple as incrementing values in the tile coordinate. To iterate in a specific direction, use a loop such as
for (Vector2 coordinate = new Vector(0,0); /*end condition*/;
coordinate = Hex.Neighbor(coordinate, Hex.Direction.NorthEast))

There's not much left. All we need to do now is get a tile coordinate back from a world coordinate. This requires a little bit of linear algebra, and once again we are complicated by odd rows.
public Vector2 TileAt(Vector2 worldCoordinate)
{
First we will calculate a few constants to make the rest of code simpler.
float rise = height - rowHeight; float slope = rise / halfWidth;
World to Tile
The first step is to find our position in a square grid. This grid allows us to divide the hex map into two types of tiles.
int X = (int)Math.Floor(worldCoordinate.X / width); int Y = (int)Math.Floor(worldCoordinate.Y / rowHeight);
Now we find the offset of the real point from the corner of this square grid section.
Vector2 offset = new Vector2(worldCoordinate.X - X * width, worldCoordinate.Y - Y * rowHeight);
if (Y % 2 == 0) //Is this an even row?
{
//Section type A
Looking at the diagram for section A, we can see that two hexagons poke into the bottom of the square. We plug the offset's X value into the equation for the line of the top of those hexes at the bottom. If you've taken algebra, you know that the equation of a line like this is Y=MX+B. That's why we calculated the rise and slope earlier. The line on the left of section A has a negative slope, and a Y intercept of rise. The other has a positive slope, and a Y intercept of negative rise. We adjust the X,Y tile coordinates if we discover that the point is below one of these lines. This is the same adjustment used for finding neighbors.
//Point is below left line; inside SouthWest neighbor
if (offset.Y < (-slope * offset.X + rise))
{
X -= 1;
Y -= 1;
}
//Point is below right line; inside SouthEast neighbor
else if (offset.Y < (slope * offset.X - rise))
{
Y -= 1;
}
else
{
//Section type B
Section B is slightly more complex. First we determine if the point is in the right or left section. Since odd rows are offset by halfWidth, the large section on the right has the same coordinates as the square grid.
if (offset.X >= halfWidth) //Is the point on the right side?
{
if (offset.Y < (-slope * offset.X + rise * 2.0f))
//Point is below bottom line; inside SouthWest neighbor.
Y -= 1;
}
else //Point is on the left side
{
if (offset.Y < (slope * offset.X))
//Point is below the bottom line; inside SouthWest neighbor.
Y -= 1;
else //Point is above the bottom line; inside West neighbor.
X -= 1;
}
}
return new Vector2(X, Y);
}
Here's a complete class, ready to handle Hex coordinate systems.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
namespace HexDemo
{
class Hex
{
private float radius;
private float width;
private float halfWidth;
private float height;
private float rowHeight;
public Hex(float radius)
{
this.radius = radius;
this.height = 2 * radius;
this.rowHeight = 1.5f * radius;
this.halfWidth = (float)Math.Sqrt((radius * radius) - ((radius / 2) * (radius / 2)));
this.width = 2 * this.halfWidth;
}
public Vector2 TileOrigin(Vector2 tileCoordinate)
{
return new Vector2(
(tileCoordinate.X * width) + ((tileCoordinate.Y % 2 == 1) ? halfWidth : 0), //Y % 2 == 1 is asking 'Is Y odd?'
tileCoordinate.Y * rowHeight);
}
public Vector2 TileCenter(Vector2 tileCoordinate)
{
return TileOrigin(tileCoordinate) + new Vector2(halfWidth, halfWidth);
}
public enum Direction
{
NorthEast,
East,
SouthEast,
SouthWest,
West,
NorthWest,
NumberOfDirections,
}
public static Direction RotateDirection(Direction direction, int amount)
{
//Let's make sure our directions stay within the enumerated values.
if (direction < Direction.NorthEast || direction > Direction.NorthWest || Math.Abs(amount) > (int)Direction.NorthWest)
throw new InvalidOperationException("Directions out of range.");
direction += amount;
//Now we need to make sure direction stays within the proper range.
//C# does not allow modulus operations on enums, so we have to convert to and from int.
int n_dir = (int)direction % (int)Direction.NumberOfDirections;
if (n_dir < 0) n_dir = (int)Direction.NumberOfDirections + n_dir;
direction = (Direction)n_dir;
return direction;
}
public static Direction Opposite(Direction direction) { return RotateDirection(direction, 3); }
public static Vector2 Neighbor(Vector2 tile, Direction direction)
{
if (tile.Y % 2 == 0) //Is this row even?
{
switch (direction)
{
case Direction.NorthEast: tile.Y += 1; break;
case Direction.East: tile.X += 1; break;
case Direction.SouthEast: tile.Y -= 1; break;
case Direction.SouthWest: tile.Y -= 1; tile.X -= 1; break;
case Direction.West: tile.X -= 1; break;
case Direction.NorthWest: tile.X -= 1; tile.Y += 1; break;
default: throw new InvalidOperationException("Invalid direction");
}
}
else //This is an odd row.
{
switch (direction)
{
case Direction.NorthEast: tile.X += 1; tile.Y += 1; break;
case Direction.East: tile.X += 1; break;
case Direction.SouthEast: tile.X += 1; tile.Y -= 1; break;
case Direction.SouthWest: tile.Y -= 1; ; break;
case Direction.West: tile.X -= 1; break;
case Direction.NorthWest: tile.Y += 1; break;
default: throw new InvalidOperationException("Invalid direction");
}
}
return tile;
}
public Vector2 TileAt(Vector2 worldCoordinate)
{
float rise = height - rowHeight;
float slope = rise / halfWidth;
int X = (int)Math.Floor(worldCoordinate.X / width);
int Y = (int)Math.Floor(worldCoordinate.Y / rowHeight);
Vector2 offset = new Vector2(worldCoordinate.X - X * width, worldCoordinate.Y - Y * rowHeight);
if (Y % 2 == 0) //Is this an even row?
{
//Section type A
if (offset.Y < (-slope * offset.X + rise)) //Point is below left line; inside SouthWest neighbor.
{
X -= 1;
Y -= 1;
}
else if (offset.Y < (slope * offset.X - rise)) //Point is below right line; inside SouthEast neighbor.
Y -= 1;
}
else
{
//Section type B
if (offset.X >= halfWidth) //Is the point on the right side?
{
if (offset.Y < (-slope * offset.X + rise * 2.0f)) //Point is below bottom line; inside SouthWest neighbor.
Y -= 1;
}
else //Point is on the left side
{
if (offset.Y < (slope * offset.X)) //Point is below the bottom line; inside SouthWest neighbor.
Y -= 1;
else //Point is above the bottom line; inside West neighbor.
X -= 1;
}
}
return new Vector2(X, Y);
}
}
}
Download the sample code here :- Link
April 1st, 2010 - 03:50
the draw code in the sample is a bit slow when the tiles begin to mount up, do you have suggestions for optimization of the draw code?