Tutorial 19: Saving and Loading Data

Storage  ·  StorageDevice  ·  StorageContainer  ·  JSON

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

PlatformRoot 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.