Tutorial 19: Saving and Loading Data
CNA implements the XNA 4.0 StorageDevice / StorageContainer API for reading and writing save data. The API abstracts over platform-specific paths: on Linux it writes to ~/.local/share/<GameName>, on Windows to %APPDATA%\<GameName>, on Android to the app's internal files directory, and on Web to Emscripten's persistent file system (backed by IndexedDB).
StorageDevice overview
StorageDevice represents a physical or virtual storage medium. In XNA on Xbox 360 this was the memory unit or hard drive; in CNA it maps to the current platform's writable area. You obtain a StorageDevice asynchronously via StorageDevice::BeginShowSelector() (desktop platforms auto-select the default device).
#include "Microsoft/Xna/Framework/Storage/StorageDevice.hpp"
#include "Microsoft/Xna/Framework/Storage/StorageContainer.hpp"
using namespace Microsoft::Xna::Framework::Storage;
StorageContainer
A StorageContainer is a named folder within a StorageDevice. Each game typically uses one container per save slot. Open a container with StorageDevice::OpenContainer():
// Open (or create) a container named "SaveSlot1"
auto container = device->OpenContainer("SaveSlot1");
// Get the full filesystem path to write files into
std::string savePath = container->getPath();
// e.g. ~/.local/share/MyGame/SaveSlot1/
Files inside the container are regular files — you can read and write them with standard C++ I/O or any serialisation library.
Platform save paths
| Platform | Root save path |
|---|---|
| Linux | ~/.local/share/<GameName>/ |
| Windows | %APPDATA%\<GameName>\ |
| Android | /data/data/<package>/files/<GameName>/ |
| Web (Emscripten) | /persistent/<GameName>/ (IndexedDB-backed) |
| macOS | ~/Library/Application Support/<GameName>/ |
CNA sets the game name from Game::setTitle() or, if not set, falls back to the executable name. Paths are created automatically on first write.
StorageDevice::BeginOpenContainer
The XNA async pattern uses IAsyncResult. CNA implements this but also provides a synchronous shortcut for simpler use:
// Async (XNA-compatible):
IAsyncResult* ar = device->BeginOpenContainer("SaveSlot1", nullptr, nullptr);
// ... do other work ...
auto container = device->EndOpenContainer(ar);
// Synchronous shortcut (CNA extension — not in original XNA):
auto container = device->OpenContainer("SaveSlot1");
JSON serialization pattern
CNA does not bundle a JSON library, but C++23's standard library makes it easy to write lightweight JSON manually. For production use, bundle a header-only library such as nlohmann/json or rapidjson.
SaveData struct
// SaveData.hpp
#pragma once
#include <string>
#include <cstdint>
struct SaveData {
intcs level = 1;
intcs score = 0;
intcs lives = 3;
Single musicVolume = 0.8f;
bool sfxEnabled = true;
std::string playerName;
// Manual JSON serialise
std::string ToJson() const {
return "{\n"
" \"level\": " + std::to_string(level) + ",\n"
" \"score\": " + std::to_string(score) + ",\n"
" \"lives\": " + std::to_string(lives) + ",\n"
" \"musicVolume\": " + std::to_string(musicVolume) + ",\n"
" \"sfxEnabled\": " + (sfxEnabled ? "true" : "false") + ",\n"
" \"playerName\": \"" + playerName + "\"\n"
"}\n";
}
// Minimal JSON parse (replace with a real library in production)
static SaveData FromJson(const std::string& json);
};
Save function
#include <fstream>
#include <filesystem>
#include "Microsoft/Xna/Framework/Storage/StorageDevice.hpp"
#include "Microsoft/Xna/Framework/Storage/StorageContainer.hpp"
#include "SaveData.hpp"
bool SaveGame(StorageDevice* device, intcs slot, const SaveData& data) {
std::string containerName = "SaveSlot" + std::to_string(slot);
auto container = device->OpenContainer(containerName);
if (!container) return false;
std::string path = container->getPath() + "/save.json";
std::filesystem::create_directories(container->getPath());
std::ofstream out(path);
if (!out.is_open()) return false;
out << data.ToJson();
return true;
}
bool LoadGame(StorageDevice* device, intcs slot, SaveData& out) {
std::string containerName = "SaveSlot" + std::to_string(slot);
auto container = device->OpenContainer(containerName);
if (!container) return false;
std::string path = container->getPath() + "/save.json";
std::ifstream in(path);
if (!in.is_open()) return false;
std::string json((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
out = SaveData::FromJson(json);
return true;
}
Save slots
Multiple save slots are supported by using different container names:
// Save to slot 1
SaveGame(device_, 1, currentSave_);
// Load from slot 2
SaveData slot2;
if (LoadGame(device_, 2, slot2)) {
// Use slot2 data
}
// Check if a slot exists
bool SlotExists(StorageDevice* device, intcs slot) {
auto container = device->OpenContainer("SaveSlot" + std::to_string(slot));
if (!container) return false;
std::string path = container->getPath() + "/save.json";
return std::filesystem::exists(path);
}
Complete game integration
#include <memory>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Storage/StorageDevice.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
#include "SaveData.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Storage;
using namespace Microsoft::Xna::Framework::Input;
class SaveDemo final : public Game {
public:
SaveDemo() : graphics_(this) {}
protected:
void Initialize() override {
Game::Initialize();
// Obtain the default StorageDevice (synchronous on desktop)
device_ = StorageDevice::GetDefault();
}
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// Try to auto-load save slot 1 on startup
if (device_) {
SaveData loaded;
if (LoadGame(device_.get(), 1, loaded)) {
save_ = loaded;
}
}
}
void Update(GameTime& gt) override {
auto kb = Keyboard::GetState();
// F5 = save slot 1
if (kb.IsKeyDown(Keys::F5) && !prevKb_.IsKeyDown(Keys::F5)) {
if (device_) {
save_.score += 100; // increment for demo
SaveGame(device_.get(), 1, save_);
}
}
// F9 = load slot 1
if (kb.IsKeyDown(Keys::F9) && !prevKb_.IsKeyDown(Keys::F9)) {
if (device_) {
LoadGame(device_.get(), 1, save_);
}
}
prevKb_ = kb;
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::DarkGreen);
spriteBatch_->Begin();
// Draw save_.score, etc.
spriteBatch_->End();
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::shared_ptr<StorageDevice> device_;
SaveData save_;
KeyboardState prevKb_;
};
int main() { SaveDemo game; game.Run(); }
Web platform notes
On Emscripten, writes to /persistent/ must be followed by a call to EM_ASM(FS.syncfs(false, function(){})) to flush IndexedDB. CNA handles this automatically when a StorageContainer is closed (goes out of scope or is explicitly closed). Do not hold containers open across frames longer than necessary on Web targets.