15,790,526 members
Articles / Multimedia / GDI+
Article

# Hexagonal grid for games and other projects - Part 1

Rate me:
26 Jul 2006CPOL7 min read 305.6K   5.2K   96   23
First version of a hexagonal grid for games or other apps.

## Introduction

The goal of my project is to create a modular, reusable hexagon based map that could be used in simple games and ALife applications. I wanted to leverage as much functionality as possible from .NET, which meant using GDI+ and Forms. Drawing shapes with GDI+ and capturing mouse events with Forms is fairly trivial, which would allow me to spend my programming time solving on more important issues (like hexagon geometry!). This is the first "version" of the hex map, and by no means complete.

## Hexagon

Hexagon based games, whether traditional board games or computer-based, provide more strategic and tactical game-play when compared to simple square based games (like the Checkers game board). The hexagon has six sides, which allows movement in six directions, instead of four. The distance from the center of a hexagon to the center of each neighboring hexagon is equal, which eliminates the distortion of calculating diagonal distance in a traditional square based map. Hexagons are more pleasing to look at, which counts for something, right?

The core of my code is based on the geometry of the hexagon. When I use the word hexagon, I really mean regular hexagon, which is a six-sided polygon where all six sides have the same length. The beauty of the hexagon based map is that you really only need to know one thing: the length of a side of a hexagon. After that, you can calculate everything else you need to know.

If you know the length of side s, then you can calculate r and h. The values for a and b are pretty much irrelevant because you can calculate them from s, r, and h, and you don't really need a and b for any calculations anyway. So, how do you find r and h?

```h = sin( 30°) * s
r = cos( 30°) * s
b = s + 2 * h
a = 2 * r```

## My namespace is Hexagonal

For lack of a better term, I called my namespace `Hexagonal`, and that's where all my core classes live. The class `Math` has a bunch of static methods to handle geometric calculations. Some people may argue that these are trigonometric calculations, but for my purposes, trigonometry is a subset of geometry.

C#
```public static float CalculateH(float side)
{
return ConvertToFloat(System.Math.Sin(DegreesToRadians(30)) * side);
}

public static float CalculateR(float side)
{
return ConvertToFloat(System.Math.Cos(DegreesToRadians(30)) * side);
}
public static double DegreesToRadians(double degrees)
{
return degrees * System.Math.PI / 180;
}```

The `Sin` and `Cos` methods in `System.Math` take arguments in radians, not degrees. So, we need a helper method to convert degrees to radians.

The `Hex` object represents a hexagon. When creating a `Hex` object, you need to know a few things - the length of a side, the x,y coordinates of the upper vertex, and the orientation of the hex. I introduced the concept of orientation so that hexes could be created with the flat side down or a pointy side down. The orientation will affect how the vertices are calculated.

The vertices are numbered somewhat arbitrarily on my part, but we need to refer to vertices in some manner. The important method in `Hex` is `CalculateVertices()`, which is `private` and called by the constructor. I also created an enumeration for hexagonal orientation.

C#
```public class Hex
{
private System.Drawing.PointF[] points;
private float side;
private float h;
private float r;
private Hexagonal.HexOrientation orientation;
private float x;
private float y;
...

private void CalculateVertices()
{
h = Hexagonal.Math.CalculateH(side);
r = Hexagonal.Math.CalculateR(side);
switch (orientation)
{
case Hexagonal.HexOrientation.Flat:
// x,y coordinates are top left point

points = new System.Drawing.PointF[6];
points[0] = new PointF(x, y);
points[1] = new PointF(x + side, y);
points[2] = new PointF(x + side + h, y + r);
points[3] = new PointF(x + side, y + r + r);
points[4] = new PointF(x, y + r + r);
points[5] = new PointF(x - h, y + r );
break;
case Hexagonal.HexOrientation.Pointy:
//x,y coordinates are top center point

points = new System.Drawing.PointF[6];
points[0] = new PointF(x, y);
points[1] = new PointF(x + r, y + h);
points[2] = new PointF(x + r, y + side + h);
points[3] = new PointF(x, y + side + h + h);
points[4] = new PointF(x - r, y + side + h);
points[5] = new PointF(x - r, y + h);
break;
default:
throw new Exception("No HexOrientation defined for Hex object.");
}
}
}

public enum HexOrientation
{
Flat = 0,
Pointy = 1,
}```

