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

Latest Updates

SDL2 Rogue tutorial
Wed, 29th September 2021

SDL2 Gunner tutorial
Thu, 26th August 2021

SDL2 Shooter 2 tutorial
Tue, 13th July 2021

SDL2 Widget tutorial
Fri, 18th June 2021

SDL2 Adventure tutorial
Tue, 8th June 2021

All Updates »

Tags

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

Books

« Back to tutorial listing

— Creating a simple roguelike —
Part 17: Saving

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

Introduction

Saving and loading games can be quite important, especially in a roguelike, which can take many hours to complete. In this part, we're going to be looking into saving our game data, including the dungeon and the map, so that the player can continue where they left off.

Extract the archive, run make, and then use ./rogue17 to run the code. You will see a window open displaying the player character in a small room, with just a stair case (leading up). Play the game as normal. The game will save automatically each time you move to a new floor. There's not much else to see, other than the two save files that are created: save.json and save.map in the same directory as the binary. Once you're finished, close the window to exit.

Inspecting the code

Saving our game is very similar to saving highscores, in that we'll be creating a load of JSON objects and arrays, and storing them in a file. We'll also be saving the map data, to preserve the dungeon layout.

To start with, we've added a couple of new defines to defs.h:


#define SAVE_GAME_FILENAME        "save.json"
#define SAVE_MAP_FILENAME         "save.map"

SAVE_GAME_FILENAME and SAVE_MAP_FILENAME are the filenames of the main JSON save file and the map save respectively.

Moving over the structs.h, we've updated Entity:


struct Entity {
	int id;
	int type;
	char name[MAX_NAME_LENGTH];
	char typeName[MAX_NAME_LENGTH];
	char description[MAX_DESCRIPTION_LENGTH];
	int x;
	int y;
	int dead;
	int solid;
	int facing;
	int alwaysVisible;
	void (*data);
	void (*touch)(Entity *self, Entity *other);
	void (*save)(Entity *self, cJSON *root);
	AtlasImage *texture;
	Entity *next;
};

We've added in a function pointer called `save`. It takes two parameters - `self`, the Entity itself, and `root`, the JSON object. This function exists so that an Entity can save its extended data, as well as the base data. We'll see how this is done later on. We've also added in a new field called typeName. This is important for our saving and loading, since our entity factory needs to know what the real name of the entity is. For most entities, it will be the same as the name. However, in the case of weapons and armour, it can change, due to the bonuses, the entity then being called something like "Stun Baton +3", which won't exist in our entity factory. While when loading (in the next part) we could've simply used the name up to the plus symbol, doing such things can actually introduce further problems. It's best avoided.

To handle the saving our game, we've created a new file called save.c. There are great number of functions in it. However, these mostly involve writing out JSON data, so we'll keep everything brief, and only go into detail for things that break the mould or need to be expanded upon (as by now saving JSON objects should be quite clear..!).

Starting with saveGame:


void saveGame(void)
{
	saveDungeon();

	saveMap();
}

This is the function that will be called when we want to save our game. It saves all the dungeon data (entities, floor number, etc), as well as the map data. It does this by calling saveDungeon and saveMap. We'll look at saveDungeon first:


static void saveDungeon(void)
{
	cJSON *root;
	char *out;

	root = cJSON_CreateObject();

	cJSON_AddNumberToObject(root, "entityId", dungeon.entityId);
	cJSON_AddNumberToObject(root, "floor", dungeon.floor);
	cJSON_AddItemToObject(root, "entities", saveEntities());
	cJSON_AddItemToObject(root, "equipment", saveEquipment());
	cJSON_AddItemToObject(root, "inventory", saveInventory());
	cJSON_AddItemToObject(root, "messages", saveMessages());
	cJSON_AddItemToObject(root, "highscore", saveHighscore());

	out = cJSON_Print(root);

	writeFile(SAVE_GAME_FILENAME, out);

	cJSON_Delete(root);

	free(out);
}

We're creating a JSON object to contain all the dungeon data. We're first storing dungeon's entityId and `floor`, and then adding in the entities, equipment, inventory, messages, and highscore (not the highscore table, but the player's current score data). We're doing this by calling saveEntities, saveEquipment, saveInventory, saveMessages, and saveHighscore, respectively. The result of these calls will be added to the `root` JSON object.

We'll look at saveEntities first:


static cJSON *saveEntities(void)
{
	int i;
	Entity *e;
	cJSON *root;

	root = cJSON_CreateArray();

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		cJSON_AddItemToArray(root, saveEntity(e));
	}

	for (e = game.inventoryHead.next ; e != NULL ; e = e->next)
	{
		cJSON_AddItemToArray(root, saveEntity(e));
	}

	for (i = 0 ; i < EQUIP_MAX ; i++)
	{
		if (game.equipment[i] != NULL)
		{
			cJSON_AddItemToArray(root, saveEntity(game.equipment[i]));
		}
	}

	return root;
}

