Migrating from MonoGame / XNA

Porting a C# XNA 4.0 or MonoGame game to CNA (C++23)

Implementation status: CNA implements 227 of the 245 public XNA 4.0 types. The structural mapping is mechanical — the same namespaces, the same class names. The work in a port is almost never the API; it is the content pipeline (no .xnb) and custom shaders (no .fx).

Overview

CNA is a C++23 reimplementation of Microsoft XNA 4.0, built on SDL3. Its API is intentionally faithful to XNA: the same namespace hierarchy, the same class names, the same method names. For a developer coming from MonoGame or XNA the conceptual model is identical — Game, SpriteBatch, ContentManager, GraphicsDevice, BasicEffect — all exist and behave as expected.

The differences are almost entirely syntactic: C++ does not have garbage collection, properties, delegates, or using-disposal blocks. Each of these maps cleanly to a C++ equivalent. Once you have internalised the mechanical substitutions in this guide, most XNA code ports line-by-line.

1. Namespace mapping

XNA uses dot-separated C# namespaces; CNA uses the identical names with :: separators. The using directive works the same way.

C# (XNA / MonoGame) C++ (CNA)
Microsoft.Xna.Framework Microsoft::Xna::Framework
Microsoft.Xna.Framework.Graphics Microsoft::Xna::Framework::Graphics
Microsoft.Xna.Framework.Input Microsoft::Xna::Framework::Input
Microsoft.Xna.Framework.Audio Microsoft::Xna::Framework::Audio
Microsoft.Xna.Framework.Content Microsoft::Xna::Framework::Content
using Microsoft.Xna.Framework; using namespace Microsoft::Xna::Framework;

2. C++ vs C# quick reference

The table below covers the most common syntactic substitutions you will encounter during a port.

Concept C# (XNA) C++ (CNA)
Property read game.IsActive game.getIsActive()
Property write graphics.PreferredBackBufferWidth = 800 graphics.setPreferredBackBufferWidth(800)
Inheritance class MyGame : Game class MyGame final : public Game
Object creation new SpriteBatch(gd) std::make_unique<SpriteBatch>(gd)
Null reference null nullptr
String type string std::string or String (sharp-runtime alias)
Boolean bool (true/false) bool (true/false) — same
Delegates / events EventHandler handler = ... Virtual method override or EventHandler<T>
using resource disposal using (var x = ...) {} RAII / std::unique_ptr<>
Abstract override override void Draw(...) void Draw(...) override
Integer types int, byte, short intcs, bytecs, shortcs (sharp-runtime aliases)
Float type float Single or float
Array T[] std::vector<T> or std::array<T, N>
Optional / nullable T? std::optional<T>
foreach foreach (var x in col) Range-for: for (auto& x : col)

3. Game class migration

The Game subclass is the entry point for any XNA game. The lifecycle methods — Initialize, LoadContent, Update, and Draw — exist identically in CNA. The only differences are C++ syntax: override comes after the signature, member variables are declared with their types (no implicit nullability), and resources are owned by std::unique_ptr rather than being garbage-collected.

C# (MonoGame) C++ (CNA)
public class MyGame : Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    Texture2D playerTexture;

    public MyGame()
    {
        graphics =
          new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void LoadContent()
    {
        spriteBatch =
          new SpriteBatch(GraphicsDevice);
        playerTexture =
          Content.Load<Texture2D>("player");
    }

    protected override void Update(
        GameTime gameTime)
    {
        if (GamePad.GetState(
              PlayerIndex.One).Buttons.Back
            == ButtonState.Pressed)
            Exit();
        base.Update(gameTime);
    }

    protected override void Draw(
        GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.Black);
        spriteBatch.Begin();
        spriteBatch.Draw(
            playerTexture,
            new Vector2(100, 100),
            Color.White);
        spriteBatch.End();
        base.Draw(gameTime);
    }
}
class MyGame final : public Game
{
    GraphicsDeviceManager graphics;
    std::unique_ptr<SpriteBatch> spriteBatch;
    std::shared_ptr<Texture2D> playerTexture;

public:
    MyGame()
        : graphics(this)
    {
        Content->setRootDirectory("Content");
    }

    void LoadContent() override
    {
        spriteBatch =
          std::make_unique<SpriteBatch>(
            GraphicsDevice);
        playerTexture =
          Content->Load<Texture2D>("player");
    }

