Tutorial 46: Custom ContentTypeReader

CNA — C++ XNA 4.0 reimplementation

The ContentManager can be extended to load arbitrary file types by registering a ContentTypeReader<T>. This mirrors the XNA pattern and lets you route game-specific data (levels, dialogue, item databases) through the same managed asset pipeline.

ServiceProvider note. ContentManager::ServiceProvider is partially implemented in CNA. The IGraphicsDeviceService service is wired and returned by getServicesProperty(). Third-party service injection (custom services) works at the C++ level but does not mirror the full XNA interface-based service locator.

ContentTypeReader<T> template base

#include "Microsoft/Xna/Framework/Content/ContentTypeReader.hpp"
#include "Microsoft/Xna/Framework/Content/ContentReader.hpp"

namespace Microsoft::Xna::Framework::Content {

    // Base template you specialise for your type T
    template<typename T>
    class ContentTypeReader {
    public:
        virtual ~ContentTypeReader() = default;

        // Override this to deserialise the asset from the reader stream
        virtual T Read(ContentReader& input, T existingInstance) = 0;
    };

} // namespace

ContentReader provides file access

// ContentReader wraps the open file stream and the ContentManager context
class ContentReader {
public:
    // Primitive reads
    int32_t     ReadInt32();
    float       ReadSingle();
    std::string ReadString();
    bool        ReadBoolean();

    // Read raw bytes into a buffer
    void ReadBytes(void* buffer, size_t count);

    // Access to the ContentManager for loading sub-assets
    ContentManager& getContentManager();

    // The full path to the asset file being read
    std::string getAssetName() const;
};

Example: LevelData struct and reader

The data type (LevelData.hpp)

// LevelData.hpp
#pragma once
#include <string>
#include <vector>
#include "Microsoft/Xna/Framework/Vector3.hpp"

struct EnemySpawn {
    Microsoft::Xna::Framework::Vector3 position;
    std::string                         type;
    int                                 health;
};

struct LevelData {
    std::string              name;
    std::string              skyboxTexture;
    int                      width  = 0;
    int                      height = 0;
    std::vector<EnemySpawn>  enemies;
};

The descriptor file (Content/levels/level01.level.json)

// Content/levels/level01.level.json
{
    "name":          "Ice Cavern",
    "skyboxTexture": "textures/skybox_ice",
    "width":         64,
    "height":        64,
    "enemies": [
        { "position": [10, 0,  5], "type": "Goblin", "health": 30 },
        { "position": [30, 0, 20], "type": "Troll",  "health": 80 }
    ]
}

The reader (LevelDataReader.hpp)

// LevelDataReader.hpp
#pragma once
#include "Microsoft/Xna/Framework/Content/ContentTypeReader.hpp"
#include "LevelData.hpp"
#include <nlohmann/json.hpp>  // or your preferred JSON library
#include <fstream>

class LevelDataReader
    : public Microsoft::Xna::Framework::Content::ContentTypeReader<LevelData>
{
public:
    LevelData Read(
        Microsoft::Xna::Framework::Content::ContentReader& input,
        LevelData /*existingInstance*/) override
    {
        // Open the JSON file directly via the asset path
        std::string path = input.getAssetName();
        std::ifstream f(path);
        if (!f.is_open())
            throw std::runtime_error("Cannot open level: " + path);

        nlohmann::json j;
        f >> j;

        LevelData data;
        data.name          = j.at("name").get<std::string>();
        data.skyboxTexture = j.at("skyboxTexture").get<std::string>();
        data.width         = j.at("width").get<int>();
        data.height        = j.at("height").get<int>();

        for (const auto& e : j.at("enemies")) {
            EnemySpawn spawn;
            auto pos          = e.at("position");
            spawn.position    = { pos[0], pos[1], pos[2] };
            spawn.type        = e.at("type").get<std::string>();
            spawn.health      = e.at("health").get<int>();
            data.enemies.push_back(spawn);
        }
        return data;
    }
};

Registering the reader

// In your Game class, before calling Load<LevelData>:
void RegisterCustomReaders() {
    auto& manager = getContentProperty();
    // Register the reader so ContentManager knows how to handle LevelData
    manager.RegisterTypeReader<LevelData>(
        std::make_unique<LevelDataReader>());
}

Loading the custom asset

class MyGame final : public Game {
protected:
    void LoadContent() override {
        auto& content = getContentProperty();
        content.setRootDirectory("Content");

        // Register before loading
        content.RegisterTypeReader<LevelData>(
            std::make_unique<LevelDataReader>());

        // Load just like any built-in type
        LevelData* level = content.Load<LevelData>("levels/level01");

        for (const auto& enemy : level->enemies) {
            spawnEnemy(enemy.type, enemy.position, enemy.health);
        }
    }

    void spawnEnemy(const std::string& type,
                    const Vector3& pos, int hp) {
        // instantiate enemy based on type string
    }
};

Tips

  • The existingInstance parameter mirrors XNA's reader contract. For reference types it can be used to update an existing object; for value types (structs) just return a new one.
  • Custom readers can call input.getContentManager().Load<Texture2D>(subAssetPath) to load sub-assets recursively.
  • JSON is not mandated — a reader can parse binary, CSV, XML, or any other format.
  • Each ContentManager instance maintains its own reader registry. If you use multiple managers (one per screen), register your readers on each.