The `Hex` class was designed to be simple. All it does is remember its position in two dimensional space. The `Board` class is a collection of `Hex` objects that represent a game board. For this first version, the only type of board that can be created is rectangular. Arranging hexagons in a rectangular shape can be done fairly simply using a two dimensional array. For example, a board with `Flat` orientation would map to a two dimensional array like this:

The most important method in the `Board` class is `Initialize()`, which is `private` and called from the constructor. `Initialize()` creates a two dimensional array of `Hex` objects with all the calculations for the hex vertices.

C#
```public class Board
{
private Hexagonal.Hex[,] hexes;
private int width;
private int height;
private int xOffset;
private int yOffset;
private int side;
private float pixelWidth;
private float pixelHeight;
private Hexagonal.HexOrientation orientation;

...

private void Initialize(int width, int height, int side,
Hexagonal.HexOrientation orientation,
int xOffset, int yOffset)
{
this.width = width;
this.height = height;
this.xOffset = xOffset;
this.yOffset = yOffset;
this.side = side;
this.orientation = orientation;
hexes = new Hex[height, width];
//opposite of what we'd expect

this.boardState = new BoardState();
// short side
float h = Hexagonal.Math.CalculateH(side);
// long side
float r = Hexagonal.Math.CalculateR(side);

//
// Calculate pixel info..remove?
// because of staggering, need to add an extra r/h

float hexWidth = 0;
float hexHeight = 0;
switch (orientation)
{
case HexOrientation.Flat:
hexWidth = side + h;
hexHeight = r + r;
this.pixelWidth = (width * hexWidth) + h;
this.pixelHeight = (height * hexHeight) + r;
break;
case HexOrientation.Pointy:
hexWidth = r + r;
hexHeight = side + h;
this.pixelWidth = (width * hexWidth) + r;
this.pixelHeight = (height * hexHeight) + h;
break;
default:
break;
}

bool inTopRow = false;
bool inBottomRow = false;
bool inLeftColumn = false;
bool inRightColumn = false;
bool isTopLeft = false;
bool isTopRight = false;
bool isBotomLeft = false;
bool isBottomRight = false;

// i = y coordinate (rows), j = x coordinate
//      (columns) of the hex tiles 2D plane

for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
// Set position booleans

#region Position Booleans
if (i == 0)
{
inTopRow = true;
}
else
{
inTopRow = false;
}
if (i == height - 1)
{
inBottomRow = true;
}
else
{
inBottomRow = false;
}
if (j == 0)
{
inLeftColumn = true;
}
else
{
inLeftColumn = false;
}
if (j == width - 1)
{
inRightColumn = true;
}
else
{
inRightColumn = false;
}
if (inTopRow && inLeftColumn)
{
isTopLeft = true;
}
else
{
isTopLeft = false;
}
if (inTopRow && inRightColumn)
{
isTopRight = true;
}
else
{
isTopRight = false;
}
if (inBottomRow && inLeftColumn)
{
isBotomLeft = true;
}
else
{
isBotomLeft = false;
}
if (inBottomRow && inRightColumn)
{
isBottomRight = true;
}
else
{
isBottomRight = false;
}
#endregion

//
// Calculate Hex positions
//

if (isTopLeft)
{
//First hex
switch (orientation)
{
case HexOrientation.Flat:
hexes[0, 0] = new Hex(0 + h + xOffset,
0 + yOffset, side, orientation);
break;
case HexOrientation.Pointy:
hexes[0, 0] = new Hex(0 + r + xOffset,
0 + yOffset, side, orientation);
break;
default:
break;
}
}
else
{
switch (orientation)
{
case HexOrientation.Flat:
if (inLeftColumn)
{
// Calculate from hex above
hexes[i, j] = new Hex(hexes[i - 1, j].
Points[(int)Hexagonal.FlatVertice.BottomLeft],
side, orientation);
}
else
{
// Calculate from Hex to the left
// and need to stagger the columns
if (j % 2 == 0)
{
// Calculate from Hex to left's
// Upper Right Vertice plus h and R offset
float x = hexes[i, j - 1].Points[
(int)Hexagonal.FlatVertice.UpperRight].X;
float y = hexes[i, j - 1].Points[
(int)Hexagonal.FlatVertice.UpperRight].Y;
x += h;
y -= r;
hexes[i, j] = new Hex(x, y, side, orientation);
}
else
{
// Calculate from Hex to left's Middle Right Vertice
hexes[i, j] = new Hex(hexes[i, j - 1].Points[
(int)Hexagonal.FlatVertice.MiddleRight],
side, orientation);
}
}
break;
case HexOrientation.Pointy:
if (inLeftColumn)
{
// Calculate from hex above and need to stagger the rows

if (i % 2 == 0)
{
hexes[i, j] = new Hex(hexes[i - 1, j].Points[
(int)Hexagonal.PointyVertice.BottomLeft],
side, orientation);
}
else
{
hexes[i, j] = new Hex(hexes[i - 1, j].Points[
(int)Hexagonal.PointyVertice.BottomRight],
side, orientation);
}
}
else
{
// Calculate from Hex to the left
float x = hexes[i, j - 1].Points[
(int)Hexagonal.PointyVertice.UpperRight].X;
float y = hexes[i, j - 1].Points[
(int)Hexagonal.PointyVertice.UpperRight].Y;
x += r;
y -= h;
hexes[i, j] = new Hex(x, y, side, orientation);
}
break;
default:
break;
}
}
}
}
}
}

public enum FlatVertice
{
UpperLeft = 0,
UpperRight = 1,
MiddleRight = 2,
BottomRight = 3,
BottomLeft = 4,
MiddleLeft = 5,
}

public enum PointyVertice
{
Top = 0,
UpperRight = 1,
BottomRight = 2,
Bottom = 3,
BottomLeft = 4,
TopLeft = 5,
}```