    void Update(
        GameTime gameTime) override
    {
        if (GamePad::GetState(
              PlayerIndex::One)
            .getButtons().getBack()
            == ButtonState::Pressed)
            Exit();
        Game::Update(gameTime);
    }

    void Draw(
        GameTime gameTime) override
    {
        GraphicsDevice->Clear(Color::Black);
        spriteBatch->Begin();
        spriteBatch->Draw(
            playerTexture,
            Vector2(100, 100),
            Color::White);
        spriteBatch->End();
        Game::Draw(gameTime);
    }
};

4. Memory management

XNA relies on the .NET garbage collector: objects are allocated with new and reclaimed automatically. C++ has no garbage collector; you manage object lifetimes explicitly. CNA follows standard modern C++ ownership conventions:

  • std::unique_ptr<T> — single-owner resource. Use this for game components, renderers, and anything your Game class owns exclusively. The resource is released automatically when the owning pointer goes out of scope or is reset.
  • std::shared_ptr<T> — shared ownership. Use this for assets (textures, sounds, models) that multiple objects may reference simultaneously.
  • Stack allocation — use for temporaries such as Vector2, Color, Rectangle, and GameTime. These are small value types and incur no heap allocation.
  • ContentManager::Unload() — releases all assets currently held by the content manager. Call this when transitioning between scenes to free GPU and CPU memory.
  • Game components — add drawable/updatable components to the Components collection (identical to XNA), or hold them as std::unique_ptr members on your Game subclass.

The key rule: if you would write new Foo(...) in C#, write std::make_unique<Foo>(...) in C++ and store the result in a std::unique_ptr<Foo> member. The destructor will run automatically when your game object is destroyed.

5. Content pipeline

XNA and MonoGame use a pre-compilation pipeline that converts source assets into proprietary .xnb binary files, which are then loaded at runtime via ContentManager.Load<T>. CNA does not support the XNB format.

XNA / MonoGame CNA
Pre-compile assets with MGCB / XNA Content Pipeline Tool No build step required for most asset types
.xnb binary files at runtime Raw asset files (PNG, WAV, OGG) or JSON descriptor files
Content.Load<Texture2D>("player") loads player.xnb Content->Load<Texture2D>("player") loads player.png directly
Fonts: .spritefont + MGCB Fonts: .font.json descriptor referencing a TTF file
Models: .fbx / .obj compiled to .xnb Models: .model.json descriptor referencing OBJ/FBX
Custom shaders: .fx HLSL compiled by pipeline Shaders: .shader.json referencing GLSL (EasyGL) or SPIR-V (Vulkan)

For a typical 2D game the migration is straightforward: place PNG and OGG files in your content directory and update Content->setRootDirectory() to point at it. The Load<T> call is identical.

6. Audio

CNA implements the main XNA audio API in the Microsoft::Xna::Framework::Audio namespace. The common classes work as expected:

  • SoundEffect — loads a WAV or OGG file and plays it as a fire-and-forget sound. API identical to XNA.
  • SoundEffectInstance — a controllable instance of a SoundEffect: pause, resume, loop, and volume control all work.
  • MediaPlayer / Song — background music playback. API identical to XNA.

The XACT subsystem (AudioEngine, SoundBank, WaveBank, Cue) is now genuinely implemented (~97% functional) — a real .xgs/.xsb/.xwb parser plays back through SDL3_mixer with category/instance-limit/fade handling and continuous RPC curves. Games that rely on XACT for audio can keep doing so; SoundEffect and MediaPlayer remain simpler alternatives for projects without existing XACT content.

7. Known gaps and limitations

Before porting, review these known gaps. The API surface is largely there, but the items below are absent, partial, or need hardware validation. See Verification & Known Issues for the complete current list.

  • No XNB content pipeline.xnb files are not supported. Replace with raw assets (PNG, WAV, OGG) and JSON descriptors.
  • GamerServices — a complete API port exists (Gamer, SignedInGamer, GamerProfile, leaderboards, Guide, achievements), with local/synthetic semantics rather than real Xbox Live binary compatibility — the same approach FNA itself uses for this namespace.
  • Touch input — the TouchPanel API and gesture pipeline (Tap, FreeDrag, Flick, Pinch...PinchComplete) are fully wired and byte-faithful to FNA's behavior (~98%).
  • macOS — not an official target. SDL3 supports macOS and CNA may compile there, but it is untested.
  • FillMode::WireFrame — available on the Vulkan backend only; not supported on EasyGL.

