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 18: Loading

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

Introduction

Now that we're able to save our game, we should look into reloading it, so we can continue playing. In this part, we'll load all our save data back up.

Extract the archive, run make, and then use ./rogue18 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 as you move between floors. Exit the game and run ./rogue18 to resume your game. Remember that the game only saves when you move between floors, not in the middle of exploring. If you are killed, the save data will be completely deleted. Once you're finished, close the window to exit.

Inspecting the code

Loading our save game is easy - it's largely just the reverse of saving, but with a few extra considerations. Again, since we've seen loading JSON many, many times before, we'll not spend much time talking about how that all works.

Moving first to 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 (*load)(Entity *self, cJSON *root);
	void (*save)(Entity *self, cJSON *root);
	AtlasImage *texture;
	Entity *next;
};

We've added in a few function pointer called `load`. The function's signature is just like that of `save`, in that it takes the Entity and the JSON node to work with as parameters.

Our loading code is handled in a file called load.c. It contains a similar number of functions to save, which we'll work through one at a time. Starting with loadGame:


int loadGame(void)
{
	if (fileExists(SAVE_GAME_FILENAME) && fileExists(SAVE_MAP_FILENAME))
	{
		loadDungeon();

		loadMap();

		updateFogOfWar();

		dungeon.currentEntity = dungeon.player;

		return 1;
	}

	return 0;
}

The first thing we're doing is checking that both our main save file and the map save data exist. We're calling fileExists for both. If this check returns true, we'll call loadDungeon, loadMap, and updateFogOfWar. We'll also assign the dungeon's currentEntity to the player, so that the player can take their turn immediately. We'll also return 1, to say that we successfully loaded a save game. Otherwise, we'll return 0. We'll see how this is used later on.

The loadDungeon function comes next:


