Tutorial 45: ContentManager and Asset Pipeline

CNA — C++ XNA 4.0 reimplementation

CNA's ContentManager provides the same Load<T> interface as XNA 4.0 but works with open file formats — PNG, OBJ, WAV, OGG, and JSON descriptors — rather than compiled XNB binary assets.

No XNB support. CNA does not read Microsoft's XNB binary content format. All assets are loaded from their native formats (PNG textures, OBJ/FBX models, WAV/OGG audio) described by lightweight JSON descriptor files.

ContentManager::Load<T> generic

#include "Microsoft/Xna/Framework/Content/ContentManager.hpp"
using namespace Microsoft::Xna::Framework::Content;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Audio;

// Inside Game::LoadContent():
auto& content = getContentProperty(); // returns ContentManager&
content.setRootDirectory("Content");  // relative to the executable

// Texture2D — loads PNG/JPG/BMP from Content/textures/player.png
auto* tex = content.Load<Texture2D>("textures/player");

// Model — loads via Content/models/ship.model.json
auto* model = content.Load<Model>("models/ship");

// SoundEffect — loads from Content/audio/explosion.wav
auto* sfx = content.Load<SoundEffect>("audio/explosion");

// Song (streaming) — loads from Content/music/theme.ogg
auto* song = content.Load<Song>("music/theme");

// SpriteFont — loads from Content/fonts/arial20.font.json
auto* font = content.Load<SpriteFont>("fonts/arial20");

Supported asset types

TypeFile extension(s)Notes
Texture2D.png .jpg .bmp .tgaLoaded via SDL3_image; all standard formats
Model.model.json + referenced OBJJSON descriptor points to geometry files
SoundEffect.wavLoaded into memory; use for short clips
Song.ogg .mp3Streaming; only one Song plays at a time
SpriteFont.font.json + .png atlasPre-rendered glyph atlas format
Effect.effect.jsonBuilt-in effects (BasicEffect etc.)

ContentManager::RootDirectory

// Default is empty string (alongside the executable)
content.setRootDirectory("Content");

// Nested managers per state (one content manager per game screen)
ContentManager levelContent(getServicesProperty(), "Content/Level1");

ContentManager::Unload()

Unload() disposes every asset loaded through that ContentManager instance. Call it when transitioning between game states to reclaim GPU and audio memory.

// On screen exit:
levelContent.Unload();
// All Texture2D, Model, SoundEffect, etc. loaded via levelContent are now freed.
// Do NOT use any pointers obtained from levelContent after this point.

JSON descriptor formats

.texture.json

When you need to specify texture settings beyond defaults:

// Content/textures/terrain.texture.json
{
    "file": "terrain.png",
    "generateMipmaps": true,
    "addressU": "Wrap",
    "addressV": "Wrap",
    "filter": "Linear"
}

.model.json

// Content/models/ship.model.json
{
    "meshes": [
        { "file": "ship_body.obj",   "material": "textures/hull" },
        { "file": "ship_engine.obj", "material": "textures/engine" }
    ]
}

.font.json

// Content/fonts/arial20.font.json
{
    "atlas":       "arial20.png",
    "size":        20,
    "lineSpacing": 24,
    "glyphs": [
        { "char": 32, "x": 0,  "y": 0,  "w": 6,  "h": 0, "xoff": 0, "yoff": 0, "advance": 6 },
        { "char": 65, "x": 10, "y": 0,  "w": 14, "h": 20,"xoff": 0, "yoff": 0, "advance": 14 }
    ]
}

.sound.json

// Content/audio/explosion.sound.json (optional — plain .wav works too)
{
    "file": "explosion.wav",
    "volume": 0.8,
    "pitch": 0.0,
    "pan": 0.0
}

Code example: load multiple assets, unload on state change

class MultiAssetGame final : public Game {
public:
    MultiAssetGame() : graphics_(this),
        menuContent_(nullptr),
        gameContent_(nullptr) {}

protected:
    void Initialize() override {
        Game::Initialize();
        state_ = GameState::Menu;
    }

    void LoadContent() override {
        // Menu assets use the global ContentManager
        auto& shared = getContentProperty();
        shared.setRootDirectory("Content");
        menuFont_     = shared.Load<SpriteFont>("fonts/title");
        menuBg_       = shared.Load<Texture2D>("ui/menu_bg");

        // Game-level assets in a separate manager so they can be unloaded
        gameContent_ = std::make_unique<ContentManager>(
            getServicesProperty(), "Content/Game");
        playerTex_   = gameContent_->Load<Texture2D>("player");
        enemyTex_    = gameContent_->Load<Texture2D>("enemy");
        shootSfx_    = gameContent_->Load<SoundEffect>("shoot");
        levelModel_  = gameContent_->Load<Model>("level01");
    }

    void Update(GameTime& gameTime) override {
        auto kb = Keyboard::GetState();
        if (state_ == GameState::Menu && kb.IsKeyDown(Keys::Enter)) {
            state_ = GameState::Playing;
        }
        if (state_ == GameState::Playing && kb.IsKeyDown(Keys::Escape)) {
            // Free all in-game assets; menu assets remain loaded
            gameContent_->Unload();
            playerTex_  = nullptr;
            enemyTex_   = nullptr;
            shootSfx_   = nullptr;
            levelModel_ = nullptr;
            state_ = GameState::Menu;
        }
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::Black);
        // ... draw based on state_ ...
        gd.Present();
    }

private:
    enum class GameState { Menu, Playing };
    GameState state_ = GameState::Menu;

    GraphicsDeviceManager                  graphics_;
    std::unique_ptr<ContentManager>        gameContent_;
    SpriteFont*  menuFont_    = nullptr;
    Texture2D*   menuBg_      = nullptr;
    Texture2D*   playerTex_   = nullptr;
    Texture2D*   enemyTex_    = nullptr;
    SoundEffect* shootSfx_    = nullptr;
    Model*       levelModel_  = nullptr;
};

Pointers returned by Load<T> are owned by the ContentManager. After calling Unload(), all pointers from that manager are dangling. Null them immediately as shown above.