PC Games

Orb
Lasagne Monsters
Three Guys Apocalypse
Water Closet
Blob Wars : Attrition
The Legend of Edgar
TBFTSS: The Pandoran War
Three Guys
Blob Wars : Blob and Conquer
Blob Wars : Metal Blob Solid
Project: Starfighter
TANX Squadron

Tutorials

2D shoot 'em up
2D top-down shooter
2D platform game
Sprite atlas tutorial
Working with TTF fonts
2D adventure game
Widget tutorial
2D shoot 'em up sequel
2D run and gun
Roguelike
Medals (Achievements)
2D turn-based strategy game
2D isometric game
2D map editor
2D mission-based shoot 'em up
2D Santa game
2D split screen game
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Versus game tutorial
Wed, 20th March 2024

Download keys for SDL2 tutorials on itch.io
Sat, 16th March 2024

The Legend of Edgar 1.37
Mon, 1st January 2024

SDL2 Santa game tutorial 🎅
Thu, 23rd November 2023

SDL2 Shooter 3 tutorial
Wed, 15th February 2023

All Updates »

Tags

android (3)
battle-for-the-solar-system (10)
blob-wars (10)
brexit (1)
code (6)
edgar (9)
games (43)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (17)
water-closet (4)

Books


The Attribute of the Strong (Battle for the Solar System, #3)

The Pandoran War is nearing its end... and the Senate's Mistake have all but won. Leaving a galaxy in ruin behind them, they set their sights on Sol and prepare to finish their twelve year Mission. All seems lost. But in the final forty-eight hours, while hunting for the elusive Zackaria, the White Knights make a discovery in the former Mitikas Empire that could herald one last chance at victory.

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating an in-game achievement system —
Part 5: Online functionality

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

Introduction

Services such as Steam, PSN, and Xbox all allow for their achievements and trophies to be synced online. In this part of the tutorial, we're going to demonstrate how this can be done using SDL Net. In order to make full use of this tutorial, you will need to create a medal key. Head over to the Medals page and scroll down to the Create Medal Key section. Enter a name or id (whatever you like) into the input box, and click "Create Key". A 16 character key will be displayed below. Make sure you copy it and keep it somewhere safe, as there is no way to retrieve it again (although, you could always just create another key..!). Now armed with your key, you can try out the game:

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./medals05 to run the code. In the main menu, select Options, scroll down to Medal Key, and enter your medal key. If entering it here is too cumbersome, you can also edit the save.json file. You will need to start the game at least once for it to be created. Make sure the game is not running when you edit the save.json. Locate the "medalKey" in the JSON data and enter your medal key there, then start the game. Ensure in the Options screen that the status beneath the medal key says OK. If not, check your internet connection and also that your medal key is correct. Play the game as normal, earning medals. Should all go to plan, you will be able to see your medal online at:

https://www.parallelrealities.co.uk/games/medals/player/{playerId}/

where {playerId} is the name or id your first entered when you created your medal key. You can also find a list of players on the Medals page. When you're finished, close the window to exit.

Inspecting the code

Before we begin, something very important to keep in mind - at the time of writing (November 2021) SDL Net cannot talk to SSL connections (such as https). If you need to communicate over such a protocol, you will need to use a different approach, such as libcurl. This tutorial part talks to a standard (unsecured) http port.

If we first turn our attention to structs.h, we've made an addition to Game:


typedef struct {
	int soundVolume;
	int musicVolume;
	double stats[STAT_MAX];
	int stageRanks[NUM_STAGES];
	Medal medalsHead;
	char medalKey[MAX_NAME_LENGTH];
} Game;

We've added a field called medalKey, that will hold our medalKey.

Next, we've updated App:


typedef struct {
	struct {
		void (*logic)(void);
		void (*draw)(void);
	} delegate;
	SDL_Renderer *renderer;
	SDL_Window *window;
	int keyboard[MAX_KEYBOARD_KEYS];
	double deltaTime;
	double fontScale;
	Widget *activeWidget;
	char inputText[MAX_INPUT_LENGTH];
	struct {
		int connected;
		long lastSync;
		int lastResponseCode;
	} server;
	struct {
		int fps;
	} dev;
} App;

We've added an inner struct called `server`. This struct contains three fields: `connected`, which will tell use whether we're connected to the medal server; lastSync, which will hold the time that the game last sent medal data to the server; lastResponseCode, which will contain the last http response code from the server when we synced.

Moving over to medals.c now, we've made a number of updates and additions to the code to support our online features. 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();

	setupWidgets();

	connectToServer();
}

