• 2D shoot 'em up
SDL2 Rogue tutorial
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— Creating a simple roguelike —
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:
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:
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:
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).
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:
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:
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.
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:
We're just setting game's highscore data from that contained in the JSON object.
loadMap is the final function to consider:
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:
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:
A simple change, we're setting the `e`'s `load` function pointer to loadMonster:
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:
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:
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:
... as has createMicrochip, in microchips.c:
doors.c has also had its `load` function attached:
It is now calling a function named `load`:
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:
And the `load` function itself:
As expected, we're setting the Stairs's data from that supplied by the JSON object.
Finally, we've updated initPlayer in player.c:
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:
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:
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:
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 ...
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.
Share your comments and thoughts below. All comments are anonymous and cannot be edited.