Tutorial 46: Custom ContentTypeReader
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
existingInstanceparameter 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
ContentManagerinstance maintains its own reader registry. If you use multiple managers (one per screen), register your readers on each.