At the end of the function, we're calling a new function called connectToServer. This will, as the name suggests, connect to the medals server once the medal data has been loaded (at game startup).

The next function is syncMedals:


void syncMedals(void)
{
	postMedals();
}

This function does one thing - calls postMedals. This function is used throughout the code when we want to sync our medals, such as when we startup the game, complete a level, etc. The reason we're not calling postMedals directly will be expanded upon in the next part of this tutorial.

We now come to connectToServer:


static void connectToServer(void)
{
	app.server.lastResponseCode = 503;

	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, "Medal networking - Connecting to server ...");

	app.server.connected = SDLNet_ResolveHost(&ip, HOST, PORT) == 0;

	if (!app.server.connected)
	{
		SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "Medal networking - Failed to connect to server.");
	}
}

This is the function that performs our initial connection to our server. We start by setting app's server's lastResponseCode to 503 (HTTP Service Unavailable); all our response codes will align to HTTP status codes, for ease of handling. We then call SDLNet_ResolveHost, passing in a reference to `ip` (an SDL IPaddress struct, static within medals.c), and the host and port we wish to connect to. HOST is defined as "parallelrealities.co.uk" and PORT as 5556. We're expecting this call to return 0. If it doesn't, it means there was an error. We're assigning the result of the call to app's server's connected variable. Note that SDLNet_ResolveHost does not actually open any sockets, but does tell us whether it was able to prepare the networking system. You can test this by disconnecting from the internet and then running the code. You will see the warning printed out to the console as shown at the end of the function. Basically, we'll be offline, so we can't use the medal system.

The next function is postMedals:


static void postMedals(void)
{
	TCPsocket socket;
	char *request, response[MAX_LINE_LENGTH];
	unsigned int len;

	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, "Medal networking - Syncing ...");

	app.server.lastResponseCode = -1;

	socket = SDLNet_TCP_Open(&ip);

	if (socket != NULL)
	{
		memset(response, 0, MAX_LINE_LENGTH);

		request = createMedalJSONData();

		len = strlen(request);

		if (SDLNet_TCP_Send(socket, request, len) == len)
		{
			if (SDLNet_TCP_Recv(socket, response, MAX_LINE_LENGTH) > 0)
			{
				app.server.lastResponseCode = getResponseCode(response);
			}
		}

		SDLNet_TCP_Close(socket);

		free(request);

		if (app.server.lastResponseCode == 200)
		{
			app.server.lastSync = time(NULL);
		}
		else
		{
			SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "Medal networking - Sync failed: %d.", app.server.lastResponseCode);
		}
	}

	if (app.server.lastResponseCode == -1)
	{
		app.server.lastResponseCode = 503;
	}
}

This is the function that performs the actual sending of data over the our remote server.

We start by setting app's server's lastResponseCode to -1, to let our code know we're currently communicating with the server. We're then calling SDLNet_TCP_Open, passing over `ip` that we setup in connectToServer, and assigning the result to a variable called `socket`. SDLNet_TCP_Open will open a socket to the remote server. We then test if socket is not NULL, and proceed to post the data if not. We memset a variable called `response`, that will hold the response data from the server. It can hold quite a lot of return data; MAX_LINE_LENGTH is defined as 1024 bytes, even though the response from our server is much smaller than that. Next, we call a function named createMedalJSONData and assign the result to a variable called `request`. This function does as its name implies - it creates a JSON string containing all our medal JSON. We'll see more on this in a bit. Now that we have our request data, we use strlen to find its length, and assign it to a variable named `len`.