This method starts by creating a `Hex` at the array position 0,0. After a `Hex` object is created, every other `Hex` can be created because some vertex of a `Hex` is also the vertex of another `Hex`. So, you can loop through the two dimensional array from top to bottom, left to right, creating `Hex`es. The orientation will affect the calculations. I also created enumerations to give friendly names to the vertices. It's important to note that we have a two dimensional array of `Hex` objects, and we're also calculating x,y pixel coordinates for our hexes, so it's easy to get confused when you see x,y or i,j, or 0,0.

The `Hex` and `Board` code above is not complete, you'll have to download the source to view all of it. There's just too much to show it all here. I've shown you the core methods that do the important work. The position booleans in `Board`'s `Initialize()` method are not strictly necessary, and not all of them are used, but I left them in for now.

C#
```public class GraphicsEngine
{
private Hexagonal.Board board;
private float boardPixelWidth;
private float boardPixelHeight;
private int boardXOffset;
private int boardYOffset;
...

public void Draw(Graphics graphics)
{

int width =  Convert.ToInt32(System.Math.Ceiling(board.PixelWidth));
int height = Convert.ToInt32(System.Math.Ceiling(board.PixelHeight));
// seems to be needed to avoid bottom and right from being chopped off

width += 1;
height += 1;

//
// Create drawing objects
//

Bitmap bitmap = new Bitmap(width, height);
Graphics bitmapGraphics = Graphics.FromImage(bitmap);
Pen p = new Pen(Color.Black);
SolidBrush sb = new SolidBrush(Color.Black);

//
// Draw Board background
//

sb = new SolidBrush(board.BoardState.BackgroundColor);
bitmapGraphics.FillRectangle(sb, 0, 0, width, height);

//
// Draw Hex Background
//

for (int i = 0; i < board.Hexes.GetLength(0); i++)
{
for (int j = 0; j < board.Hexes.GetLength(1); j++)
{
bitmapGraphics.FillPolygon(new SolidBrush(board.Hexes[i,j].
HexState.BackgroundColor), board.Hexes[i, j].Points);
}
}

//
// Draw Hex Grid
//

p.Color = board.BoardState.GridColor;
p.Width = board.BoardState.GridPenWidth;
for (int i = 0; i < board.Hexes.GetLength(0); i++)
{
for (int j = 0; j < board.Hexes.GetLength(1); j++)
{
bitmapGraphics.DrawPolygon(p, board.Hexes[i, j].Points);
}
}

//
// Draw Active Hex, if present
//

if (board.BoardState.ActiveHex != null)
{
p.Color = board.BoardState.ActiveHexBorderColor;
p.Width = board.BoardState.ActiveHexBorderWidth;
bitmapGraphics.DrawPolygon(p, board.BoardState.ActiveHex.Points);
}

//
// Draw internal bitmap to screen
//
graphics.DrawImage(bitmap, new Point(this.boardXOffset,
this.boardYOffset));

//
// Release objects
//
bitmapGraphics.Dispose();
bitmap.Dispose();
}```

