« Back to tutorial listing

— Creating an in-game achievement system —
Part 6: Threading

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

Introduction

In our previous part, we implemented networking into our game, so that our earned medals could be uploaded to a server, for viewing online. However, in doing so we introduced an issue - the networking causes our game to lockup for a second when the sync is happening. In this final part, we'll be looking at how to shift this networking logic into a background thread, so it no longer interfers with our game.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./medals06 to run the code. Set your medal key as before, and play the game as normal. Notice how the game no longer pauses when performing network functions. Once you're finished, close the window to exit.

Inspecting the code

SDL has strong suppport for threading, and so this part will be very straightforward and easy to understand. In fact, the only file we need to change is medals.c. Starting with initMedals:


void initMedals(void)
{
	loadMedals();

	medalTextures[MEDAL_BRONZE] = getAtlasImage("gfx/bronzeMedal.png", 1);
	medalTextures[MEDAL_SILVER] = getAtlasImage("gfx/silverMedal.png", 1);
	medalTextures[MEDAL_GOLD] = getAtlasImage("gfx/goldMedal.png", 1);
	medalTextures[MEDAL_RUBY] = getAtlasImage("gfx/rubyMedal.png", 1);
	unearnedMedalTexture = getAtlasImage("gfx/unearnedMedal.png", 1);

	notifyOrder = 1;

	resetAlert();

	doMedalSync = 0;

	mutex = SDL_CreateMutex();

	cond = SDL_CreateCond();

	thread = SDL_CreateThread(networking, "networking", (void*)NULL);

	setupWidgets();
}

We're doing several new things. First, we're setting a variable called doMedalSync to 0. This variable will control whether our background thread is allowed to post our medals. Next, we're calling SDL_CreateMutex and assigning the result to a variable called `mutex`. In networking, a mutex is an exclusive variable (or flag or memory address) that only one thread is allowed to access at a time. We next call SDL_CreateCond, to create a condition variable. Both the mutex and this condition will be working together to handle the background networking, as we'll see shortly.

Finally, we're calling SDL_CreateThread. This is the single call that is responsible for creating our background thread. We're passing over three arguments: `networking`, a pointer to the function that our background thread will run; "networking", the name of the background thread; and NULL. The final argument is a pointer to the data that we'll be passing over to the `networking` function. Since we don't have anything to pass over (our `networking` function doesn't process any arguments), we're passing over NULL.

Moving over to syncMedals now:


void syncMedals(void)
{
	doMedalSync = 1;

	SDL_CondSignal(cond);
}

You will remember that this function previously called postMedals. Now, we're setting doMedalSync to 1 and calling SDL_CondSignal, passing over our `cond` variable. What this means is that we'll be telling our thread to wakeup from its sleep state and go to work. We can see this in action in our networking function:


static int networking(void *p)
{
	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, "Medal networking - Thread started");

	connectToServer();

	while (app.server.connected)
	{
		SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, "Medal networking - Waiting ...");

		SDL_LockMutex(mutex);

		while (!doMedalSync && SDL_CondWait(cond, mutex) == 0) {}

		doMedalSync = 0;

		if (strlen(game.medalKey) > 0)
		{
			postMedals();
		}
	}

	return 0;
}

The networking function is responsible for connecting to the server and handling the postMedals function. As seen in initMedals, it will always run on a separate thread and not interfer with our main game logic.

We're first calling connectToServer, then setting up a while-loop that will repeat while app's server's `connected` flag is 1 (true). We're next calling SDL_LockMutex, so that this background thread can gain an exclusive hold on the mutex. With that done, we're proceeding to setup another while loop. This second while-loop checks for two things: first, that doMedalSync is not 0, and also that SDL_CondWait returns 0. SDL_CondWait pauses our thread and waits for SDL_CondSignal to signal it to wake up. We pass over both our `cond` and `mutex` to SDL_CondSignal. Our mutex must be locked by the current thread in order for this to work, which is why we first call SDL_LockMutex. Once we exit this second while-loop, we're resetting doMedalSync to 0, testing if we have a medalKey set (by testing the length of game's medalKey), and then calling postMedals.

In summary, our networking thread will connect to our server, and then go to sleep until we call syncMedals, to tell it to post our medal data to the server. This stops our background thread from posting data constantly. Using SDL_CondWait also helps us to avoid using a "busy wait" - a while-loop that keeps testing our doMedalSync variable endlessly but not doing anything else. Doing this would be pretty bad, as it could result in high CPU usage and wasted cycles.

Now, the reason for the second while-loop requiring both these conditions is due to a phenomenon known as spurious wakeups. This topic is somewhat controversial, as some believe they are real, while others say they are nothing more than bugs in the programming. It's perhaps worth taking some time to research the topic yourself, so that you can come to your own conclusions. Could it be your own software? Someone else's software? The underlying OS? It may even be a result of a hardware fault. At the end of the day, however, there is no harm in having this extra doMedalSync variable to guard against it. If our thread were to experience a wakeup, it would also require the doMedalSync variable to be 1. This wouldn't be the case, and so our medals wouldn't be posted. The thread would then go back to sleep, as desired.

And that's it! We've successfully created a networked, in-game achievements system. As you can see, it's mostly about testing various stats and other pieces of game data. Nothing more, really. Even if you have no plans to build such a system for yourself, you may still have found this tutorial interesting.

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