Slay Tutorial Two- Draw hexagonal maps

Wargames have long used hexagons. Compared to squares they are fairer. In a square if you move vertically or horizontally you move 1 unit, but if you move diagonally you move 1.414… units (the square root of two). With hexagons you move the same in every one of the six possible directions.

That does make hexagons a bit fiddly to use. For a start there are two orientations. Horizontal edge and vertical edge as shown here. Which you use is mostly a question of taste but for our example we’ll use a vertical hexagon. Hexagons Surprisingly mapping hex grids isn’t that difficult. Think of bricks in a brick wall.   You normally stack them like this:

Brick Brick Brick

   Brick  Brick  Brick

Brick Brick Brick

If you look at the central brick (in bold) it is surrounded by six bricks just like a hexagon.

You could build a brick wall (very badly) like this where the bricks line up:

Brick Brick Brick

Brick Brick Brick

Brick Brick Brick

Where the bold brick is surrounded by eight bricks.

So we can use a two-dimension array to hold hexagons except we lose two locations on odd rows and two different locations on even rows. In terms of the 3 x 3 cells, it looks like this

Even Row  Odd Row     Directions

7 0 B      B 0 1        7 0 1

6   2      6   2        6   2

5 4 B      B 4 3        5 4 3

So on odd rows, of the 8 cells around our location only those shows as 0,1,2,3,4,and 6 are used on odd rows and 0,2,3,4,5 and 7 are used even rows. B‘s are blanked.

We can write a function in C to return 1 if a hex is valid and 0 if not. The input is the Y coordinate and a direction (0-7) where 0 is North, 1 = North-East and so on.

int isvalidhex(int y,int dir) {
	if ((y & 1) == 1) /* odd row */
    	{
			if (dir ==5 || dir ==7)
				return 0;
			else
				return 1;
	    }
	else
	    {
			if (dir ==1 || dir ==3)
				return 0;
			else
				return 1;
	    }
}

Hexagon and Map Sizes

The maps in the game are 20 across by 15 deep (small), 30 x 20 across (medium) and 40 across x 30 deep (large). We want these to fit on a screen say 1280 x 960 so a hex size of 34 pixels High by 32 pixels wide almost suits our purposes. It gives a large map of 31 hexagons across by 28 down. The smaller and medium maps also use the same hexagon sizes.

For the game we’ll need the following hexagons all with a thin black border. If you want to make the game more interesting code this as a separate mostly transparent hexagon and have it as an option when drawing the map.

  • Eight coloured hexagons. Pastel colours work out nicely, one for each player (Computer or real).
  • Blue water hexagon.

Curiously I found exactly what I was looking for in this Stack Overflow question. The RGB colours are Purple (913CCD),Salmon (F15F74), Orange (F76D3C), Yellow (F7D842), Cyan (2CA8C2), Green (98CB4A), Grey (839098) and blue (5481E6).I’ve created a blank hexagon with an edge border and then used this to create the game hexagons.
Eight Pastel Colours

Blank hexagonBlue hexagonOrange hexagon You might notice there is one extra hexagon, a dark blue with no border. That’s for the sea around the island where Onslaught takes place.

These are loaded into the game with the LoadTextures() call. It loops through a list of files. I’ve put these in as assets folder and have an array of c-strings giving the path and name of each file.

 

This is what the map looks like. Actually I think the salmon and orange are a little too near so I’ll probably change one of them, perhaps to green.

 

Onslaught linux hexagons screen

Architecture of the program

The source files from this tutorial are on GitHub in the file onslaught1.zip.

Starting with main on line 198, this starts by calling InitSetup() to switch into SDL, set the caption and load the graphics into a textures array. Each hexagon colour has its own texture. A texture is an SDL data structure in VRAM.  InitSetup() calls srand() with the time() function to generate a new map each time. Uncomment srand(0) on the line below to have the same map; this can be useful for debugging.

Next InitMap() is called to initialise the map.

void InitMap(){
	for (int y=0;y<MAPHEIGHT;y++){
		for (int x=0;x<MAPWIDTH;x++){
			map[x][y]= Random(NUMTEXTURES)-1;
		}
	}
}

This sets every location on the map to a number in the range 0-NUMTEXTURES-1.

Next is the GameLoop() function. This sits in a loop and calls DrawHexagons() and then looks to see if any keys are pressed. Press escape to exit. Press n to generate a new map.

DrawHexagons calls a double loop to draw each hexagon. This toggles the variable alt between 1 and 0 on successive rows. This is key to drawing hexagons in the correct place. The function DrawHex(int c, int atx, int aty, int alt) does all the donkey work.  It just blits the specified texture on the screen at the specified coordinates.

DrawHexagons also times the operation and puts it in the caption bar. It uses an external library hr_time to do this calling startTimer and stopTimer. The sources for hr_time/h/.c are included in the zip file.

Compiling onslaught

I developed this on Ubuntu with Visual Studio Code and clang. Included in the zip file is the tasks.json and three other .json files in a .vscode folder.  This is tasks.json below. If your version of clang is a different one, you may need to alter the command. Just changing it to clang might work as both clang and clang-10 commands work from the terminal for me. .

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "shell",
            "label": "clang-10.0 build active file",
            "command": "/usr/bin/clang-10",
            "args": [
                "-g",
                "${file}","${workspaceFolder}/hr_time.c",
                "-o",
                "${fileDirname}/onslaught",  
                "-I/usr/include/SDL2",   
                "-lSDL2",
                "-lSDL2_image"
            ],
            "options": {
                "cwd": "/usr/bin"
            },
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

Conclusion

The main program for this is just over 200 lines long, not too bad. When I run this on my hyper-V Ubuntu I get times of about 65 microseconds to draw the entire map which is not bad.

 

(Visited 180 times, 1 visits today)