• 2D shoot 'em up
SDL2 Santa game tutorial 🎅
SDL2 Shooter 3 tutorial
The Legend of Edgar 1.36
SDL2 map editor tutorial [UPDATED]
TBFTSS: The Pandoran War - Amiga OS4 Port
— Creating a simple roguelike —
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 cmake CMakeLists.txt, followed by 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:
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:
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:
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:
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:
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:
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:
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:
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:
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.
We're just storing game's highscore's data. Nothing complicated.
saveMap is the last function to consider:
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:
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:
And the same again in microchips.c, for createMicrochip:
If we now look at items.c, we can see how the saveEquipment function is defined:
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:
We've assigned the `save` field to a function called saveMonster:
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:
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:
We've assigned the `save` function pointer to `save`:
And we're adding the Door's fields to the JSON object.
stairs.c has also been updated. initStairs:
We're assigning the `e`'s `save` function to `save`:
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:
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:
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:
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.
The source code for all parts of this tutorial (including assets) is available for purchase:
It is also available as part of the SDL2 tutorial bundle: