Losing hours of play to a corrupted save is one of the most frustrating experiences for a player, and one of the easiest negative reviews for a developer to avoid. A reliable save system should be designed early, because it shapes how data is organised throughout the whole game.
What to actually save
The most common mistake is trying to save «everything», serialising entire engine objects with references, textures and internal state. The result is fragile, bulky and breaks at the first update. It is better to distinguish:
- static data: levels, enemy stats, item descriptions. These are part of the game and should not be saved;
- persistent state: what the player has changed, meaning current level, position, inventory, completed quests, opened doors, unique enemies defeated, settings.
The save contains only the second group, expressed as simple data: numbers, strings, lists, identifiers. An inventory item is saved as "iron_sword" with a quantity, not as the whole object with its sprite.
Stable identifiers
To remember that a chest has been opened, every persistent element in the world needs a unique, stable identifier that does not change if the level is edited. Using the object's index in a list is risky: add a chest in the new version and all the following indices shift, leaving existing saves inconsistent.
Formats: text or binary
| Format | Advantages | Disadvantages |
|---|---|---|
| JSON or similar | Readable, easy to inspect and fix | Bulkier, editable by players |
| Custom binary | Compact, fast to read | Hard to inspect, versioning is trickier |
| Embedded database | Great for lots of data and partial saves | Extra complexity |
For most games a text format such as JSON, possibly compressed, is an excellent choice. During development, being able to open a save in a text editor saves a great deal of time. The fact that players can edit it is a problem only in competitive online games, where the important state should live on the server anyway.
Save file versions
The game will change after release: new features, renamed items, redesigned systems. Saves created with the previous version must keep working. The rule is simple: every file contains a version number.
{
"version": 3,
"level": "forest_02",
"health": 74,
"inventory": [{"id": "potion", "quantity": 3}]
}
On load, if the version is old, migrations are applied in sequence: from 1 to 2 a field is added with a default value, from 2 to 3 an identifier is renamed. Each migration is a small function that transforms the data, and they all stay in the code. That way a save from many versions ago is upgraded step by step.
Other useful tricks:
- ignore unknown fields instead of raising an error;
- use sensible defaults for missing fields;
- never remove the ability to read a version that has already shipped.
Writing without corrupting
If the game closes, the system goes to sleep or the power fails during writing, the file can be left half-written. A robust procedure:
- write the save to a temporary file;
- make sure the data is actually on disk;
- rename the temporary file over the old one, an operation that is atomic on most file systems;
- keep one or two backup copies of previous versions.
On load, integrity is checked, for example with a checksum stored alongside the data. If the main file is damaged, the most recent backup is offered instead of wiping the progress.
When to save
Design choices affect the technique too:
- fixed save points: the state to save is predictable and limited;
- save anywhere: you must be able to serialise the state at any moment, even mid-fight, with bullets in flight and enemies moving;
- autosave: should happen at quiet moments, such as level changes or the end of a dialogue, and in a separate slot, so as not to overwrite a manual save.
Saving during play can cause small hitches. Gather the data quickly on the main thread and hand the disk write to a background thread, showing an icon warning the player not to switch off.
Where to save
Every operating system has specific folders for user application data. Writing to the game's installation folder is a bad idea: you often lack permission, and an update or reinstall could delete everything. Engines provide functions that return the correct path on each platform. On platforms with cloud saves, a compact format and versioning become even more important.
Testing the system
The save system should be treated as a critical feature: automated tests that save and reload state and check that it matches, a collection of saves from every shipped version to load on each new build, and tests that interrupt the process mid-write.
In short
- Save only persistent state, with simple data and stable identifiers.
- JSON is often the most practical choice; binary is for when size and speed matter.
- Every file has a version number, and migrations upgrade old saves.
- A temporary file, atomic rename and backups prevent corruption.
- Autosaves at quiet moments and the correct system folders complete the picture.