« Back to tutorial listing

— A simple turn-based strategy game —
Part 23: Threaded map generation

Note: this tutorial assumes knowledge of C, as well as prior tutorials.

Introduction

While we can generate maps, the way in which we're doing so can cause our game to freeze on startup, with nothing shown on screen. This almost looks like the game has instantly crashed, as we've got an empty window with nothing happening. This will last for as long as the map is being created, which can take several seconds. In this part, we're going to look at generating our map in a background thread, and presenting the player with a screen to say the map is being generated.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS23 to run the code. You will see a window open like the one above, showing a message that the map is being generated, and an animated sequence of squares beneath. After a while, the game will begin, showing three wizards in a maze-like map, as well as a number of ghosts. Play the game as normal. Once you're finished, close the window to exit.

Inspecting the code

Adding in our threaded background map generation is much easier than it might first seem. There are only a handful of considerations that need to be made in order to achieve this.

First, let's update defs.h, and increase the size of our map:


#define MAP_WIDTH                 120
#define MAP_HEIGHT                60

We're going to increase the size by 1.5 times, to make it purposely take a little longer to generate (although, if you have a very powerful computer, this might not make a noticeable difference - try setting the values to 160 and 80, but be prepared to wait).

Next, let's head over to mapGen.c, where we've made a bunch of changes. Starting with generateMap:


void generateMap(void (*_mapGenDone)(void))
{
	mapGenDone = _mapGenDone;

	SDL_CreateThread(generateMapWorker, "generateMapWorker", (void*) NULL);
}

This function is now much shorter than before, and also takes a function pointer as an argument (_mapGenDone). The function pointer being passed in is the function that we'll call once our map generation is finished; so, in effect, it is a callback. We're assigning _mapGenDone to our local mapGenDone variable. Next, we're creating a thread, using SDL_CreateThread. To this, we're passing over a function called generateMapWorker, the name of the thread ("generateMapWorker"), and NULL (as the data to pass to the thread function, that we don't require). What this will do is create a thread that will invoke generateMapWorker. This is a new function that we're using for our map generation.

Let's looks at the generateMapWorker function now:


static int generateMapWorker(void *p)
{
	SDL_Delay(250);

	do
	{
		doCellularAutomata();
	} while (!verifyCapacity());

	growSlime();

	decorate();

	mapGenDone();

	return 0;
}

You'll likely recognise this as the original function for create our map, albeit with a couple of changes. To begin with, we're calling SDL_Delay, with a value of 250, to make the generation pause for a quarter of a second before continuing. This is done on purpose, just to exaggerate the map generation phase, so we can see our generation screen for at least a moment before it vanishes (we can safely remove this delay, without harm).

Our map generation calls are then happening as normal, but after we're finished, we're calling mapGenDone, the callback function we passed into generateMap. We're then returning 0, as a return value is required by SDL_CreateThread.

That's it for threading! But, in order for this to all work correctly, we need to make a few more changes. So, let's head over to stage.c, where we've made updates to handle this new way of creating our map.

Starting with initStage:


void initStage(void)
{
	memset(&stage, 0, sizeof(Stage));

	initEntities();

	initUnits();

	initHud();

	initMap();

	initEffects();

	generateMap(mapGenDone);

	stage.stats.rounds = 1;

	endTimer = FPS * 2;

	app.delegate.logic = genLogic;

	app.delegate.draw = genDraw;
}

We've done a few things here. First, we've removed initPlayer, initAI, and addRandomItems from the function. These all rely on the map having been built, as they will be placing entities within it. We therefore can't do this until our background generation finishes. Note that we're still calling generateMap, but we're now passing over a function called mapGenDone, as is now expected. The other change we've made is that we're now setting our logic and draw delegates as two new functions: genLogic and genDraw. We'll see these in a bit.

Let's look at mapGenDone now:


static void mapGenDone(void)
{
	int i;

	initPlayer();

	initAI();

	for (i = 0 ; i < 10 ; i++)
	{
		addRandomItem();
	}

	app.delegate.logic = logic;

	app.delegate.draw = draw;
}

This is where we've moved initPlayer, initAI, and addRandomItems. Since our map is now completed, we can safely call these functions and have them place the entities inside the map. We're also setting our logic and draw delegates to the original ones that drive our game (`logic` and `draw`).

So, in short, we've deferred the entity placement until after our map is generated, and told the map generation thread to call mapGenDone once it is finished. mapGenDone will then add in our entities and start to process our `logic` and `draw` functions as before. That wasn't too hard, was it?

Let's now look at what genLogic and genDraw do. These are two new function that will be called while the map generation is happening. Let's start with genLogic:


static void genLogic(void)
{
	mapGenTimer += 0.1 * app.deltaTime;
}

We're increasing the value of a variable called mapGenTimer upon each call. This variable is static within stage.c. Not a lot else to talk about.

genDraw is a little more interesting:


static void genDraw(void)
{
	int i, x, n, a, cubes;

	cubes = 5;

	x = (SCREEN_WIDTH - (cubes * 42)) / 2;

	n = ((int) mapGenTimer) % cubes;

	drawText("Generating map. Please wait ...", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 150, 125, 175, 200, TEXT_ALIGN_CENTER, 0);

	for (i = 0 ; i < cubes ; i++)
	{
		a = i == n ? 255 : 64;

		drawRect(x + (i * 42), SCREEN_HEIGHT - 80, 32, 32, 64, 200, 255, a);
	}
}

This is the function that displays "Please wait ..." screen, with the animated squares at the bottom.

We start by setting a variable called `cubes` to 5. This will be the number of squares that are displayed. Next, we're working out how to centre our line of cubes, and assigning that value to `x`. Next, we're taking the value of mapGenTimer, casting it to an int, and taking the modulo of the number of `cubes`, with the result being assigned to `n`. What this means is that as the value of mapGenTimer changes (as it does in genLogic), the value of `n` will move between 0 and 4 (the number of cubes we defined, less 1).

Next, we're rendering the "Generating map" text, and then enter a for-loop to draw all our cubes. For each iteration, we're testing whether `i` (our loop index) is equal to `n`. If so, it means the current cube is the one we want to highlight. We assign a variable called `a` (short for alpha) a value of 255 or 64, depending on whether `i` matches `n`. Finally, we're call drawRect, to draw our cube, passing over `a` to the alpha value, to either makes the cube dark or bright.

Another part wrapped up, and our game is more or less done. We just need to add the finishing touches, which will be some misc. features, as well as music and sound effects, and we're all finished.

Purchase

The source code for all parts of this tutorial (including assets) is available for purchase:

From itch.io

It is also available as part of the SDL2 tutorial bundle:

Desktop site