8. Code examples

Full Game class

The following pair shows a minimal but complete game loop: window setup, content loading, update, and draw.

C# (MonoGame) C++ (CNA)
public class MyGame : Game
{
    GraphicsDeviceManager _graphics;
    SpriteBatch _spriteBatch;
    Texture2D _background;

    public MyGame()
    {
        _graphics =
          new GraphicsDeviceManager(this);
        _graphics
          .PreferredBackBufferWidth = 1280;
        _graphics
          .PreferredBackBufferHeight = 720;
        Content.RootDirectory = "Content";
        IsMouseVisible = true;
    }

    protected override void LoadContent()
    {
        _spriteBatch =
          new SpriteBatch(GraphicsDevice);
        _background =
          Content.Load<Texture2D>("bg");
    }

    protected override void Update(
        GameTime gt)
    {
        if (Keyboard.GetState()
            .IsKeyDown(Keys.Escape))
            Exit();
        base.Update(gt);
    }

    protected override void Draw(
        GameTime gt)
    {
        GraphicsDevice.Clear(
            Color.CornflowerBlue);
        _spriteBatch.Begin();
        _spriteBatch.Draw(
            _background,
            Vector2.Zero,
            Color.White);
        _spriteBatch.End();
        base.Draw(gt);
    }
}
class MyGame final : public Game
{
    GraphicsDeviceManager _graphics;
    std::unique_ptr<SpriteBatch> _spriteBatch;
    std::shared_ptr<Texture2D> _background;

public:
    MyGame() : _graphics(this)
    {
        _graphics
          .setPreferredBackBufferWidth(1280);
        _graphics
          .setPreferredBackBufferHeight(720);
        Content->setRootDirectory("Content");
        setIsMouseVisible(true);
    }

    void LoadContent() override
    {
        _spriteBatch =
          std::make_unique<SpriteBatch>(
            GraphicsDevice);
        _background =
          Content->Load<Texture2D>("bg");
    }

    void Update(GameTime gt) override
    {
        if (Keyboard::GetState()
            .IsKeyDown(Keys::Escape))
            Exit();
        Game::Update(gt);
    }

    void Draw(GameTime gt) override
    {
        GraphicsDevice->Clear(
            Color::CornflowerBlue);
        _spriteBatch->Begin();
        _spriteBatch->Draw(
            _background,
            Vector2::Zero,
            Color::White);
        _spriteBatch->End();
        Game::Draw(gt);
    }
};

SpriteBatch draw

C# (MonoGame) C++ (CNA)
spriteBatch.Begin(
    SpriteSortMode.Deferred,
    BlendState.AlphaBlend);

spriteBatch.Draw(
    texture,
    new Rectangle(10, 10, 64, 64),
    Color.White);

spriteBatch.DrawString(
    font,
    "Hello, World!",
    new Vector2(200, 100),
    Color.Yellow);

spriteBatch.End();
spriteBatch->Begin(
    SpriteSortMode::Deferred,
    BlendState::AlphaBlend);

spriteBatch->Draw(
    texture,
    Rectangle(10, 10, 64, 64),
    Color::White);

spriteBatch->DrawString(
    font,
    "Hello, World!",
    Vector2(200, 100),
    Color::Yellow);

spriteBatch->End();

ContentManager load

C# (MonoGame) C++ (CNA)
// Load a texture
Texture2D tex =
    Content.Load<Texture2D>("sprites/hero");

// Load a sound
SoundEffect boom =
    Content.Load<SoundEffect>("sfx/boom");

// Load a font
SpriteFont font =
    Content.Load<SpriteFont>("fonts/ui");

// Unload all assets
Content.Unload();
// Load a texture (PNG loaded directly)
auto tex =
    Content->Load<Texture2D>("sprites/hero");

// Load a sound (WAV or OGG)
auto boom =
    Content->Load<SoundEffect>("sfx/boom");

// Load a font (.font.json descriptor)
auto font =
    Content->Load<SpriteFont>("fonts/ui");

// Unload all assets
Content->Unload();