Save Systems — Games with Persistent Memory

By Carlos Montiel | Enterprise AI Specialist
Leer en español →
Published: 2026-07-28 | By: Carlos Montiel | Reading time: ~5 minutes

A game with no memory is a game the player abandons after the first session. Today we give your game the ability to remember progress between sessions.

Why Saving Is Trickier Than It Looks

In previous lessons we worked with data structures that live in memory while the game runs: positions, velocities, animation states. The moment you close the program, all of that disappears. A save system takes that state and writes it to disk so it can be rebuilt exactly the same way next time.

The main risk isn't writing the file — that part is easy. The risk is compatibility: what happens when you update your game and add a new field to the data structure, and now you have old save files that don't match the new format. We'll design the system with this in mind from the start.

Option 1: Simple Binary Save

The most direct way to save data in C++ is to write a structure's memory straight to a file with `fstream` in binary mode. It's fast and compact, but fragile against structural changes if not handled carefully.

#include <fstream> #include <cstdint> struct SaveData { uint32_t version = 1; int current_level; int lives; float position_x; float position_y; int coins; }; bool save_binary(const SaveData& data, const std::string& path) { std::ofstream file(path, std::ios::binary); if (!file) return false; file.write(reinterpret_cast<const char*>(&data), sizeof(SaveData)); return file.good(); } bool load_binary(SaveData& data, const std::string& path) { std::ifstream file(path, std::ios::binary); if (!file) return false; file.read(reinterpret_cast<char*>(&data), sizeof(SaveData)); return file.good(); }

Notice the `version` field at the start of the structure. You should always include a version number in your save formats — it's what lets you, in the future, detect an old file and migrate it instead of misreading it and corrupting the game state.

bool load_binary_safe(SaveData& data, const std::string& path) { if (!load_binary(data, path)) return false; if (data.version < 1 || data.version > 1) { SDL_Log("Unsupported save version: %u", data.version); return false; } return true; }

Why Raw Binary Has Limits

Writing an entire `struct` with `write()` works as long as the structure has no pointers or dynamic containers (`std::string`, `std::vector`). If your `SaveData` included a `std::vector<Item> inventory`, writing the raw memory would write the vector's internal pointer, not its data — and loading the file in another run would leave that pointer invalid. This is exactly the kind of bug worth asking your AI assistant to review before trusting a save system: "Is this structure safe to serialize with a raw `write()`, or does it contain something pointing to dynamic memory?"

Option 2: A Simple Text Format (Manual JSON)

For data that needs to be human-readable, hand-editable, or versioned with more flexibility (like variable-length inventory lists), a JSON-style text format is more robust. You could use a library like `nlohmann/json`, but to understand the concept from first principles we'll write a minimal manual serializer.

#include <fstream> #include <sstream> #include <vector> #include <string> struct Item { std::string name; int quantity; }; struct SaveDataJSON { int version = 1; int current_level; int lives; std::vector<Item> inventory; }; std::string serialize(const SaveDataJSON& p) { std::ostringstream json; json << "{\n"; json << " \"version\": " << p.version << ",\n"; json << " \"current_level\": " << p.current_level << ",\n"; json << " \"lives\": " << p.lives << ",\n"; json << " \"inventory\": [\n"; for (size_t i = 0; i < p.inventory.size(); ++i) { json << " { \"name\": \"" << p.inventory[i].name << "\", \"quantity\": " << p.inventory[i].quantity << " }"; if (i + 1 < p.inventory.size()) json << ","; json << "\n"; } json << " ]\n}\n"; return json.str(); } bool save_json(const SaveDataJSON& p, const std::string& path) { std::ofstream file(path); if (!file) return false; file << serialize(p); return file.good(); }

This manual serializer is useful for understanding the problem, but for a real project I'd recommend using a proven library like `nlohmann/json` instead of maintaining your own parser — writing a correct JSON parser (with quote escaping, negative numbers, nesting) is more work than it looks. This is a perfect case for asking your AI assistant to wire the library into your `CMakeLists.txt` and show you the equivalent using `nlohmann::json` instead of the manual serializer.

Validating Data on Load

Never blindly trust a save file, even one your own game generated — it could be corrupted by an abrupt shutdown, a full disk, or manual editing by the player.

bool validate_save(const SaveData& data) { if (data.lives < 0 || data.lives > 99) return false; if (data.current_level < 1 || data.current_level > 50) return false; if (data.coins < 0) return false; return true; } SaveData load_save_or_default(const std::string& path) { SaveData data; if (load_binary_safe(data, path) && validate_save(data)) { return data; } SDL_Log("Invalid or missing save, using default data"); return SaveData{}; // struct default values }

Atomic Saving: Avoiding Half-Written Corrupt Files

If your game closes (or crashes) right while it's writing the save file, you can end up with a half-written file. The standard technique is to write to a temporary file first and, only if the write succeeds, replace the original file.

#include <cstdio> bool save_atomic(const SaveData& data, const std::string& path) { std::string temp_path = path + ".tmp"; if (!save_binary(data, temp_path)) return false; if (std::rename(temp_path.c_str(), path.c_str()) != 0) { std::remove(temp_path.c_str()); return false; } return true; }

`std::rename` is an atomic operation on most filesystems: either the new file completely replaces the old one, or nothing happens — you never end up with a corrupt intermediate state.

With persistent saving handled, in the next lesson we'll organize the game's overall flow (main menu, gameplay, pause, end screen) with a state machine, which is the structure that will let you load a saved game from a menu cleanly.

Carlos Montiel
Enterprise AI Solutions Architect
Specialist in LLMs, Agents, and Orchestration
guatemalia.com/en/#contact · info@guatemalia.com

Need to implement AI at your company?

Carlos Montiel is an enterprise AI solutions architect. He implements LLMs, Agents, RAG, and orchestrators for companies across Guatemala and Latin America. Reach out for a consultation.

Contact Carlos Montiel

info@guatemalia.com