static void loadDungeon(void)
{
	char *data;
	cJSON *root;

	data = readFile(SAVE_GAME_FILENAME);

	root = cJSON_Parse(data);

	dungeon.floor = dungeon.newFloor = cJSON_GetObjectItem(root, "floor")->valueint;

	loadEntities(cJSON_GetObjectItem(root, "entities"));

	loadEquipment(cJSON_GetObjectItem(root, "equipment"));

	loadInventory(cJSON_GetObjectItem(root, "inventory"));

	loadMessages(cJSON_GetObjectItem(root, "messages"));

	loadHighscore(cJSON_GetObjectItem(root, "highscore"));

	dungeon.entityId = cJSON_GetObjectItem(root, "entityId")->valueint;

	cJSON_Delete(root);

	free(data);

This function mostly delegates to other functions. We're first setting dungeon's `floor` and newFloor to that found in the save game JSON data, then calling loadEntities, loadEquipment, loadInventory, loadMessages, and loadHighscore, passing over the relevant save game data, extracted from the JSON. Finally, we're setting the dungeon's entityId to that stored in the save file. We need to do this because our spawnEntity function will increment the id itself and so the numbers will be mismatched. This could lead to errors down the line, so we need to keep things in sync (as entityId will start from 0 each time, meaning we could end up with two or more entities with the same id after loading).

loadEntities follows:


static void loadEntities(cJSON *root)
{
	char *name, *typeName;
	Entity *e;
	cJSON *node;

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

		typeName = cJSON_GetObjectItem(node, "typeName")->valuestring;

		if (strlen(typeName) != 0)
		{
			e = initEntity(typeName);
		}
		else
		{
			e = initEntity(name);
		}

		e->id = cJSON_GetObjectItem(node, "id")->valueint;
		STRCPY(e->name , name);
		e->type = cJSON_GetObjectItem(node, "type")->valueint;
		STRCPY(e->typeName , typeName);
		e->x = cJSON_GetObjectItem(node, "x")->valueint;
		e->y = cJSON_GetObjectItem(node, "y")->valueint;
		e->facing = cJSON_GetObjectItem(node, "facing")->valueint;
		e->solid = cJSON_GetObjectItem(node, "solid")->valueint;

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

This is standard JSON loading function. Notice, however, that for each entity we're extracting both `name` and typeName from the JSON. We're testing to see if typeName is set (is not a blank string) and, if so, we're calling initEntity using typeName. Otherwise, we're using `name`. This means that if a weapon is named "Stun Baton +3", we can use its typeName, which will be "Stun Baton". This will match up with what is expected by our entity factory, which will be able to create the object.

For the remainder of the function, we're setting all the entity's fields as expected, extracted from the JSON. We're then testing if the entity has a `load` function. If so, we'll call it (we'll see more on these later).

Moving across to loadEquipment now:


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

	i = 0;

	for (node = root->child ; node != NULL ; node = node->next)
	{
		if (node->valueint != -1)
		{
			game.equipment[i] = getEntityById(node->valueint);

			removeEntityFromDungeon(game.equipment[i]);
		}

		i++;
	}
}

Our equipment an array of ints, each representing the id of an entity. We're stepping through our array of numbers and setting the entity at the appropriate equipment slot (`i`). We're first testing the value of the number, and if it's not -1, we're going to look up the entity by calling getEntityById. As we'll see in a bit, this function searches our entity linked list for an entity with an `id` matching that which is passed over. Should one be found, we'll return it. With our game's equipment slot filled with the appropriate entity, we're calling removeEntityFromDungeon and passing over the entity in the slot, to remove it from the dungeon. By default, all the entities that we load will be added to the dungeon. Since the item has been equippped, we therefore need to remove it.

loadInventory works in a similar way:


static void loadInventory(cJSON *root)
{
	cJSON *node;

	for (node = root->child ; node != NULL ; node = node->next)
	{
		addToInventory(getEntityById(node->valueint));
	}
}

We're merely stepping through our JSON array, fetching an entity with a matching `id`, and adding it to our inventory, with a call to addToInventory.

loadMessages follows:


static void loadMessages(cJSON *root)
{
	cJSON *node;
	HudMessage *h;

	h = &game.messages[0];

	for (node = root->child ; node != NULL ; node = node->next)
	{
		h->type = cJSON_GetObjectItem(node, "type")->valueint;
		STRCPY(h->text , cJSON_GetObjectItem(node, "text")->valuestring);

		h++;
	}
}

This function loads all our HUD messages. It's quite similar to the function for loading our highscore table, in that we're grabbing a reference to the first element in our game's messages array (as `h`), setting the values from the JSON, and then incrementing `h` to move to the next element in the array. Again, keep in mind that we're assuming there are never more than 5 HUD messages.

loadHighscores is equally simple:


static void loadHighscore(cJSON *root)
{
	game.highscore.xp = cJSON_GetObjectItem(root, "xp")->valueint;
	game.highscore.kills = cJSON_GetObjectItem(root, "kills")->valueint;
	game.highscore.floor = cJSON_GetObjectItem(root, "floor")->valueint;
}

We're just setting game's highscore data from that contained in the JSON object.

loadMap is the final function to consider:


static void loadMap(void)
{
	int x, y;
	char *data, *p;

	data = readFile(SAVE_MAP_FILENAME);

	p = data;

	for (y = 0 ; y < MAP_HEIGHT ; y++)
	{
		for (x = 0 ; x < MAP_WIDTH ; x++)
		{
			memset(&dungeon.map[x][y], 0, sizeof(MapTile));

			dungeon.map[x][y].tile = atoi(p);

			do {p++;} while (isdigit(*p));

			dungeon.map[x][y].revealed = atoi(p);

			do {p++;} while (isdigit(*p));
		}
	}

	free(data);
}

We've seen this map loading function before, in the Gunner tutorial. It operates in mostly the same way, except that we're reading back both the `tile` and the `revealed` flag of each tile. Remember that we're saving them in pairs, and therefore need to read them back in a similar way. This will fully restore our map, with tiles and exploration data all as it should be.

That's it for loading. As you can see, it's quite easy to understand.

We'll now move onto the specifics. Starting with entities.c, we've added in a new function called getEntityById, which is used when we loaded the equipment and inventory:


Entity *getEntityById(int id)
{
	Entity *e;

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->id == id)
		{
			return e;
		}
	}

	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_CRITICAL, "No such entity with id #%d", id);
	exit(1);

	return NULL;
}

No surprises here. The function takes a parameter called `id`, which is the id of the entity we're after. We're looping through all the entities in our dungeon, searching for an entity with a matching `id` and returning it. If we don't find anything, we're printing an error and exiting. This might seem a bit heavy handed, but since we're expecting this entity to exist when loading, for it not to exist means the integrity of our save data has been lost. Our save data is very stable, so this shouldn't happen, unless a user has fiddled with the save.json file.

