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

Android Games

DDDDD
Number Blocks
Match 3 Warriors

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)
SDL 1 tutorials (outdated)

Latest Updates

Water Closet ported to PlayStation Vita
Tue, 4th January 2022

The Legend of Edgar 1.35
Sat, 1st January 2022

Achievements tutorial
Thu, 2nd December 2021

SDL2 Rogue tutorial
Thu, 30th September 2021

SDL2 Gunner tutorial
Fri, 27th August 2021

All Updates »

Tags

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

Books


The Honour of the Knights (Second Edition) (Battle for the Solar System, #1)

When starfighter pilot Simon Dodds is enrolled in a top secret military project, he and his wingmates begin to suspect that there is a lot more to the theft of a legendary battleship and the Mitikas Empire's civil war than the Helios Confederation is willing to let on. Somewhere out there the Pandoran army is gathering, preparing to bring ruin to all the galaxy...

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating an in-game achievement system —
Part 3: Saving progress

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

Introduction

Now that we've integrated Medals into our gameplay, we should consider that a player might not be able to earn all the Medals in one session; the game could have multiple levels, etc., and also require the player to reach certains goals that might not be obtainable right away. In this part, we'll look at saving our progress.

Extract the archive, run make, and then use ./medals03 to run the code. You will see a window open like the one above, showing a little blue alien and a handful of floating batteries. The goal is to collect all the batteries. Guide the little alien around using the WASD control scheme. Press Tab to show the stats and also the Medals that are on offer (press Tab again to dismiss the display). Collect the batteries that you are able to, and then close the window to save your game. Re-run the game to start collecting batteries again. Notice when you press Tab that your Medal progress and stats are carried over. Continue playing until you have earned all the Medals.

Inspecting the code

We've started by adding in some new stats. Our Medals call for awards given for collecting different battery types, so we've updated defs.h to handle this:


enum {
	STAT_BATTERIES_COLLECTED,
	STAT_FULL_BATTERIES_COLLECTED,
	STAT_MEDIUM_BATTERIES_COLLECTED,
	STAT_LOW_BATTERIES_COLLECTED,
	STAT_MAX
};

We've now added in three new enums: STAT_FULL_BATTERIES_COLLECTED, STAT_MEDIUM_BATTERIES_COLLECTED, and STAT_LOW_BATTERIES_COLLECTED, to record the stats for the number of full batteries, medium batteries, and low batteries that can be collected.

Next, we've updated structs.h, making changes to the Battery struct:


typedef struct {
	int type;
	double bob;
} Battery;

We've added in a `type` field, that will be used to track the type of battery that this is.

Heading over to batteries.c, we can now see how we've put this to use:


void initBatteryFull(Entity *e)
{
	initBattery(e);

	((Battery*)e->data)->type = BT_FULL;

	e->texture = getAtlasImage("gfx/batteryFull.png", 1);
}

void initBatteryMedium(Entity *e)
{
	initBattery(e);

	((Battery*)e->data)->type = BT_MEDIUM;

	e->texture = getAtlasImage("gfx/batteryMedium.png", 1);
}

void initBatteryLow(Entity *e)
{
	initBattery(e);

	((Battery*)e->data)->type = BT_LOW;

	e->texture = getAtlasImage("gfx/batteryLow.png", 1);
}

In the respective initBatteryXXX functions, we've set the type of battery that this is. BT_FULL, BT_MEDIUM, and BT_LOW are defined in battery.h. The names correspond to the battery type.

Next, we've updated the `touch` function, to incorporate `type`:


static void touch(Entity *self, Entity *other)
{
	if (other == stage.player)
	{
		game.stats[STAT_BATTERIES_COLLECTED]++;

		switch (((Battery*) self->data)->type)
		{
			case BT_FULL:
				game.stats[STAT_FULL_BATTERIES_COLLECTED]++;
				break;

			case BT_MEDIUM:
				game.stats[STAT_MEDIUM_BATTERIES_COLLECTED]++;
				break;

			case BT_LOW:
				game.stats[STAT_LOW_BATTERIES_COLLECTED]++;
				break;

			default:
				break;
		}

		self->dead = 1;

		playSound(SND_BATTERY, 0);

		if (game.stats[STAT_FULL_BATTERIES_COLLECTED] == 10)
		{
			awardMedal("batteriesFull10");
		}

		if (game.stats[STAT_MEDIUM_BATTERIES_COLLECTED] == 10)
		{
			awardMedal("batteriesMedium10");
		}

		if (game.stats[STAT_LOW_BATTERIES_COLLECTED] == 10)
		{
			awardMedal("batteriesLow10");
		}

		if (game.stats[STAT_BATTERIES_COLLECTED] == 10)
		{
			awardMedal("batteries10");
		}

		if (game.stats[STAT_BATTERIES_COLLECTED] == 25)
		{
			awardMedal("batteries25");
		}

		if (game.stats[STAT_BATTERIES_COLLECTED] == 50)
		{
			awardMedal("batteries50");
		}
	}
}

Now, as well as adding to our STAT_BATTERIES_COLLECTED stat when we collect a battery, we're testing the type of battery that we've collected. We're then incrementing game's stats STAT_FULL_BATTERIES_COLLECTED, STAT_MEDIUM_BATTERIES_COLLECTED, or STAT_LOW_BATTERIES_COLLECTED depending on the type of battery this is, by performing a switch against the Battery `type`.

We're continuing to flag the battery as dead and playing a sound, but we've now added in many more stat checks. If STAT_FULL_BATTERIES_COLLECTED is 10, we're calling awardMedal and passing over "batteriesFull10". If STAT_MEDIUM_BATTERIES_COLLECTED is 10, we're calling awardMedal and passing over "batteriesMedium10", and so on. (remember: all these Medals are defined in data/medals.json).

In short, we've added some new stats, that can be incremented and tested in the `touch` function in batteries.c, by checking the battery's `type`.

Now, we should look at how we're persisting our game. If we move over to game.c, we can see we've added in many more functions, to handle loading and saving.

Starting with loadGame:


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

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

		root = cJSON_Parse(text);

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

		cJSON_Delete(root);

		free(text);
	}
}

