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.

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.

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: 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: 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.

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]; this .boardState = new BoardState(); float h = Hexagonal.Math.CalculateH(side); float r = Hexagonal.Math.CalculateR(side); 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 ; for ( int i = 0 ; i < height; i++) { for ( int j = 0 ; j < width; j++) { #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 if (isTopLeft) { 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) { hexes[i, j] = new Hex(hexes[i - 1 , j]. Points[( int )Hexagonal.FlatVertice.BottomLeft], side, orientation); } else { if (j % 2 == 0 ) { 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 { hexes[i, j] = new Hex(hexes[i, j - 1 ].Points[ ( int )Hexagonal.FlatVertice.MiddleRight], side, orientation); } } break ; case HexOrientation.Pointy: if (inLeftColumn) { 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 { 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.

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)); width += 1 ; height += 1 ; Bitmap bitmap = new Bitmap(width, height); Graphics bitmapGraphics = Graphics.FromImage(bitmap); Pen p = new Pen(Color.Black); SolidBrush sb = new SolidBrush(Color.Black); sb = new SolidBrush(board.BoardState.BackgroundColor); bitmapGraphics.FillRectangle(sb, 0 , 0 , width, height); 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); } } 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); } } if (board.BoardState.ActiveHex != null ) { p.Color = board.BoardState.ActiveHexBorderColor; p.Width = board.BoardState.ActiveHexBorderWidth; bitmapGraphics.DrawPolygon(p, board.BoardState.ActiveHex.Points); } graphics.DrawImage(bitmap, new Point( this .boardXOffset, this .boardYOffset)); 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.

private void Form_Paint( object sender, PaintEventArgs e) { foreach (Control c in this .Controls) { c.Refresh(); } if (graphicsEngine != null ) { graphicsEngine.Draw(e.Graphics); } 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.

private void Form_MouseClick( object sender, MouseEventArgs e) { if (board != null && graphicsEngine != null ) { 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.

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.