Moving onto monsters.c, we've updated 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->load = loadMonster;
	e->save = saveMonster;

	return m;
}

A simple change, we're setting the `e`'s `load` function pointer to loadMonster:


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

	m = (Monster*) self->data;

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

A standard loading function - we're pulling back all the data the Monster needs from the JSON object.

Moving over to armour.c, we've updated createArmour:


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->load = loadEquipment;
	e->save = saveEquipment;

	return eq;
}

We're assigning `e`'s `load` function to a new function named loadEquipment. This function, like saveEquipment, is a global function that lives in items.c:


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

	eq = (Equipment*) self->data;

	eq->hp = cJSON_GetObjectItem(root, "hp")->valueint;
	eq->minAttack = cJSON_GetObjectItem(root, "minAttack")->valueint;
	eq->maxAttack = cJSON_GetObjectItem(root, "maxAttack")->valueint;
	eq->defence = cJSON_GetObjectItem(root, "defence")->valueint;
	eq->savingThrow = cJSON_GetObjectItem(root, "savingThrow")->valueint;
}

Again, nothing special. We're setting the Equipment's data fields using the values found in the JSON object.

createWeapon in weapons.c has seen the loadEquipment function added:


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->load = loadEquipment;
	e->save = saveEquipment;

	return eq;
}

... as has createMicrochip, in microchips.c:


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->load = loadEquipment;
	e->save = saveEquipment;

	return eq;
}

doors.c has also had its `load` function attached:


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->load = load;
	e->save = save;

	return e;
}

It is now calling a function named `load`:


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

	d = (Door*) self->data;

	d->locked = cJSON_GetObjectItem(root, "locked")->valueint;
}

As expected, `load` is merely setting the Door's data from the supplied JSON object.

We're doing the same in stairs.c. initStairs has seen its own `load` function pointer assigned:


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->load = load;
	e->save = save;
}

And the `load` function itself:


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

	s = (Stairs*) self->data;

	s->dir = cJSON_GetObjectItem(root, "dir")->valueint;
}

As expected, we're setting the Stairs's data from that supplied by the JSON object.

Finally, we've updated initPlayer in player.c:


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->load = loadMonster;
	e->save = saveMonster;

	dungeon.player = e;

	updatePlayerAttributes(m, -1);

	m->hp = m->maxHP;

	moveDelay = 0;
}

As the player is a Monster, we're assigning the loadMonster function to `e`'s `load` pointer.

And that's our loading done! We've just got to do one more thing in order for it to work. Heading over to dungeon.c, we've updated initDungeon:


void initDungeon(void)
{
	memset(&dungeon, 0, sizeof(Dungeon));

	floorChangeTimer = FPS / 2;

	playerDeathAlpha = -FPS / 2;

	initMap();

	initHud();

	initInventory();

	initEntities();

	if (!loadGame())
	{
		createDungeon();
	}

	app.delegate.logic = logic;

	app.delegate.draw = draw;
}

We want the game to load our save data, if it's available. We do this by simply making a call to loadGame and testing the result. If loadGame returns 1, we'll do nothing more. However, if it returns 0 (false), we'll know that the game couldn't be loaded. We'll therefore call createDungeon to setup the game from the beginning.

The very last thing we'll do is make a tweak to game.c. As this is a traditional roguelike, we're only going to give the player one chance to make it to the Mouse King and taste victory. As such, we've made a change to initGameOver:


void initGameOver(void)
{
	deleteFile(SAVE_GAME_FILENAME);

	deleteFile(SAVE_MAP_FILENAME);

	addHighscore();

	clearDungeonEntities();

	free(dungeon.player->data);

	free(dungeon.player);

	destroyGame();

	initHighscoreView();
}

We're now making two calls to deleteFile, passing over SAVE_GAME_FILENAME and SAVE_MAP_FILENAME. These calls will delete our two save files upon the player being killed, meaning they will have to start over. The deleteFile function itself lives in util.c and is quite short:


void deleteFile(const char *filename)
{
	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, "Deleting file '%s' ...", filename);

	remove(filename);
}

remove is a function from stdio.h that removes a file by name. Good luck, adventurer! Best make sure you grind out a few levels, and stock up on health packs and antidotes before you venture higher!

Our game is very nearly complete. We've only one thing left to do, and that is to introduce the Mouse King and the means by which you will win the game. In our next part, we'll do just that. Be warned, however, that you might be very surprised by what is to come ...

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