We now have all we need to send our medal data. We send it to the server by calling SDLNet_TCP_Send, passing over `socket`, the request data, and the length of the data. SDLNet_TCP_Send returns how much data was sent, and so we compare that with `len`, to ensure that the amount of data we wanted to send was the amount of data that was actually sent. If so, we're then calling SDLNet_TCP_Recv, again passing over `socket`, the response char array, and the length of the array. SDLNet_TCP_Recv will fill `response` with the response data from the server, up to a maximum length of the size of MAX_LINE_LENGTH (1024 bytes). SDLNet_TCP_Recv will return how much data was read. If the value was more than 0, we got some data back. We're next calling getResponseCode, passing over `response`, and assigning the result to app's server's lastResponseCode.

With all that done, we've now completed our sending and receiving of data. We call SDLNet_TCP_Close, passing over `socket` to close it, and then free `request` (our medal data). We next test the value of app's server's lastResponseCode. If it's 200, everything's good, so we set app's server's lastSync to the current time. Otherwise, we'll print a warning that things didn't go right. Finally, we're testing to see if app's server's lastResponseCode is -1. If so, it means our sending and receiving failed at some point, in which case we'll set lastResponseCode to 503 (HTTP Service Unavailable).

So, in summary, this function is opening a socket to our server, sending across our medal data, and then receiving and recording the response code.

The createMedalJSONData function is next:


static char *createMedalJSONData(void)
{
	char *out, key[MAX_NAME_LENGTH * 2];
	Medal *m;
	cJSON *root, *items, *item;

	root = cJSON_CreateObject();

	cJSON_AddItemToObject(root, "medalKey", cJSON_CreateString(game.medalKey));
	cJSON_AddItemToObject(root, "game", cJSON_CreateString("UFO Rescue!"));

	items = cJSON_CreateArray();

	for (m = game.medalsHead.next ; m != NULL ; m = m->next)
	{
		if (m->awardDate > 0)
		{
			item = cJSON_CreateObject();

			sprintf(key, "UFO_%s", m->id);

			cJSON_AddStringToObject(item, "id", key);
			cJSON_AddNumberToObject(item, "awardDate", m->awardDate);

			cJSON_AddItemToArray(items, item);
		}
	}

	cJSON_AddItemToObject(root, "medals", items);

	out = cJSON_PrintUnformatted(root);

	cJSON_Delete(root);

	return out;
}

This function creates a JSON object to send across to our server, holding our medal data. We start by creating a JSON object and assigning it to a variable named `root`. We next set two fields: "medalKey", that is our medal key; and "game", which is the name of the game this medal data is for. Next, we create a JSON array, and assign it to a variable called `items`. We're then looping through all our medals. If we find one with an awardDate of greater than 0 (it has been awarded to the player), we're proceeding to create a JSON object for it. Our server expects our medals to be identified with a prefix of "UFO_" plus the `id` of the medal. We therefore use sprintf to create such a string, using a char array called key. With that done, we set a field in our item JSON called "id" using the key string we created, and another called "awardDate" with the medal's awardDate. We then add `item` to our `items` array.

With that done, we add the array of items to our root JSON, as "medals", then call cJSON_PrintUnformatted, passing over `root`, to get our JSON output. We're using the unformatted call here, so that it's all one line string, omitting line breaks, indents, etc. This saves us a few bytes when posting the data, resulting in slightly faster processing (but only very slightly..!). The output is assigned to a variable called `out`. We then delete `root` to free the memory, and return `out`.

The last function to look at is getResponseCode:


static int getResponseCode(char *response)
{
	int code;

	sscanf(response, "%d", &code);

	return code;
}

