Thursday, November 10, 2016


How to implement save/load functionality for games with levels


Some games don't have levels. Open world games, flappy bird, ..and cant think of another example right now, but some games have levels. In this article I will talk specifically about games where character can move between levels interact with objects around him and save the progress. Later he can load saved game and continue where he left of.

Requirements


First, lets write down the requirements that define our game:
  • game has multiple levels, levels have static (inane) and moving (interactive) parts.
  • player starts new game at level 1, but can load the game at any subsequent level
  • player moves between levels mostly forward, but he is able to go back to previous levels and he can jump between levels with some kind of teleport device. For example, on 5th level there is portal that takes him back to 2nd level, exactly the part of 2nd level that couldn't been accessed before.
  • level has multiple entry and exit points. This means if you go from level 3 to level 4 you appear by the river at the cave entrance. But if you go from level 8 to level 4 you appear in front of traders house. So level 4 has two entry points.
  • player can save position anywhere at any moment and load that saved position from anywhere in the game. if player exits game, unsaved progress is lost.

This is how we define level:
  • it is environment around the character with interactive pieces.
  • player can change state of interactive pieces, closed doors are left opened, aroused levers are pulled down, healthy living monsters are now not so well, coins are gone...
  • player can not change static environment: background, props, ambient music, things that stay the same no matter what you do on that level (some games allow player to destroy background environment, leaving it completely changed - its ok it just have to be taken into account as interactive piece as well).

For exercise, take a look at this example picture and try to identify interactive objects vs static background:

platform games are good example of games with levels


In this article I will discuss simplified version of such game where level only has 10 colored balls and static background. Each level has different background, but it can not be changed by player so no need to save it. Player can move the balls around and paint them different color so we want to be able to save these changes. We are using this simple scenario just to demonstrate the save/load ecosystem without adding complexity of level itself.

How do we code it?


Note: Im gonna use some pseudocode in this article that somewhat resembles javascript and json, but its not the code you can copypaste and run. 

First we define Level class/object with these fields:


Lets say our game has 20 levels, we are going to create 20 objects that describe initial state of our levels (where the balls are, what is their color, where are level exits and entrances). in real life we would store these in 20 different files, but for our very simple example we will use just one array with 20 elements:


And some globals for our simple example:

Let me explain what arrayOfVisitedLevels is about. When player starts the game for the first time ha appears on first level. He can then move to other levels: second, third, fourth, all without saving the game. And if he decides to go back a level, we want him to see all the changes he made on those previous levels, although he didnt hit the Save button yet. So this is what arrayOfVisitedLevels does, it holds all visited levels (and their changes) in RAM and when player hits Save button we take all these levels and store them to permanent memory and empty the array. So when player moves from level 4 to level 5 we have to ask these questions:

  • Is level 5 in arrayOfVisitedLevels? If yes it means player was just there
  • If not, is this level saved on disk? If yes we want to load it.
  • If not, player never went to this level before, so we load its initial state from our gameLevelsArray.

Below is how level loading could look like. This function is called when player is just starting a game, or when changing levels while playing the game.



Game Save


In this example we don't address how player moves around and changes the balls color and position, but he does and he is satisfied with what he's done and now he wants to save it.

Lets consider different saving scenarios:
  • Player starts new game, moves through three levels and then press save.
  • He then fiddles some more on third level, and goes back to second level and presses save there again.
  • After that, he goes back to third level, then fourth and fifth and finally saves again before exiting the game.

We want to have 5 levels saved so that when player loads the game he can go back and see those levels exactly as he left them.

While player was playing we decided to hold visited levels in dynamic variable, in memory. When he presses save, it would be nice to store those visited levels in permanent storage and release dynamic objects from RAM. So first save is pretty straight forward - he hits the save - we save three levels, and release first and second level from memory (third one is still being used). When player wants to move to second level again we have to check first if we have that level in RAM, if not we have to check if that level was visited before, and if it is - we load it from saved file. So now player wants to hit save button second time. He is at the second level but has third and second changed a little, so we have to save that too. If he saves over the same game, we can overwrite those levels in saved file. If he saves new game slot, we have to keep previously saved data in case he wants to load that first saved game later, so we create new save file, but what do we put in second save file - just second and third level or all levels from the start? By the time he hits save button third time, we understood we need to go back one step and discuss save position some more.



