I wrote the save game system for Disney's Guilty Party. I can give you a high level overview of how it was done. Although it was shipped as a Wii game, for debugging purposes we needed a save game system that worked both on the Wii and on the PC. Disclaimer: this is the only time I've written such a system, and by no means can I claim that it is The Standard Way(tm) to write a save game system. It is more of a case study.
From a purely design standpoint, binary files are always a hassle. For one thing, they are extremely hard to debug since you have to open the save game files in a hex editor. Second, you cannot download a save game file generated on the console and open it on the PC without making sure that your endianness is correct, which requires you either to A) have duplicate serialization code for big and little endian machines or B) wrap your serialization code in swapBytes() calls that switch things based on the endianness of your machine. Both are clunky.
Hence, we were looking for a save game system that had the ability to
- Be able to be read and written from both the Wii and the PC with the same format.
- Be easy to debug the serialization of our objects
- Have a versioning system that allowed us to read save game files from previous versions of the game in order to track down crash reports during playtesting of old builds.
- Ignore loading time because loading a game from a deserialized save file was on the same order of magnitude as loading a fresh level from scratch.
With these ideas in mind, we used (roughly) the following system. Each save game file was saved as plaintext. The beginning of each save game file always started with a line that looked like:
Version: 21
Then, we used something akin to the following for serializing and deserializing:
const unsigned int kCurrentVersion = 24;
void Serialize(GameState &state, const char *filename) {
File saveFile = File::Open(filename);
saveFile.Write("%d\n", kCurrentVersion);
saveFile.Write("%f %f %f\n", state.playerPosition);
saveFile.Write("%f %f %f\n", state.playerPantsColor);
saveFile.Write("%d\n", state.kNumTacosIHadForLunch);
}
// Any save game file before this version is invalid...
const unsigned int kMinimumSupportedVersion = 18;
GameState Deserialize(const char *filename) {
File saveFile = File::Open(filename);
unsigned int version = 0;
GameState savedState;
if(saveFile.Read("%d\n", &version) != 1 || !version) {
fprintf(stderr, "No version... no idea what's going on.");
return kInvalidState;
}
if(version < kMinimumSupportedVersion || version > kCurrentVersion) {
fprintf(stderr, "Unsupported version: %d\n", version);
return kInvalidState;
}
float pos[3];
if(saveFile.Read("%f %f %f\n", &pos[0], &pos[0], &pos[0]) != 3) {
fprintf(stderr, "Error reading position.\n");
return kInvalidState;
} else {
savedState.setPlayerPosition(pos);
}
if(version > 10) {
float c[3];
if(saveFile.Read("%f %f %f\n", &c[0], &c[1], &c[2]) != 3) {
fprintf(stderr, "Error reading pants color.\n");
return kInvalidState;
} else {
savedState.setPantsColor(c);
}
}
if(version > 17 && version < 22) {
int nsiwaat;
if(saveFile.Read("%d\n", &nsiwaat) != 1) {
fprintf(stderr, "Error reading the number of socks I wear at a time.\n");
return kInvalidState;
} else {
savedState.setNSIWAAT(nsiwaat);
}
}
// ...etc
return savedState;
}
The key takeaways from this are: Your serialization code should always write the most up to date version of your game state, regardless of what that is. However, it must also write what the most up to date version is so that future (and current) versions of the game are able to know how to read the file.
The deserialization code gets a little bit messy with this technique. It takes a very defensive approach to reading save game files. There are security reasons (i.e. buffer overflow) for why this is a good idea. It also reduces the risk of reading a corrupt save game file and loading bad data and crashing (or worse, not crashing). For debugging purposes, however, you should make sure to be as verbose as possible for why a saved game could not be read. This will save you many headaches in future versions where you thought a thing should be a certain way but it isn't. This system proved to not only be very effective, but I had a very easy time maintaining it.