This function is very simple. It takes one argument - a char array called `response`. This is expected to be the response string from the server. We use sscanf to read the response code (just a number) from `response`, into a variable called `code`. We then return `code`.

That's all that's needed to send over our medal data to our server. We can now look at how we're integrating this into our game.

Starting with options.c, we're drawing the server connection status in drawOptions:


void drawOptions(void)
{
	char connectionStatus[MAX_DESCRIPTION_LENGTH];
	int hasMedalKey;

	drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0, 168);

	app.fontScale = 3;

	drawText("Options", SCREEN_WIDTH / 2, 65, 255, 255, 255, TEXT_ALIGN_CENTER, 0);

	app.fontScale = 1;

	drawWidgets("options");

	hasMedalKey = strlen(game.medalKey) > 0;

	if (app.server.connected && hasMedalKey)
	{
		if (app.server.lastResponseCode == 200)
		{
			sprintf(connectionStatus, "Connection OK (Last Sync: %s)", getSyncTime());

			drawText(connectionStatus, connectionStatusPoint.x, connectionStatusPoint.y, 0, 255, 0, TEXT_ALIGN_LEFT, 0);
		}
		else if (app.server.lastResponseCode == -1)
		{
			drawText("Syncing Medals. Please wait ...", connectionStatusPoint.x, connectionStatusPoint.y, 192, 192, 192, TEXT_ALIGN_LEFT, 0);
		}
		else
		{
			sprintf(connectionStatus, "Connection error (HTTP Status: %d)", app.server.lastResponseCode);

			drawText(connectionStatus, connectionStatusPoint.x, connectionStatusPoint.y, 255, 0, 0, TEXT_ALIGN_LEFT, 0);
		}
	}
	else if (hasMedalKey)
	{
		drawText("Not connected", connectionStatusPoint.x, connectionStatusPoint.y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	}
}

After drawing our header and widgets, we're then testing if a medal key is set. We do this by simply calling strlen on `game`'s medalKey, and assign the result of a test against the length being greater than 0 to a variable called hasMedalKey. Next, we test if the server is connected and also if hasMedalKey is true. This stops us from reporting an error with the medal server if the player doesn't have a key set. If the server is connected and we have a key set, we'll test the value of lastResponseCode. If it's 200, we'll report everything as fine. We'll display a message saying so, along with the result of a call to getSyncTime, to show when we last talked to the server. If it's -1, this means that communication with the server is still in-flight. We'll just print a message saying the sync is happening. Otherwise, we'll print an error message, with the value of lastResponseCode. Finally, if we have a medal key but aren't connected, we'll print a message to say we're not connected.

This helps the player to see the status of the connection without having to refer to the console output. We're also printing the status results in green for good, a light grey for in-flight, white for not connected, and red in case of an error.

The getSyncTime function follows:


static char *getSyncTime(void)
{
	int delta;

	delta = (time(NULL) - app.server.lastSync) / 60;

	switch (delta)
	{
		case 0:
			strcpy(syncTime, "Less than a minute ago");
			break;

		case 1:
			sprintf(syncTime, "1 minute ago");
			break;

		default:
			sprintf(syncTime, "%d minutes ago", delta);
			break;
	}

	return syncTime;
}

This is a simple function that merely returns how long ago a successful server sync occurred. We first subtract the value of lastSync from the current time (both values in seconds) and divide this by 60, to get minutes, and assign the value to `delta`. We then perform a switch on `delta`. If `delta` is 0, we set the value of syncTime (a static char array in options.c) to "Less than a minute ago". If it's 1, we set it as "1 minute ago". For all other values, we set the text as the number of minutes ago the sync happened.

The next function to consider is medalKey:


static void medalKey(void)
{
	InputWidget *iw;

	iw = (InputWidget*) app.activeWidget->data;

	STRCPY(game.medalKey, iw->text);

	syncMedals();
}