The `GraphicsEngine` class takes a `Board` object and writes it to the screen using GDI+. I'm not going to take a lot of time to explain GDI+, but the `Draw()` method accepts a `Graphics` object which is derived from a calling form. `Draw()` then writes the `Board` and `Hex`es to a bitmap variable, and finally displays that bitmap to the screen. You'll notice that there are the `HexState` and `BoardState` classes that are properties of the `Hex` and `Board` classes, respectively. The `HexState` and `BoardState` classes contain state type information about the `Hex` or `Board`. In this case, the state information is color. These classes are not strictly necessary, but I wanted to keep the `Hex` and `Board` classes as pure as possible, meaning that they only contain information about geometry and pixels. This way, the stateful information is separated, and can be developed independently.

## Pulling it all together in a Form

To make this all work, you need to create a `Form` with a `GraphicsEngine` object and a `Board` object. Then, create a handler for the form's `Paint` event.

C#
```private void Form_Paint(object sender, PaintEventArgs e)
{
foreach (Control c in this.Controls)
{
c.Refresh();
}
if (graphicsEngine != null)
{
graphicsEngine.Draw(e.Graphics);
}
//Force the next Paint()

this.Invalidate();
}```

Another option is to override the form's `OnPaint` method. I've seen this done both ways, but I've decided to leave `OnPaint` alone. I'm not sure which is the best method, but they both work. Also, the form's `DoubleBuffered` property needs to be set to `true`. This can be done in code or in the designer. Double buffering prevents flicker when you are painting to the screen (set it to `false` and see what happens).

To capture mouse clicks, create a handler for the form's `MouseClick` or `MouseDown` event.

C#
```private void Form_MouseClick(object sender, MouseEventArgs e)
{
if (board != null && graphicsEngine != null)
{
//
// need to account for any offset
//

Point mouseClick = new Point(e.X - graphicsEngine.BoardXOffset,
e.Y - graphicsEngine.BoardYOffset);
Hex clickedHex = board.FindHexMouseClick(mouseClick);
if (clickedHex == null)
{
board.BoardState.ActiveHex = null;
}
else
{
board.BoardState.ActiveHex = clickedHex;
if (e.Button == MouseButtons.Right)
{
clickedHex.HexState.BackgroundColor = Color.Blue;
}
}
}
}```

One of the things the `GraphicsEngine` can do is keep track of an x,y offset so that the `Board` object can be drawn anywhere on the form. If there is an offset, the mouse click needs to account for that offset and pass that new x,y value to the `Board`'s `FindHexMouseClick()` method. The `FindHexMouseClick()` method is very important because it translates x,y pixel coordinates to `Board/Hex` coordinates. There are several ways to convert pixel to hex coordinates, Google "pixel to hexagon". I found a really slick algorithm that will work for any polygon, not just hexagons. The algorithm takes a point and determines if it lies within a polygon by drawing lines through the edges of the polygon. A full description can be found here. My implementation lives in my `Math` class.

