Introduction
Some games don't have levels. Open world games, flappy bird, ..and can't think of another example right now, but some games have levels. In this article, I will talk specifically about games where the 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 off.
Requirements
First, let's write down the requirements that define our game:
- the game has multiple levels, levels have static (inane) and moving (interactive) parts.
- the player starts a new game at level 1 but can load the game at any subsequent level
- the player moves between levels mostly forward, but he can 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.
- a 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.
- the 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 the environment around the character with interactive pieces.
- the player can change the 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...
- the 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 - it's ok it just have to be taken into account as an interactive piece as well).
In this article, I will discuss a simplified version of such game where level only has 10 colored balls and static background. Each level has a different background, but it can not be changed by the player so no need to save it. The player can move the balls around and paint them different colors 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 the complexity of level itself.
How do we code it?
I'm going to use some pseudocode in this article that sometimes resembles JavaScript and json, but it's not the code you can copy-paste and run. First, we define Level class/object with these fields:
LevelObj
{
//simple number
id
//image path
background
//position of balls and their color
array levelBalls[{position,color}]
//position of exits and where they lead to
array levelExits[{position,idOfNextLevel,idOfNextLevelEntrance}]
//position of player and facing upon level entrance
array levelEntrances[{id,position,rotation}]
}
Let's 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:
gameLevelsArray = [
//level 1
{
id:1,
levelBalls:[
{
"position": {"x":2, "y":3},
"color": "red"
},
{
"position": {"x":4, "y":6},
"color": "red"
},
{
"position": {"x":9, "y":9},
"color": "green"
},
...
],
levelExits:[
{
"position": {"x":3, "y":3},
"nextLevelID": 2,
"nextLevelEntrance": 1
},
...
],
levelEntrances:[
{
"entranceID": 1,
"position": {"x":1, "y":2},
"rotation": 90
},
...
]
},
//level 2
{
id:2,
levelBalls:[...],
levelExits:[...],
levelEntrances:[...]
},
...
];
Globals:
- currentLevelPointer //initially null
- gameState //initially GAME_STATE_MAINMENU;
- arrayOfVisitedLevels //initially empty, but we push levels to this array as player visits them
The player starts the game at Main Menu, and there he can choose "New Game" or "Load Game." We splash the loading screen and in the background load assets for either first level, or some saved level and position.
Let me explain what arrayOfVisitedLevels is about. When the player starts the game for the first time, he appears on the 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 didn't hit the Save button yet. So this is what arrayOfVisitedLevels does, it holds all visited levels (and their changes) in RAM, and when the player hits the Save button, we take all these levels and store them to permanent memory and empty the array. So when the 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, the player never went to this level before, so we load its initial state from our gameLevelsArray.
Below is what level loading could look like. This function is called when the player is just starting a game, or when changing levels while playing the game.
//this function takes parameters
//id - what level we are going to
//entrance - what entrance we shall appear at
function loadLevel(id, entrance)
{
gameState = GAME_STATE_LOADING;
//if game hasn't just started, we need to cleanup previous level data
if(currentLevelPointer != null)
{
//save current level - here we just push pointer to our level object to an array
arrayOfVisitedLevels.push(currentLevelPointer);
//clear current level - we want to render new one
//for example this could delete objects from the 3d scene
clearLevel(currentLevelPointer); //this function will have access to array of balls on that level and erase them, also the background
}
//if we are entering level that we already visited
if(levelAlreadyVisited(id) //check arrayOfVisitedLevels to see if id is in there
{
//we get the level from array of visited levels
//big idea here is that all the changes player made to this level are still in this object in memory
nextLevel = getLevelFromArrayByID(arrayOfVisitedLevels,id);
}
else if(levelIsSaved(id) //check to see if level is saved
{
//we get the level from permanent storage
nextLevel = getLevelFromArrayByID(savedLevels,id);
}
else
{
//get level from array of game levels - these are default unchanged levels
//in real life we would load level from file here
nextLevel = getLevelFromArrayByID(gameLevelsArray,id);
}
//now that we got the level we need, lets draw it
loadLevelAssets(nextLevel);
showLevel(nextLevel);
//place player at given entrance
player.position = entrance.position;
player.facing = entrance.rotation;
//remove the loading screen and start the game loop
gameState = GAME_STATE_PLAY;
}
Game Save
In this example, we don't address how the player moves around and changes the ball's color and position, but he does and he is satisfied with what he's done and now he wants to save it. Let's consider different saving scenarios:
- The player starts a new game, moves through three levels and then press save.
- He then fiddles some more on the third level and goes back to the second level, and presses save there again.
- After that, he goes back to the third level, then fourth and fifth and finally saves again before exiting the game.
We want to have 5 levels saved so that when the player loads the game he can go back and see those levels exactly as he left them.
While the player was playing we decided to hold visited levels in a 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 (the third one is still being used). When the player wants to move to the 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, which 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 a previously saved game and now. The differential approach means smaller save files, but we must traverse through all previously 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 the player has 20 saved games, and more than half of the game finished. And then he decides to click 'New Game.' He (or his brother) wants to start the game from the beginning, 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 a parallel adventure. When he keeps saving games on this new path, the differential approach 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 beginning. Even if you make a game with one save slot (checkpoint use case) - 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 the last one. Start from beginning starts a new story, and the previous one is deleted.
In this post, I will only show the scenario with one story. You can then do multiple stories as your own exercise, haha.
Here is example save function:
saveLevel()
{
//here are some hints not related to our simple ball game:
//save player stats in that moment: xp, hp, strength...
//save player inventory, and also what he has equipped
//these are level independent data, but needs to be saved each time
//save player position and rotation
//save what is in array of visited levels
for (var i=0; i.id;
var levelToSave = getLevelFromArrayByID(arrayOfVisitedLevels,levelId);
if(levelIsSaved(levelId)) //if this level is already saved, in one story we overwrite it
{
overwriteSavedLevel(savedLevels, levelId, levelToSave); //copy balls data to saved level object
}
else
{
addSavedLevel(savedLevels, levelId, levelToSave); //push new level object in savedLevels
}
}
//now save current level, again repeating check if level is saved
if(levelIsSaved(currentLevelPointer.id)) //if this level is already saved, in one story we overwrite it
{
//copy balls data to saved level object
overwriteSavedLevel(savedLevels, currentLevelPointer.id, currentLevelPointer);
}
else
{
//push new level object in savedLevels
addSavedLevel(savedLevels, currentLevelPointer.id, currentLevelPointer);
}
//now persist our saved data to file or database:
storeSavedDataToPermanentMemory(savedLevels);
}
So I'm 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 don't 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 the previous level, save, load - drop an item on the ground, save, load - ITEM IS GONE!" start appearing. It is getting exponentially harder to reproduce bugs so you had better create good test scenarios that cover as many use cases 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 don't move to another level, you don't 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. It's 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. It's a safe path, but it's not a 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 an additional check if level to be loaded is same as current level.
If it is, I don't want to erase everything and load everything from scratch - it's already there on the screen. I just want to rearrange dynamic objects to their saved state, and the user will get the position he saved.
In our little example, I get the ball's position and color from saved data and move them back to saved state. In a 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 the heavy loading of models and pictures again. This is why I don't release the monster from memory right after killing it, and 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 it's 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 don't remember how they work anymore, 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, I will add what needs to be done when New Game is started. This can happen during gameplay, so we might need to clear some stuff from the screen. Again, if the player hits New Game on the first level, we might optimize to avoid level reloading. This is what our new game function would look like.
function NewGame()
{
gameState = GAME_STATE_LOADING;
//get saved levels into practical array variable
savedLevels = restoreSavedDataFromPermanentMemory();
//erase this array
arrayOfVisitedLevels = [];
//check if this is a fresh start, or player is already progressing through the game
if(currentLevelPointer != null)
{
//clear up, make a fresh start
clearLevel(currentLevelPointer);
}
else if (currentLevelPointer == 1)
{
//optionally do optimized reload here
reloadNewGameOnFirstLevel(); //just move the balls and player back to starting position.
//remove the loading screen and start the game loop
gameState = GAME_STATE_PLAY;
return;
}
//get level from array of game levels - these are default unchanged levels
firstLevel = getLevelFromArrayByID(gameLevelsArray,id);
//now that we got the level we need, lets draw it
loadLevelAssets(firstLevel);
showLevel(firstLevel);
//place player at given entrance
player.position = firstLevel.levelEntrances[0].position;
player.facing = firstLevel.levelEntrances[0].rotation;
//remove the loading screen and start the game loop
gameState = GAME_STATE_PLAY;
}
Remember, when the player hits New Game during gameplay, it doesn't mean he wants to lose his saved game. He still might want to hit Load Game and continue where he left off, 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.
Extensibility
Once your save/load functionality is implemented and working flawlessly, you'll want to add new dynamic stuff to your 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:
LevelObj
{
id //simple number
background //image path
array levelBalls[{position,color}] //position of balls and their color
array levelSquares[{position,color}] //position of squares and their color
array levelExits[{position,idOfNextLevel,idOfNextLevelEntrance}] //position of exit and where it leads to
array levelEntrances[{id,position,rotation}] //position of player and facing upon level entrance
}
2. Next, you add square data to gameLevelsArray (see above, not going to 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 it's not small change, but it's manageable. It's 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. It's the one I told you it would come back to haunt you: it's 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
The second type of extension is that one specific thing that you want to appear on level 17 and you don't need it anywhere else, I call it quirks. You want a flower on level 17 and when the player eats a flower, he gets the extra life. It's totally out of game mechanics, specific thing that you want to be saved just as well. These things can bring something interesting to the game, but often you don't 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.
LevelObj
{
id //simple number
background //image path
array quirks[] //what good is empty array for? TODO: remove this
array levelBalls[{position,color}] //position of balls and their color
array levelSquares[{position,color}] //position of squares and their color
array levelExits[{position,idOfNextLevel,idOfNextLevelEntrance}] //position of exit and where it leads to
array levelEntrances[{id,position,rotation}] //position of player and facing upon level entrance
}
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, it's convenient as you don't have to edit source code later)
3. Have quirks saved and loaded if the array is not empty. If your database doesn't like expanding arrays have it have fixed array of 10 integers - all zeros.
Now imagine you want to add a flower on level 17 at a later stage. When the level is loaded, you want to see the flower, but if the player eats it and saves 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 an error in there can affect all other parts of the game. And sure it will look confusing to another coder what this level[17].quirks[0] a thing is. But you don't care anymore at this point.
Conclusion
This functionality can be quite complicated, but good planning and thinking ahead can make it easier. So hopefully this article shows you something you didn't 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 open sourced on github:
javascript game: https://github.com/bpetar/web-dungeon
c++ 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.
Article Update Log
21 Sep 2016: Initial release