This function saves ALL the entities in the dungeon, including those set as equipment and in the player's inventory. We're looping through the dungeon's entity linked list and calling saveEntity for each entity, adding them to a JSON array called `root`. We're doing the same with the inventory. For the equipment, we're testing if an entity is set at the appropriate equipment array index in game and saving it if so. We're finally returning the JSON array.

saveEntity is basic:


static cJSON *saveEntity(Entity *e)
{
	cJSON *node;

	node = cJSON_CreateObject();

	cJSON_AddNumberToObject(node, "id", e->id);
	cJSON_AddNumberToObject(node, "type", e->type);
	cJSON_AddStringToObject(node, "name", e->name);
	cJSON_AddStringToObject(node, "typeName", e->typeName);
	cJSON_AddNumberToObject(node, "x", e->x);
	cJSON_AddNumberToObject(node, "y", e->y);
	cJSON_AddNumberToObject(node, "facing", e->facing);
	cJSON_AddNumberToObject(node, "solid", e->solid);

	if (e->save != NULL)
	{
		e->save(e, node);
	}

	return node;
}

The function takes an entity (`e`) as an argument. We're creating a JSON object and adding in all the relevant details. After adding in all the base details, we're then testing to see if `e`'s `save` function pointer has been set. If so, we're calling it, passing over the entity and also the cJSON `node`. Again, we'll cover this in detail later on.

Moving onto saveEquipment:


static cJSON *saveEquipment(void)
{
	int i;
	cJSON *items;

	items = cJSON_CreateArray();

	for (i = 0 ; i < EQUIP_MAX ; i++)
	{
		if (game.equipment[i] != NULL)
		{
			cJSON_AddItemToArray(items, cJSON_CreateNumber(game.equipment[i]->id));
		}
		else
		{
			cJSON_AddItemToArray(items, cJSON_CreateNumber(-1));
		}
	}

	return items;
}