Save slots


Some games have checkpoints for saving progress. On the level there is some prop that player needs to approach to save the progress. If he dies, the game will automatically return him to last saved position. This is equivalent to one saving slot that is overwritten each time. Some games have multiple save slots, that allow you to name your saved game and then later overwrite it, or create a new one. When you think about it, saving each time to new game slot means last saved game should have all the data from previous saved games. We could make last saved game save only what is changed between previous saved game and now. Differential approach means smaller save files, but we must traverse through all previous saved game files when we are looking for some older level. Alternatively, last saved game could have all the levels accessed (changed) from the game start.



Now the fun starts. Imagine player has 20 saved games, and more then half game finished. And then he desides (decides) to click 'New Game'. He (or his brother) wants to start the game from the begining, and after 5 levels hits the save. Now whether you have differential or all-in-one approach, this saved game must be somehow separated from all others. And not just the "New Game", even if player load some older saved game and starts from there - he will take a new path from that point and branch into parallel adventure. When he keeps saving games on this new path, differential aproach must be able to trace back previous saved games not just by date, but some smarter linked list mechanism.

What I like to do here is create stories. Each time player starts a New Game he starts a new story. Each time he branches out from older saved game he creates another story. A story is a collection (linked list) of saved games that traces back to the begining. Even if you make a game with one save slot (checkpoint usecase) - you can use one story (in case you want to change it later). One save slot version has only one story. It can have numerous save nodes, but they are in a straight line. Load always loads last one. Start from beginning starts new story and previous one is deleted.

In this post I will only show scenario with one story. You can then do multiple stories as exercise, haha.

Here is example save function:

So Im using savedLevels here as some kind of preloaded array of saved games, and I edit values in this array first before storing it to persistent memory. Even if your levels are small like this, you dont need this preloaded array of saved games, but work directly with data from file/database. I just thought this would make save logic easier to understand.

At this point, bugs like "Go to next level, save, load, pick an item, go to previous level, save, load - drop item on the ground, save, load - ITEM IS GONE!" start appearing. It is getting exponentially harder to reproduce bugs so you better create good test scenarios that cover as much usecases you can think of. And get the QA guy repeat them until his eyes bleed.

But then you might want to complicate things some more with a little save/load optimization.


Optimization


You all played a game where loading screen was taking forever. Sometimes it even kills the joy out of playing. Sure you could always blame the computer being old, but sometimes the developers can do a little extra to make things snappy. 

Imagine saving a game on level X, after the save you dont move to another level, you dont play for long and change many things, you just move one ball a little bit, change your mind, and hit load again. What you want to see is that ball moved back to saved position and nothing else. Its just a tiny little change, how long should it take? Well, if we look at our functions above, we are calling clearLevel(currentLevelPointer), then loading level from savedLevels and calling loadLevelAssets(nextLevel), followed by showLevel(nextLevel). So basically clear everything and load and draw from the scratch. Its a safe path. But its not superb solution. We can do many things to avoid this overhead and I will show you one thing that I like to do.



I like to make additional check if level to be loaded is same as current level.
If it is, I dont want to erase everything and load everything from scratch - its already there on the screen. I just want to rearange dynamic objects to their saved state, and user will get the position he saved.

In our little example, i get the balls position and color from saved data and move them back to saved state. In more complicated level, I would also load player health, experience, monsters, container content, and everything else that can be changed, but it is still on the light level of changing properties and position and not doing heavy loading of models and pictures again. This is why I dont release monster from memory right after killing it, I just make it invisible. Some games could not afford such luxury and they would have to do some reloading, but still not all. All static content is there, loaded, visible on the screen, whether its a background image or 3d models of mountains and trees.