This is a standard game loading function that we've used in some other tutorials. We're first checking to see if our save game file exists (SAVE_GAME_FILENAME is defined as "save.json" in game.h). If so, we're reading in the data and converting it to JSON, and then calling loadStats and loadMedals, passing over the "stats" and "medals" objects from the JSON to them.

Our loadStats function is quite simple:


static void loadStats(cJSON *root)
{
	cJSON *node;
	int i;

	for (node = root->child ; node != NULL ; node = node->next)
	{
		i = lookup(cJSON_GetObjectItem(node, "name")->valuestring);

		game.stats[i] = cJSON_GetObjectItem(node, "value")->valueint;
	}
}

For loading our stats, we're looping through each of the child nodes (as `node`) in our JSON (our stats are saved as a JSON array, passed into this function as a variable named `root`), grabbing the name from the node and passing it to our `lookup` function to get its int value (assigning it to a variable called `i`), and then setting the value at the appropriate stat index to the value of "value" in the JSON node. Nothing complicated.

loadMedals follows:


static void loadMedals(cJSON *root)
{
	char *id;
	Medal *m;
	cJSON *node;

	for (node = root->child ; node != NULL ; node = node->next)
	{
		id = cJSON_GetObjectItem(node, "id")->valuestring;

		for (m = game.medalsHead.next ; m != NULL ; m = m->next)
		{
			if (strcmp(m->id, id) == 0)
			{
				m->awardDate = cJSON_GetObjectItem(node, "awardDate")->valueint;
				break;
			}
		}
	}
}

Again, we're looping through all the nodes in the JSON array passed into the function (`root`), again assigning them to a variable called `node`. We're then extracting the "id" field from node and searching for a matching Medal in our Medals linked list. When we find a Medal with an `id` that matches the id from the JSON node, we're updating the Medal's awardDate with the awardDate from the JSON. Basically, when we start up our game, all our Medals will be locked and unearned. This function is updating those unlock dates with those from the save file (note that the value might still be 0!).

Moving on saveGame, this function is quite simple:


void saveGame(void)
{
	cJSON *root;
	char *out;

	root = cJSON_CreateObject();

	cJSON_AddItemToObject(root, "stats", saveStats());
	cJSON_AddItemToObject(root, "medals", saveMedals());

	out = cJSON_Print(root);

	writeFile(SAVE_GAME_FILENAME, out);

	cJSON_Delete(root);

	free(out);
}

Saving is much the same as that found in other tutorials: we're creating our root JSON object, and then adding the stats and medals fields to it, using the results of calls to saveStats and saveMedals. The JSON data is written to saved to SAVE_GAME_FILENAME (save.json).

Moving over to saveStats now:


static cJSON *saveStats(void)
{
	int i;
	cJSON *items, *item;

	items = cJSON_CreateArray();

	for (i = 0 ; i < STAT_MAX ; i++)
	{
		item = cJSON_CreateObject();

		cJSON_AddStringToObject(item, "name", getLookupName("STAT_", i));
		cJSON_AddNumberToObject(item, "value", game.stats[i]);

		cJSON_AddItemToArray(items, item);
	}

	return items;
}

We're looping through all our stats, creating a JSON object for each, and setting the "name" and "value" fields. Notice how we're making a call to getLookUpName, and passing over "STAT_" and the loop index (`i`) to the function, to translate the stat name into a human-readable format. All the stats are saved into a JSON array called `items` that we're returning at the end of the function.

saveMedals works in a similar way:


static cJSON *saveMedals(void)
{
	Medal *m;
	cJSON *items, *item;

	items = cJSON_CreateArray();

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

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

		cJSON_AddItemToArray(items, item);
	}

	return items;
}

For each of our Medals, we're creating a JSON object (`item`), and storing the Medal `id` and awardDate. We're adding `item` to a JSON array (`items`) and then returning it at the end of the function.