C#
```public static bool InsidePolygon(PointF[] polygon, int N, PointF p)
{
int counter = 0;
int i;
double xinters;
PointF p1,p2;
p1 = polygon[0];
for (i=1;i<=N;i++)
{
p2 = polygon[i % N];
if (p.Y > System.Math.Min(p1.Y,p2.Y))
{
if (p.Y <= System.Math.Max(p1.Y,p2.Y))
{
if (p.X <= System.Math.Max(p1.X,p2.X))
{
if (p1.Y != p2.Y)
{
xinters = (p.Y-p1.Y)*(p2.X-p1.X)/(p2.Y-p1.Y)+p1.X;
if (p1.X == p2.X || p.X <= xinters)
counter++;
}
}
}
}
p1 = p2;
}
if (counter % 2 == 0)
return false;
else
return true;
}```

## Conclusion

Please download the source project since it was impossible to include every last line of code in this article. My source is a Visual Studio 2005 console project. The console project actually launches the form. I did this because you can make `Console.WriteLine()` calls from the form and send messages to the console, which is obviously helpful for debugging.

This is the first "version" of the `Hexagonal` namespace. I'd like to add more functionality as I have time. Some of the features I'd like to add are gradient backgrounds and images, scrollable boards, and boards of different shapes. Being able to serialize a `Board` object out to XML for storage would be nice as well. Comments and suggestions welcome.

Written By
Web Developer
United States
Technical Architect
Sungard HE

## Comments and Discussions

 First Prev Next
 thanks Hooman_Kh15-Aug-15 15:41 Hooman_Kh 15-Aug-15 15:41
 My vote of 5 Peter Hawke15-Jun-13 16:52 Peter Hawke 15-Jun-13 16:52
 Insert MessageBox Member 993771328-Mar-13 7:58 Member 9937713 28-Mar-13 7:58
 difficulty in understanding the code .. garima alreja4-Dec-12 6:57 garima alreja 4-Dec-12 6:57
 Bug in source code qweras12325-Sep-12 12:17 qweras123 25-Sep-12 12:17
 How to change the board shape to hexagon Tonielro17-Jul-12 12:25 Tonielro 17-Jul-12 12:25
 My vote of 4 avinash8419-Jan-12 0:20 avinash84 19-Jan-12 0:20
 My vote of 5 msickle1-Jan-12 12:47 msickle 1-Jan-12 12:47
 There might be a simpler way Member 38142078-Apr-11 1:05 Member 3814207 8-Apr-11 1:05
 My vote of 5 daviger23-Feb-11 6:06 daviger 23-Feb-11 6:06
 Hexagonal Touch Tiago Conceição13-Sep-08 14:53 Tiago Conceição 13-Sep-08 14:53
 Excellent Article. Nic Rowan24-Sep-07 23:43 Nic Rowan 24-Sep-07 23:43
 Nicely done Michael Potter19-Sep-07 9:51 Michael Potter 19-Sep-07 9:51
 Very useful psiclopz22-May-07 7:14 psiclopz 22-May-07 7:14
 How is the next version coming? jjwilson615-Feb-07 18:34 jjwilson61 5-Feb-07 18:34
 Re: How is the next version coming? Jeff Modzel6-Feb-07 3:43 Jeff Modzel 6-Feb-07 3:43
 Re: How is the next version coming? jjwilson617-Feb-07 13:02 jjwilson61 7-Feb-07 13:02
 Re: How is the next version coming? Member 1403010724-Nov-18 14:05 Member 14030107 24-Nov-18 14:05
 Impressive! Ross Holder6-Jan-07 12:32 Ross Holder 6-Jan-07 12:32
 Re: Impressive! Jeff Modzel8-Jan-07 3:18 Jeff Modzel 8-Jan-07 3:18
 Re: Impressive! Ross Holder4-Jun-09 18:47 Ross Holder 4-Jun-09 18:47