But as the game complexity grows, this little optimization will make you pull your hair out. Those will be the parts of your code that you dont remember how they work any more, and when the cobwebs and dust cover those functions spooky variables will stare at you from the dark asking for sacrifices.


Time to start


After all said and done, we have to mention what needs to be done on Init and when New Game is started. Init is called when player starts the game. New game loading can happen on start but also during gameplay if player decides to restart from beginning in which case we must clear the array of saved games and need to clear some stuff from the screen. Again, if player hits New Game while still on first level, we might optimize to avoid level reloading. This is what our new game function would look like.



Remember, when player hits New Game during gameplay, it doesnt mean he wants to lose his saved game. He still might want to hit Load Game and continue where he left of. BUT *mark this important* if he starts New Game and then hits Save Game - all his previous progress will be lost and you might want to warn him about it.

At last, here is the init function.


Extensibility


Once your save/load functionality is implemented and working flawlessly, you'll want to add new dynamic stuff to you levels. You or your boss will have this great idea that colored balls should be accompanied with colored squares. So how do we add the squares now?

There are two types of extensions, one that is affecting all levels, and another affecting only one specific level. If you want to add squares to all levels you have to 

1. Extend the level model/class:


2. Next you add square data to gameLevelsArray (see above, not gonna copy here again with some square example data).

3. Function clearLevel will be changed to erase squares.

4. Functions loadLevelAssets and showLevel are extended to include squares.

5. Functions overwriteSavedLevel, storeSavedDataToPermanentMemory and restoreSavedDataFromPermanentMemory need some edits as well.

As you can see its not small change, but its manageable. Its not impossible to add savable data but you have to remember all the places where you manipulate save and load data and add it there. For example I forgot to add squares to one function now. Its the one I told you it will come back to haunt you: its optimized loadGameOnSavedLevel. In this function we are not clearing all level assets, but just moving back dynamic objects in saved position so we need to add squares there as well.


Quirks


Second type of extensions is that one specific thing that you want to appear on level 17 and you dont need it anywhere else, I call it quirks. You want a flower on level 17 and when player eats a flower he gets extra life. Its totally out of game mechanics, specific thing, that you want saved just as well. These things can bring something interesting to the game, but often you dont think of them at the very beginning. So you add generic quirks array to each level. And you can use it later if you need it.

1. Extend each level with quirks array. It can be empty array in all levels at first.

2. Add levelOnLoad function for each level that is called when that level is loaded, and pass saved data to it. It can be empty function for all levels at first (if you use some sort of scripting in your games, then this can be script that is executed when level is loaded, its convenient as you dont have to edit source code later)

3. Have quirks saved and loaded if array is not empty. If your database dont like expanding arrays, have it have fixed array of 10 integers - all zeros.

Now imagine you want to add flower on level 17 at later stage. When level is loaded, you want to see the flower, but if player eats it and save the game you want to save the fact that flower is gone. This is what you do:

1. In gameLevelsArray add 1 to quirk array : quirks:[1],
2. In levelOnLoad (script) function draw flower only if quirk is 1
3. When player eats the flower, give him extra life but also set currentLevelPointer.quirks[0] to 0

Maybe this is stupid cause you are changing the code to add this flower eating functionality so you can also edit save and load functions to include this new feature, but I like to avoid changing save/load functions cause error in there can affect all other parts of the game. And sure it will look confusing to other coder what this level[17].quirks[0] thing is. But you dont care anymore at this point.


Conclusion


This functionality can be quite complicated, but good planing and thinking ahead can make it easier. So hopefully this article shows you something you didnt think of and helps you with the planning.

All of these concepts are used in 'real' code in two of my games that you can find opensourced on github:javascript game: https://github.com/bpetar/web-dungeonc++ game: https://github.com/bpetar/leonline

In the end, I must tell you that no one told me how to make save/load functionality and I never read any existing article or book on this subject and I made this up all by my self, so maybe there is a better way to do this, and I advise you to browse some more.