Slay Tutorial Four – Adding forts etc

This is the fourth tutorial for the Slay game on Linux (called Onslaught). Follow the link for the first tutorial in this series.  In this tutorial, I’ll add forts, trees and wild areas into the map.

Here’s a quick run down on how these play in the game. First we need some new graphics.

FortFort

Every player area with two or more contiguous hexes has a fort. If two or more player areas coalesce into one area then one fort is picked from the coalescing areas, and the rest are removed. Only a spearman (6 points), Knight (18) or Baron (54) can move next to or onto an enemy Fort. You cannot put your pieces or move them onto one of your forts,

Wild AreaWild Areas

When a unit starves it is replaced by a grave stone. GravestoneThen one turn later, the grave stones are replaced by a wild area. Each turn after there is a 50% chance that a wild area will spread into each adjacent empty hex.

Wild areas are bad because any hex of yours that is wild does not provide support; it is not counted. Given that they spread so rapidly, you really want to keep them under control. by moving a unit into each wild hex. Add wild areas to 3%.

Trees Tree

These are placed randomly on the map at the start. Approximately 10% of the map has trees.  Any empty hex next to two tree hexes will become a tree hex. Moving a unit 9onto a tree hex will remove it.

Each of these has a byte in the hex struct. I could have used a single bit as I’m only interested in false/true but memory is cheap and a byte each isn’t that wasteful. I’ve set them to 0 in the ClearMap() function.

Adding Trees and Wild

This is fairly easy. First I scan the map looking for land hexes and count them up. Then I take 10% for trees and 3% for wilds. Then a couple of while loops adds trees and wild. I’ve copied the FindRandomLand() and called it FindRandomLand2(). This looks for any hex with a .island == ltland but doesn’t check the continent number in the .map field.

Adding forts is harder as there has to be one fort for every block; that is a group of hexes all contiguous and the same colour.

The SetPlayers() function starts by populating an array hexCount() with the number of hexes for each player.  This is typically 60-65 depending on map size.

It then scans the whole map looking for empty land squares with a blockId of 0.

This is the hex struct now.

struct hex {
	int player;       // 1-8 fr player or 0 for sea, use for drawing hexes-
	int continent; 
	landtype island;  // ltempty, ltland or ltsea
	char wild;
	char fort;
	char tree;
	char blockId;
};

The blockId field is used to identify a block of hexes belonging to a player. These blocks are the little empires that a player starts with. One hex in each block is designated as the starting fort. It has the fort set to 1.

Originally I had one function allocates block Ids, set fort etc but hit a bit of a road bump. Each block was 2-4 hexs in size, but blocks for the same player could be put next to each other and so I ended up with a larger block with 2 or 3 forts and having 2 or 3 different blockIds. Not a good thing.

To get round this, I added players, then blocks and forts in a separate call.

Setting Player Blocks

The SetPlayers() function is actually quite simple. It starts by dividing all land hexes equally between all players in the array hexCount. It then looks at every empty (.player ==0 ) land square on the map in a double loop. It then picks a random player with some hexes left to allocate. It then calls AllocatePlayerHexes() to assign between 2 and 4 hexes to that player.

AllocatePlayerHexes() sets the player field in each hex. Originally I used for (dir=0;dir<8;dir++) to loop through the eight locations around each hex, but I found this gave maps with very odd looking allocations. So I replaced with a While loop. This picks a random direction and an array dirBits which is pre-set to powers of 2. By subtracting these from dirBit (which starts at 255- all 8 bits set to 1), it can pick directions at random and recursively calls itself. However it can only run for the size of the block otherwise it would fill in the whole map. Also if it has set the player for its quote of hexes it stops. This makes sure all players start with the same number of owned hexes.

Global variable or pass by pointer?

I have used a couple of global variables but generally I try not to. In AllocatePlayerHexes(), I have two variables that I need to track. Count, which represents how many hexes there are in a block and hexesperplayer which is a master count.  Both of these need to be accessed from within the function so I pass them both in as int *.

This is why code like this, which looks a bit odd is used. Note the use of brackets so the contents of what the pointer is pointing to is affected not the pointer itself.

(*count)--;
(*hexesperplayer)--;
if (*count > *hexesperplayer) {
	*count = *hexesperplayer;
}
if (*count == 0) return;

Debugging

To check that my blockid code was correctly setting blocks, I added a DebugMode flag, toggled by pressing the Tab key. When set, the DrawHexagons() function doesn’t draw forts, wild or trees but instead prints the blockid in each hex as the picture below shows. To make this work I added SDL_TTF.h to the list of #includes then added a texture for a surface and one for the output.
Debug Mode
To Draw text using SDL_TTF you need a SDL_Surface. This is a block of RAM that the text is output to. To display it, it needs to be copied into a Texture.

This is the code in DrawHexagons() that does it.

  1. Output the text to buff using snprintf_s.
  2. Render buff to a surface.
  3. Create a texture from the surface (ie copy the image from RAM to video RAM)
  4. Copy the video RAM to the screen Video RAM
if (pl>0) {
	sprintf_s(buff, sizeof(buff), "%3d", map[ix][iy].blockId);
	SDL_Rect textRect = { 0,0,w,h };
	if (!(surface = TTF_RenderText_Solid(font, buff, black))) {
		LogError("TTF_RenderText failed", (char*)TTF_GetError());
		return;
	}
	ttfTexture = SDL_CreateTextureFromSurface(renderer, surface);
	target.x -= 5;
	SDL_RenderCopy(renderer, ttfTexture, &hex, &target);
}

The font was loaded earlier in a function LoadFont(() that loads the font from a ttf file and black is an SDL_Color. A bit of trial and error found that subtracting five from the target.x position gave a reasonable centering of the text in the hexagon.

The caption shows roughly how long each frame takes to draw. When DebugMode is 1, it takes typically 0.03 seconds to draw compared to 0.0003 without it.

Fixing the bug from the previous tutorial

This was the bug that was leaving individual hexes. It turned out I had been calling isvalidhex() with the wrong parameters. There is still a minor bug in the map generator where sometimes an island comprising several hexes is left separated by one sea hex from the main continent. This I believe is the map generator not calling isvalidhex() on some occasions.

Another bug I found was toggling between debug mode and off a number of times seems to lock it up for a while. I added a call to SDL_DestroyTexture to go with the SDL_CreateTextureFromSurface and that fixed the bug and made it faster so in DebugMode it only took 0.012 seconds to draw rather than the 0.03.

I’ve zipped up the source code and uploaded it to GitHub as Onslaught3.zip.  It has been compiled and run on both Windows and Ubuntu.

Compiling on Linux

Not the .vscode folder has the JSON files needed for VS Code with the C/C++ extension.  There was one addition to tasks.json; this line

 "-lSDL2_ttf"

This is needed to link in the SDL_ttf code.

(Visited 12 times, 1 visits today)