Tutorial 83: Migrating from MonoGame/FNA to CNA
What you’ll learn
- The namespace and type mappings from C# MonoGame to C++ CNA.
- What changes about memory management once there is no GC.
- Content loading and audio differences.
- Which APIs have no CNA equivalent yet.
Before you start — Tutorial 01: Introduction to CNA — it covers the C++-versus-C# differences this builds on. Prior MonoGame or FNA experience is assumed; no earlier tutorial is strictly required.
Why migrate?
CNA offers C++23 performance, no garbage collector pauses, and the same XNA 4.0 API surface you already
know from MonoGame or FNA. Games ported from MonoGame/FNA keep their architecture intact — the
Game loop, SpriteBatch calls, Effect hierarchy, and math types
are all preserved. Only the language changes from C# to C++.
Namespace changes
CNA uses the same Microsoft::Xna::Framework namespace hierarchy as MonoGame, expressed in
C++ :: notation instead of C# . notation.
| C# Namespace | C++ Namespace |
|---|---|
Microsoft.Xna.Framework | Microsoft::Xna::Framework |
Microsoft.Xna.Framework.Graphics | Microsoft::Xna::Framework::Graphics |
Microsoft.Xna.Framework.Audio | Microsoft::Xna::Framework::Audio |
Microsoft.Xna.Framework.Input | Microsoft::Xna::Framework::Input |
Microsoft.Xna.Framework.Media | Microsoft::Xna::Framework::Media |
C# to C++ type mapping
| C# Type | C++ CNA Type | Notes |
|---|---|---|
int | intcs (= int32_t) | sharp-runtime alias |
float | Single (= float) | or just use float |
double | double | same |
bool | bool | same |
byte | bytecs (= uint8_t) | sharp-runtime alias |
string | std::string | no implicit conversions |
List<T> | System::Collections::Generic::List<T> | from sharp-runtime, or std::vector<T>. Count is getCountProperty() |
null | nullptr | pointer null |
? nullable | std::optional<T> | or raw pointer |
Memory management
MonoGame relies on the .NET GC. In CNA you manage lifetimes explicitly. Use
std::unique_ptr<T> for owned resources (most game objects),
std::shared_ptr<T> when shared ownership is genuinely needed, and pass by reference
to avoid unnecessary copying. All CNA resource classes (Texture2D, VertexBuffer,
etc.) implement RAII — they release GPU resources in their destructor so you never need to call a
separate Dispose().
Content loading differences
MonoGame uses .xnb binary files produced by the XNA content pipeline. CNA reads
those too. It implements the XNB container format, LZX decompression, and 50 built-in
content type readers (49 when FFmpeg is absent, which drops VideoReader), so an
existing ContentManager::Load<T>() call over pre-built .xnb assets carries
across.
Note the direction: this is read-side XNB support, not a content pipeline. CNA has no
ContentCompiler, ContentImporter or ContentProcessor and never will
— authoring stays on the XNA/MonoGame/FNA side, or moves to CNA's own formats.
Readers are not auto-registered. Call RegisterAllBuiltInXnbReaders() once at
startup or every .xnb load fails. It currently lives in an Internal header, which is
a known ergonomics gap.
Compiled effects need a format and renderer check. Alpha.1's EffectReader accepts XNA/FNA
D3D9 Effect Framework binaries on FNA3D, or on SDL_GPU, EasyGL-family and Vulkan builds whose
renderer-specific compiled-effects option is enabled. It does not compile HLSL .fx source and
does not accept DXBC or MGFX. On other renderers, migrate the effect to ShaderEffect in that
renderer's native language. The stock effects remain a separate working path.
CNA additionally lets you skip .xnb entirely and load assets straight from source files,
which is usually simpler for a new project. Load<T> tries .xnb first, then
CNA's own .cnj JSON format, then the older loose-file .model.json:
- Textures:
Texture2D("assets/logo.png", gd)— PNG/JPG loaded via SDL3_image - Models:
Model::Load("assets/house.obj", gd)via the model loader — and, newer and more useful for a migration, runtime glTF 2.0: drop a.gltfor.glbin the content root andContent.Load<Model>("name")loads it with no tooling step, including skeletal animation and morph targets - Fonts: a JSON-based
SpriteFontdescriptor, as an alternative to.spritefontXML compiled to XNB
Audio (SoundEffect same, XACT now real)
SoundEffect and SoundEffectInstance APIs are identical to MonoGame.
SoundEffect("sound.wav") (the CNAEXT assetName constructor) loads OGG/WAV via SDL3_mixer. Song/
MediaPlayer streaming is also fully implemented via SDL3_mixer. XACT
(.xap-authored AudioEngine/SoundBank/WaveBank/Cue) is now a real,
largely functional runtime with a genuine .xgs/.xsb/.xwb parser — MonoGame
games that use XACT do not need to be rewritten to use SoundEffect directly, though it
remains the simpler option for one-off sounds.
Two audio gaps to check your assets against. A WaveBank containing XMA or WMA content does not
throw — it logs to stderr and returns nullptr, so the sound is simply missing. (The XNB
SoundEffectReader path does throw.) And .m4a/.aac are unplayable:
SDL3_mixer has no AAC decoder.
Missing APIs
A few MonoGame extensions are not present in CNA:
VideoPlayer— partial (see the Video Playback tutorial)
Two things that are not missing
Storage works. Microsoft::Xna::Framework::Storage is fully functional:
StorageDevice, StorageContainer, and
StorageDeviceNotConnectedException are all implemented. StorageContainer
gives you CreateFile(), OpenFile(), FileExists(),
DeleteFile(), GetFileNames(), and the matching directory operations, with
file access returning a std::unique_ptr<System::IO::Stream>. Save-game code
written against XNA's storage API does not need rewriting to raw C++ file I/O.
Message boxes work. CNA provides CNA::Devices::MessageBox, which pops a
real native modal dialog through SDL. You do not need to call SDL_ShowMessageBox
yourself:
#include "CNA/Devices/MessageBox.hpp"
#include "CNA/Devices/MessageBoxType.hpp"
using CNA::Devices::MessageBox;
using CNA::Devices::MessageBoxType;
// Simple notification.
MessageBox::ShowSimple(MessageBoxType::Error,
"Save failed",
"The save file could not be written.");
// Multi-button prompt: returns the index of the clicked button,
// or -1 if the dialog could not be shown.
int choice = MessageBox::Show(MessageBoxType::Warning,
"Unsaved changes",
"Quit without saving?",
{"Cancel", "Quit"});
MessageBoxType is Error, Warning, or
Information. Check MessageBox::getIsSupportedProperty() first if you target
a platform where a native dialog may not be available.
Side-by-side: C# MonoGame vs C++ CNA
// ===== C# MonoGame =====
// using Microsoft.Xna.Framework;
// using Microsoft.Xna.Framework.Graphics;
//
// public class Player {
// private Texture2D sprite;
// private Vector2 position;
//
// public Player(ContentManager content) {
// sprite = content.Load<Texture2D>("player");
// position = new Vector2(100, 200);
// }
//
// public void Update(GameTime gt) {
// float dt = (float)gt.getElapsedGameTimeProperty().getTotalSecondsProperty();
// position.X += 100.0f * dt;
// }
//
// public void Draw(SpriteBatch sb) {
// sb.Draw(sprite, position, Color.White);
// }
// }
// ===== C++ CNA equivalent =====
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
class Player {
public:
Player(GraphicsDevice& gd)
: sprite_("assets/player.png", gd) // direct load, no ContentManager
, position_(100.0f, 200.0f)
{}
void Update(GameTime& gt) {
float dt = static_cast<float>(gt.getElapsedGameTimeProperty().getTotalSecondsProperty());
position_.X += 100.0f * dt;
}
void Draw(SpriteBatch& sb) {
sb.Draw(sprite_, position_, Color::White);
}
private:
Texture2D sprite_; // owns GPU resource, released in destructor
Vector2 position_;
};