In summary, our save game data is storing both our stats and our medal data, so it can be restored when we start the game back up again. This allows us to unlock our Medals at a pace that suits us.

Moving over to stage.c now, where we've made a small update to display the Medals and stats. Starting with initStage:


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

	initEntities();

	addEntities();

	showStats = 0;

	app.delegate.logic = logic;

	app.delegate.draw = draw;
}

We've added a static variable called showStats, that we're setting to 0. This variable will be used to control whether we display the stats and medals on screen.

We've then updated `logic`, to handle this variable:


static void logic(void)
{
	if (app.keyboard[SDL_SCANCODE_TAB])
	{
		app.keyboard[SDL_SCANCODE_TAB] = 0;

		showStats = !showStats;
	}

	if (!showStats)
	{
		doEntities();
	}
}

We're testing first if Tab has been pressed. If so, we'll clear the key, and then set showStats to the inverse of showStats (in effect, making it toggle between 0 and 1). We're then testing to see if showStats is 0. If so, we'll call doEntities. The reason for this is because we don't want to process our entities when we're displaying the stats. In effect, the game is paused.

Updates to `draw` follow:


static void draw(void)
{
	drawEntities();

	if (showStats)
	{
		drawStats();
	}
}

We're now testing to showStats is set and calling drawStats if so.

drawStats itself is quite straightforward:


static void drawStats(void)
{
	Medal *m;
	int i, y, r, g, b;
	char text[MAX_NAME_LENGTH];

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

	y = 50;

	for (m = game.medalsHead.next ; m != NULL ; m = m->next)
	{
		r = g = b = 128;

		if (m->awardDate > 0)
		{
			r = g = 255;
			b = 0;
		}

		drawText(m->title, 150, y, r, g, b, TEXT_ALIGN_LEFT, 0);

		drawText(m->description, 190, y + 45, r, g, b, TEXT_ALIGN_LEFT, 0);

		y += 115;
	}

	for (i = 0 ; i < STAT_MAX ; i++)
	{
		sprintf(text, statText[i], game.stats[i]);
		drawText(text, SCREEN_WIDTH - 150, 50 + (i * 100), 200, 200, 200, TEXT_ALIGN_RIGHT, 0);
	}
}

We're first calling drawRect to darken the screen a bit, to make the text we're about to draw more readable. Next, we're rendering the text for each of our medals, in much the same way as we did in the first part of the tutorial, with the text being displayed in yellow if we've unlocked the Medal.

With the Medals drawn, we're rendering our stats. We're using a for-loop to iterate through them, and sprintf to create the stats text. statText is a static char array in stage.c, with text entries that align with the type of stat (e.g., STAT_BATTERIES_COLLECTED aligns to "Batteries Collected: %d"), etc.

addEntities has been modified somewhat to aid with our demonstration:


static void addEntities(void)
{
	int i, r;
	Entity *e;

	e = initEntity("Player");
	e->x = SCREEN_WIDTH / 2;
	e->y = 32;

	for (i = 0 ; i < 8 ; i++)
	{
		r = rand() % 100;

		if (r < 50)
		{
			e = initEntity("BatteryLow");
		}
		else if (r < 90)
		{
			e = initEntity("BatteryMedium");
		}
		else
		{
			e = initEntity("BatteryFull");
		}

		e->x = rand() % (SCREEN_WIDTH - 64);
		e->y = 64 + rand() % (SCREEN_HEIGHT - 128);
	}
}

We're now creating just 8 batteries in our for-loop, rather than 25. We're also giving low batteries a 50% chance of appearing, medium batteries a 40% chance, and full batteries a 10% chance.

Lastly, we've tweaked init.c, to update initGameSystem:


void initGameSystem(void)
{
	srand(time(NULL));

	initTextures();

	initAtlas();

	initFonts();

	initSound();

	initLookups();

	initGame();

	initMedals();

	initEntityFactory();

	loadGame();
}

After everything has been setup, we're calling loadGame, to load our progress back up.

input.c has also seen a one line addition:


void doInput(void)
{
	SDL_Event event;

	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_QUIT:
				saveGame();
				exit(0);
				break;

			case SDL_KEYDOWN:
				doKeyDown(&event.key);
				break;

			case SDL_KEYUP:
				doKeyUp(&event.key);
				break;

			default:
				break;
		}
	}
}

Now, when the SDL_QUIT event is found, we're calling saveGame before `exit`. So, when we close the window, our game will be saved.

And, there we go. We can now save and reload our Medal progress, so that it will carry across between game sessions. As you can no doubt see, this is quite similar to saving regular game data.

What would be exciting now is to put our Medals into a full game, so we can see how different aspects of the Medal process works. In our next part, we'll look at how Medals have been implemented in a game called UFO Rescue!

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:

Directly

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal, and then download the tutorials directly from the main tutorials page.

SDL2_Tutorials.tar.gz 54.13MB 1st December 2021

Click here to see the list of files in the archive

Comments

 

Share your comments and thoughts below. All comments are anonymous and cannot be edited.

Mobile site