This saving routine is a bit more interesting, as what we're doing is saving an array of entity ids, rather than the entities themselves. We're looping through all game's equipment slots, testing if an entity is set, and then adding its `id` to a JSON array called `items`. If there's nothing set, we'll add -1. We'll see how this is used in the next part, but it basically involves us looking up the entities by id (the number we're saving here) and moving them into the equipment slots, to restore the equipped items state.

saveInventory follows a similar pattern:


static cJSON *saveInventory(void)
{
	cJSON *items;
	Entity *e;

	items = cJSON_CreateArray();

	for (e = game.inventoryHead.next ; e != NULL ; e = e->next)
	{
		cJSON_AddItemToArray(items, cJSON_CreateNumber(e->id));
	}

	return items;
}

The only difference is that we need not set -1 if there is nothing set, since this is a linked list and there will be no empty slots.

saveMessages is next and is quite straightforward:


static cJSON *saveMessages(void)
{
	int i;
	cJSON *messages, *node;

	messages = cJSON_CreateArray();

	for (i = 0 ; i < NUM_HUD_MESSAGES ; i++)
	{
		node = cJSON_CreateObject();

		cJSON_AddNumberToObject(node, "type", game.messages[i].type);
		cJSON_AddStringToObject(node, "text", game.messages[i].text);

		cJSON_AddItemToArray(messages, node);
	}

	return messages;
}

We're just looping through all our HUD messages, creating a JSON object to store the data, and then adding that object to a JSON array that we finally return. We're not bothered if the HUD message doesn't contain any text.

saveHighscore follows:


static cJSON *saveHighscore(void)
{
	cJSON *node;

	node = cJSON_CreateObject();

	cJSON_AddNumberToObject(node, "xp", game.highscore.xp);
	cJSON_AddNumberToObject(node, "kills", game.highscore.kills);
	cJSON_AddNumberToObject(node, "floor", game.highscore.floor);

	return node;
}

We're just storing game's highscore's data. Nothing complicated.

saveMap is the last function to consider:


static void saveMap(void)
{
	FILE *fp;
	int x, y;

	fp = fopen(SAVE_MAP_FILENAME, "w");

	for (y = 0 ; y < MAP_HEIGHT ; y++)
	{
		for (x = 0 ; x < MAP_WIDTH ; x++)
		{
			fprintf(fp, "%d %d ", dungeon.map[x][y].tile, dungeon.map[x][y].revealed);
		}
	}

	fclose(fp);
}

Our map data is saved in its own file, rather than being part of the JSON. We start by using fopen to open the file for writing (using SAVE_MAP_FILENAME). Next, we're using two for-loop, to iterate through all our map data. When it comes to writing to the file, we're saving two numbers - `tile` and `revealed`. We need to store the tile so that we know what type of map sqaure it is (whether it's a wall, floor, etc). We're also storing the `revealed` state because we need to preserve how much of the map the player has explored. Not doing so would leave us needing to either reset our fog of war when loading, or revealing the entire map. Storing `tile` and `revealed` side by side allows us to store the entire map state, and allow the player to continue where they left off.

That's it for save.c. As you can see, we're mostly writing JSON data. What we're going to look at how is how we're using the `save` function pointer and typeName field that we added to Entity struct.

We've started by updating armour.c and the createArmour function:


static Equipment *createArmour(Entity *e)
{
	Equipment *eq;

	eq = malloc(sizeof(Equipment));
	memset(eq, 0, sizeof(Equipment));

	e->type = ET_ARMOUR;
	e->data = eq;

	e->touch = touchItem;
	e->save = saveEquipment;

	return eq;
}

We're assigning `e`'s `save` function pointer to a function called saveEquipment. Just like touchItem, this function is global and lives in items.c. We'll come to it shortly.

Moving now to weapons.c, we've made a similar update to createWeapon:


static Equipment *createWeapon(Entity *e)
{
	Equipment *eq;

	eq = malloc(sizeof(Equipment));
	memset(eq, 0, sizeof(Equipment));

	e->type = ET_WEAPON;
	e->data = eq;

	e->touch = touchItem;
	e->save = saveEquipment;

	return eq;
}

And the same again in microchips.c, for createMicrochip:


static Equipment *createMicrochip(Entity *e)
{
	Equipment *eq;

	eq = malloc(sizeof(Equipment));
	memset(eq, 0, sizeof(Equipment));

	e->type = ET_MICROCHIP;
	e->data = eq;

	e->touch = touchItem;
	e->save = saveEquipment;

	return eq;
}

If we now look at items.c, we can see how the saveEquipment function is defined:


void saveEquipment(Entity *self, cJSON *root)
{
	Equipment *eq;

	eq = (Equipment*) self->data;

	cJSON_AddNumberToObject(root, "hp", eq->hp);
	cJSON_AddNumberToObject(root, "minAttack", eq->minAttack);
	cJSON_AddNumberToObject(root, "maxAttack", eq->maxAttack);
	cJSON_AddNumberToObject(root, "defence", eq->defence);
	cJSON_AddNumberToObject(root, "savingThrow", eq->savingThrow);
}

This function conforms to Entity's `save` function pointer signature. It takes two arguments - the Entity itself (`self`) and the JSON object (`root`) to which we want to add our extended data. In this case, our extended data is an Equipment struct. We're extracting this from `self`'s `data`, and then storing all the Equipment fields in the JSON object. That's it..! There's nothing more to it. But as you can see, the save function pointer allows us to very easily and neatly store all the `data` field struct data in our JSON object, without lots of messy if-statements and tests.

If we move over to monsters.c, we can see we're doing the same. Starting with createMonster:


static Monster *createMonster(Entity *e)
{
	Monster *m;

	m = malloc(sizeof(Monster));
	memset(m, 0, sizeof(Monster));

	e->type = ET_MONSTER;
	e->solid = 1;
	e->data = m;

	e->save = saveMonster;

	return m;
}

We've assigned the `save` field to a function called saveMonster:


void saveMonster(Entity *self, cJSON *root)
{
	Monster *m;

	m = (Monster*) self->data;

	cJSON_AddNumberToObject(root, "level", m->level);
	cJSON_AddNumberToObject(root, "hp", m->hp);
	cJSON_AddNumberToObject(root, "maxHP", m->maxHP);
	cJSON_AddNumberToObject(root, "xp", m->xp);
	cJSON_AddNumberToObject(root, "requiredXP", m->requiredXP);
	cJSON_AddNumberToObject(root, "minAttack", m->minAttack);
	cJSON_AddNumberToObject(root, "maxAttack", m->maxAttack);
	cJSON_AddNumberToObject(root, "defence", m->defence);
	cJSON_AddNumberToObject(root, "alert", m->alert);
	cJSON_AddNumberToObject(root, "visRange", m->visRange);
	cJSON_AddNumberToObject(root, "savingThrow", m->savingThrow);
	cJSON_AddNumberToObject(root, "patrolDest.x", m->patrolDest.x);
	cJSON_AddNumberToObject(root, "patrolDest.y", m->patrolDest.y);
	cJSON_AddNumberToObject(root, "flags", m->flags);
}

Again, this function conforms to the Entity's save function pointer signature. It's also a global function, so that it can be used by the player. Like when we saved our equipment, we're extracting the Monster from `self`'s `data` field, and adding all the relevant items to the JSON object.

If we look at player.c, we've updated the initPlayer function to use the saveMonster function:


void initPlayer(Entity *e)
{
	Monster *m;

	m = malloc(sizeof(Monster));
	memset(m, 0, sizeof(Monster));

	STRCPY(e->name, "Player");
	STRCPY(e->description, "A brave lab technician, hunting for escaped mice.");
	e->type = ET_PLAYER;
	e->texture = getAtlasImage("gfx/entities/girl.png", 1);
	e->data = m;
	e->solid = 1;

	e->save = saveMonster;

	dungeon.player = e;

	updatePlayerAttributes(m, -1);

	m->hp = m->maxHP;

	moveDelay = 0;
}

We're assigning `e`'s `save` function pointer to saveMonster. That's all we need to do.

doors.c is also using a `save` function pointer. Starting with initDoor:


static Entity *initDoor(Entity *e, int locked)
{
	Door *d;

	d = malloc(sizeof(Door));
	memset(d, 0, sizeof(Door));
	d->locked = locked;

	e->type = ET_DOOR;
	e->data = d;
	e->solid = 1;
	e->alwaysVisible = 1;

	e->touch = touch;
	e->save = save;

	return e;
}

We've assigned the `save` function pointer to `save`:


static void save(Entity *self, cJSON *root)
{
	Door *d;

	d = (Door*) self->data;

	cJSON_AddNumberToObject(root, "locked", d->locked);
}

And we're adding the Door's fields to the JSON object.

stairs.c has also been updated. initStairs:


static void initStairs(Entity *e, int dir)
{
	Stairs *s;

	s = malloc(sizeof(Stairs));
	memset(s, 0, sizeof(Stairs));

	s->dir = dir;

	e->type = ET_STAIRS;
	e->alwaysVisible = 1;
	e->data = s;

	e->touch = touch;
	e->save = save;
}

We're assigning the `e`'s `save` function to `save`:


static void save(Entity *self, cJSON *root)
{
	Stairs *s;

	s = (Stairs*) self->data;

	cJSON_AddNumberToObject(root, "dir", s->dir);
}

And we're adding the Stairs's fields to the JSON object.

We're almost finished. The only thing we need to do is make the call to saveGame, to actually save our game content. If we turn to dungeon.c, we can see we're doing this in createDungeon:


static void createDungeon(void)
{
	// snipped

	sprintf(text, "Entering floor #%d", dungeon.floor);

	game.highscore.floor = MAX(dungeon.floor, game.highscore.floor);

	addHudMessage(HUD_MSG_NORMAL, text);

	saveGame();
}

After having created our dungeon, we've added in the call to saveGame as the final line in the function. Notice that right now we're only saving the game when the player changes floors. If they close the SDL window, the game will simply quit. In the finishing touch, we'll add in a menu option to Save and Quit the game, to let the player save at any point (as long as they have control over the player!).

Before we finish, we should quickly revisit armour.c, where we can see an example of the Entity's typeName being used. In initBikerJacket, we've added a line:


void initBikerJacket(Entity *e)
{
	Equipment *eq;

	eq = createArmour(e);

	STRCPY(e->name, "Biker Jacket");
	STRCPY(e->typeName, e->name);
	STRCPY(e->description, "An ultra cool jacket that offers a decent level of protection.");
	e->texture = getAtlasImage("gfx/entities/bikerJacket.png", 1);

	eq->defence = 5;

	applyBonus(e, eq);
}

We're now copying `e`'s `name` into typeName. We're doing this before the applyBonus function call, which may change the entity's name and therefore lead to complications when loading the entity back in. We've done the same thing in the other armour and weapon init functions.

We've also set the typeName for our Antidote. If we look next at items.c, we can see we've tweaked initAntidote:


void initAntidote(Entity *e)
{
	Item *i;

	i = createItem(e);

	STRCPY(e->name, "Vial of Antivenom");
	STRCPY(e->typeName, "Antidote");
	STRCPY(e->description, "Antidote. Cures poison when used.");
	e->texture = getAtlasImage("gfx/entities/antidote.png", 1);

	i->use = useAntidote;
}

Since our antidote is named "Vial of Anitvenom", we're setting the typeName to "Antidote" so that it can be loaded by our entityFactory.

This part is now finished. We can successfully save our game each time we change floor, meaning we're no longer expected to finish the game in one sitting. In the next part, we'll look at loading the data back in, so we can continue playing.

Purchase

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

It is also available as part of the SDL2 tutorial bundle (with on-going updates):

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.

Comments

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

 

Mobile site