This is the function that is invoked by our medal key input widget when a player has updated the text. We grabbing the InputWidget from app's activeWidget's `data` field, and then copying the text data from it into `game`'s medalKey. After that, we're calling syncMedals. This is the function that talks to our medal server. We're calling syncMedals right away so that a player's key can be verified; they will know if they have entered it wrong due to the response code.

Moving over to stage.c now. In doPostStageMedal, we've added one line:


static void doPostStageMedals(void)
{
	// snipped

	syncMedals();
}

We're now calling syncMedals at the end of doPostStageMedals. This will happen after we've tested all our stats and ranks, so that any newly earned medals are sent over to our server.

Moving over to game.c, we've also calling syncMedals in loadGame:


void loadGame(void)
{
	cJSON *root;
	char *text;

	if (fileExists(SAVE_GAME_FILENAME))
	{
		text = readFile(SAVE_GAME_FILENAME);

		root = cJSON_Parse(text);

		game.soundVolume = cJSON_GetObjectItem(root, "soundVolume")->valueint;
		game.musicVolume = cJSON_GetObjectItem(root, "musicVolume")->valueint;

		loadStats(cJSON_GetObjectItem(root, "stats"));
		loadRanks(cJSON_GetObjectItem(root, "ranks"));
		loadMedals(cJSON_GetObjectItem(root, "medals"));

		STRCPY(game.medalKey, cJSON_GetObjectItem(root, "medalKey")->valuestring);

		cJSON_Delete(root);

		free(text);

		setSoundVolume(game.soundVolume);
		setMusicVolume(game.musicVolume);

		syncMedals();
	}

	saveGame();
}

After successfully loading a game, we're calling syncMedals, to send our medals over to the server. This is to deal with a case where a player might have earned some medals while not connected to the internet. Upon loading their game, those medals will be sent, keeping things in sync.

We're almost done. The only thing left to do is to setup SDL Net. We can do this in init.c, in initSDL:


void initSDL(void)
{
	int rendererFlags, windowFlags;

	rendererFlags = SDL_RENDERER_ACCELERATED;

	windowFlags = 0;

	if (SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		printf("Couldn't initialize SDL: %s\n", SDL_GetError());
		exit(1);
	}

	if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024) == -1)
	{
		printf("Couldn't initialize SDL Mixer\n");
		exit(1);
	}

	if (SDLNet_Init() < 0)
	{
		printf("Couldn't initialize SDL Net\n");
		exit(1);
	}

	if (TTF_Init() < 0)
	{
		printf("Couldn't initialize SDL TTF: %s\n", SDL_GetError());
		exit(1);
	}

	Mix_AllocateChannels(MAX_SND_CHANNELS);

	Mix_ReserveChannels(CH_MAX);

	app.window = SDL_CreateWindow("SDL2 Medals 05", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, windowFlags);

	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest");

	app.renderer = SDL_CreateRenderer(app.window, -1, rendererFlags);

	IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG);

	SDL_ShowCursor(0);
}

Along with including "SDL2/SDL_net.h" we need only call SDLNet_Init. This function will return a value of less than 0 if an error occurs. If so, we'll print an error message and exit.

And that's it for connecting our game to our medal server. This is quite a basic example of how to use SDL Net, however, and only really involves sending and receiving small amounts of data, almost as if we're sending messages to a chat server.

Now, it won't have escaped your attention that something a bit nasty is happening - whenever we sync the medals, our game is locking up for a second or so. This is because our networking is happening on our main thread, and so the code execution is pausing as it waits for the server to finish its work. In fact, the server is exaggerating this effect, as it purposely pauses for 1 second before responding to our request. This is purely to simulate the effect that slow network activity can have on our game. This pausing is clearly undesirable and something we should seek to eliminate. Therefore, in our final part we'll look at shifting our networking code into a background thread, to stop our game from locking up whenever we communicate with the medal server.

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:

Mobile site