-
20. Putting your code in the right place: a brief introduction to prg banking
21. Giving your main character a sword
22. Adding more features to the pause menu
23. Adding a second map
24. Saving the Game
25. Adding objects that attract or repel the player
26. Adding an enemy that mimics player behavior
27. Adding a new sprite size
-
40. Understanding and tweaking the build tools
41. Dealing with limited ROM space
42. Resizing your rom
43. ROM Data Map
44. Expanding available graphics using CHR banking
45. Getting finer control over graphics with chr ram
46. Writing Functions in Assembly
47. Automatic builds with GH Actions
48. Switching to unrom 512 for advanced features
Saving the Game
Sometimes adventure games are big - really big, and you don’t want to complete them all in one sitting. Some NES games let you save your game, then come back later. It turns out this isn’t hard to do at all, so you can use it in your own game!
The default options for the cartridge support up to 8kb (8192 bytes) worth of save ram. If you power the console off, then turn it back on, the values in this ram will stay the same. We of course don’t look directly at RAM. We instead care about variables - for us this means that specially marked variables will keep the same value even after the console is turned off. Cool, huh? You could also use this RAM as additional regular RAM if you wanted to - you just have to make sure to clear it on system startup.
We can demonstrate this by adding a counter that shows how many times the game has been beaten. You could use the same method to save any other data you wanted.
How to do it
Normally, when defining a variable, you would do the following to define it:
unsigned char gamesBeaten;
This is really close to what we need to do for this example - we just need a little bit of extra helper code to make the variable a saved variable. (This is enabled by a macro built into the engine.) Here’s what you need to do to keep the variable saved between resets:
SRAM_DEF(unsigned char, gamesBeaten);
Put this into source/c/globals.c
, so we can use it when we need it.
Also be sure to add the following to source/c/globals.h
so we can actually use the variable from other C code:
SRAM_EXTERN(unsigned char, gamesBeaten);
This is very similar to the delcarations we do for ZEROPAGE variables. That’s it, actually - any value
you store into the gamesBeaten
variable will be saved until the next time it is overwritten. How can we
prove this though? Let’s start setting it!
When a variable is first defined, it has an unknown value - it could be 59, 72, 255, or any other 8 bit value! That’s not ideal. There is a section further down in this chapter describing how to detect this and clear your variables properly, however for now let’s just set the value once to put it into a known state.
Note: Using save states or clearing your save game during this tutorial could put you into an unknown state. Make sure you read the rest of this chapter to make sure that doesn’t happen to players of your game!
Let’s add this to our main()
function in source/c/main.c
, to set the value once to a known value. Add this
to the very top of it:
void main(void) {
gamesBeaten = 0;
fade_out_instant();
gameState = GAME_STATE_SYSTEM_INIT;
// And so on...
Now, open your game, and get past the title menu once. Then, close it. Your emulator should have just saved the state of the game in its save folder.
Once that is done, remove the line from main()
that you just added; we don’t need it anymore!
Now, we need to increase this counter every time you beat the game, and also show it somewhere. (The title screen)
Let’s start by updating it when you beat the game. To show the credits, we set gameState
to GAME_STATE_CREDITS
, and
there is some code to handle this towards the bottom of source/c/main.c
- let’s add our variable here. Find the case
statement for GAME_STATE_CREDITS
and add the first line here:
case GAME_STATE_CREDITS:
gamesBeaten++;
music_stop();
sfx_play(SFX_WIN, SFX_CHANNEL_1);
With that one line, we will track every time you beat the game. Brilliant! Now we just need to show it on the title
screen. This is drawn by the draw_title_screen()
method in source/c/menus/title.c
. We won’t go into extreme detail,
but you can draw it by adding the following lines anywhere before we call ppu_on_all()
:
// draw the number of games the player has beaten
put_str(NTADR_A(6, 22), "Games Beaten: ");
vram_put((gamesBeaten / 10) + 0x10);
vram_put((gamesBeaten % 10) + 0x10);
Okay, that’s it for the basic example! Start the game up, you should see zero. Beat it once, it should go up to one. Close the emulator and re-open it; you should still see one! Saved! Read on for some more information on how to use this correctly.
Saving arrays of data
If you’re doing anything complex, you likely would want to save arrays of data as well. Luckily, it’s just as simple. If you created any arrays in ZEROPAGE (or looked at the code that exists) you can likely guess what it looks like. In either case, here’s an example:
SRAM_ARRAY_DEF(unsigned char, saveData, 25);
This will result in a saved variable that would otherwise look like unsigned char saveData[25]
. Be sure to clear it
out using a for loop when you are resetting your game’s data. Read on for information on that.
Resetting your data when someone plays for the first time
As was mentioned earlier in this chapter, when you first create a variable in SRAM, it could have any valid value -
for an unsigned character, it could be any value between 0
and 255
; for an integer, anything from -32768
to
32767
. Your game will likely rely on this data, so we can’t have that.
Two common ways of dealing with that are adding a known “magic” value to your SRAM, and tracking a checksum of the data. In either case, you save this value with the rest of your data, then check it when the game is started. If the this value is not what you expect, you can assume the data is wrong, and clear it out.
Either one will let you set an initial state for your game, and also give you an easy way for you to clear your saved game. (This is especially important on real cartridges, since clearing the save data on those takes effort.)
This author prefers the “magic value” method for its simplicity, but both are presented here for your use.
The simple way, using a magic value
The simplest way you can solve this problem is to write a known value to SRAM, and test if this value is present when the game is started up. If this value is not exactly equal to your expected value, then you need to clear all save data and put it into your expected initial state.
Pros:
- Simple, easy to understand
- Decently reliable
- Easy to clear by changing the “magic” value
Cons:
- Value has to be manually changed every time you add new variables to save
- Won’t help with corruption of RAM if it doesn’t change your “magic” value
- Has a 1/256 chance of being correct by chance
To implement this, we need to add a few new things. This can be combined with the example above to test it;
don’t delete that code yet! First, we need to create that magic value in SRAM. Add the following to
source/c/globals.c
:
SRAM_DEF(unsigned int, magicValue);
Next we need to create an extern
version of this number so we can use it from outside. While we’re in the file, we
will also create a constant called EXPECTED_MAGIC_VALUE
which we will compare against. You can set this to any valid
integer - the value of 100
below is completely arbitrary. Add this to source/c/globals.h
:
#define EXPECTED_MAGIC_VALUE 100
SRAM_EXTERN(unsigned int, magicValue);
Now, we need to add a way to check the SRAM to see if it needs to be reset. Let’s add a method called
test_and_reset_sram()
to the top of source/c/main.c
to test for our magic value, and reset things if they are
not as we’d like:
void test_and_reset_sram(void) {
if (magicValue != EXPECTED_MAGIC_VALUE) {
// Reset all sram values to their expected state here
gamesBeaten = 0;
// If you have any array values, use a for loop to set every value in the array correctly.
magicValue = EXPECTED_MAGIC_VALUE;
}
}
Finally, we can call this method from our main()
method to make sure it happens every time we start the console.
You could add this in a few places, but I prefer add it to the GAME_STATE_SYSTEM_INIT
case in the main loop:
switch (gameState) {
case GAME_STATE_SYSTEM_INIT:
test_and_reset_sram();
// Existing code
initialize_variables();
gameState = GAME_STATE_TITLE_DRAW;
break;
If you try to boot the game again, you should find that the games beaten value has been reset, but continues to work
as expected. If you change EXPECTED_MAGIC_VALUE
this will cause your save data to be cleared when the game is next
started up.
I suggest changing EXPECTED_MAGIC_VALUE
every time you change the values saved in SRAM, or alternatively with every
major version of your game that you release. Warn people that new versions will reset their game progress, of course.
However, a reset game is likely better than a game with corrupted save data!
The more foolproof way, using checksums
The “magic value” method is very simple and easy to understand, but there is a bit of manual work involved with
changing that value regularly - you can also mess up if you forget to change the value at all. Plus, there is a
miniscule chance that the save ram will just happen to be set to your magic value, which could cause the game
to start with a corrupted save. (This is very unlikely unless you pick a magic value of 0
though.)
If you’d like a little extra protection, in return for a bit of extra code an maintenance, here’s how you can do it.
Pros:
- Less likely to run into corrupted save
- Less likely to make mistakes manually updating
EXPECTED_MAGIC_VALUE
(which we don’t use)
Cons:
- Higher complexity; easier to mess up (resulting in lost save data)
- Maintenance effort a bit higher - you need to make sure all values are considered in the checksum
- More code involved in checking value
Wikipedia can explain checksums a lot better than I can, but for our purposes, a checksum is a small value that can be used to validate that the data stored in ram for the game is not corrupted. (And has been written before.)
There are many complex ways of doing this, but we’re going to go for a dead simple approach where we just add all of our stored values together into one integer for comparison. This value will likely overflow multiple times in complex cases, but that is okay! We only care about the resulting value.
First, we need a variable to store this value in - we’ll use an unsigned integer, which should give us some confidence.
Put this into source/c/globals.c
:
unsigned int temporarySramChecksum; // NOTE: You could also reuse a tempInt value if you would prefer
SRAM_DEF(unsigned int, sramChecksum);
And this into source/c/globals.h
to access it;
SRAM_EXTERN(unsigned int, sramChecksum);
Now, we need a method to calculate this checksum. For simplicity we can put this into source/c/main.c
, however if you
want to maintain this code, I strongly suggest creating a new file for SRAM data. Create the following method:
void calculateSramChecksum(void) {
temporarySramChecksum = 0;
// Add an entry for every single SRAM value you store here:
temporarySramChecksum += gamesBeaten;
// If you have any arrays, be sure to add every single using a for loop, like in
// the following commented-out example
/*
for (i = 0; i != 10; ++i) {
temporarySramChecksum += myArray[i];
}
*/
// temporarySramChecksum now contains the checksum value we need.
}
This method will allow us to calculate the checksum for all of our SRAM data. Make sure you update this method every time you add new SRAM data, or you could end up with corrupted save files.
Now, we need to use this method on startup to verify that our save data is correct, and update it if it isn’t. We can
use something similar to what the magic byte method uses. We will put this in source/c/main.c
but it could go in the
same separate file as well. Add a new method called test_and_reset_sram()
:
void test_and_reset_sram(void) {
calculateSramChecksum();
if (temporarySramChecksum != sramChecksum) {
// Our save ram is corrupted or blank; reset all save ram for a new game.
gamesBeaten = 0;
// If you had an array you were using, you would do this to clear it. (Commented out)
/*
for (i = 0; i != 10; ++i) {
myArray[i] = 0;
}
*/
calculateSramChecksum();
sramChecksum = temporarySramChecksum;
}
}
We can call this from main()
on game startup to make sure that the save RAM is in a known good state. (Look at the
example in the “magic byte” section - the code in main()
will be identical.
There’s one last major step we need to take - every time we update any value in save ram, we must update the
checksum, or your save data will be marked as corrupted, and reset!! We thus need to update any place we change
save ram, and update the checksum. We can create a helper method for this in source/c/main.c
:
void update_sram_checksum(void) {
calculateSramChecksum();
sramChecksum = temporarySramChecksum;
}
We must call this update_sram_checksum()
method every single time we update a save ram value. In our gamesBeaten
example, we would call this right after. Here’s the updated section from source/c/main.c
:
case GAME_STATE_CREDITS:
gamesBeaten++;
update_sram_checksum();
music_stop();
It is critical that we remember to update this value every time we update save ram. Otherwise though, this example
should now work. You can re-launch the game, and the value of gamesBeaten
should update as it did before. Now you
are more protected against save data corruption!