Tutorial 95: Speedy Blupi: A Real-World CNA Port
What you’ll learn
- What a real MonoGame codebase looked like before the port.
- The porting process, and which API differences actually caused work.
- How the content was adapted, and the patterns that recurred.
Before you start — Tutorial 83: Migrating from MonoGame/FNA to CNA — this is the same migration, carried out on a real game rather than in the abstract.
How to read this tutorial. This is a worked walkthrough of porting a typical 2D MonoGame platformer to CNA, not an audited case study. The translation patterns below are the ones that recur in any port and are worth learning; treat any specific result as illustrative rather than measured. For validation work that is mechanically checked, see the XNA oracle corpus and the FNA differential harness described on the Showcase page.
The starting point
Speedy Blupi is an open-source action platformer with a tile-based world, multiple game modes, animated sprites and SDL-based audio. It is part of the Blupi game series by EPSITEC SA. It stands in here for the general shape of a 2D MonoGame game: SpriteBatch throughout, content loaded through ContentManager, and a custom tilemap renderer.
Original MonoGame codebase
The original Speedy Blupi C# MonoGame codebase used: SpriteBatch for all 2D rendering, ContentManager.Load<Texture2D> for XNB assets, Song and MediaPlayer for background music, SoundEffect for game audio, GamePad and Keyboard input, and a custom tilemap renderer built on SpriteBatch.Draw calls.
Porting process to CNA
- Replace
using Microsoft.Xna.Framework;withusing namespace Microsoft::Xna::Framework; - Replace
new SpriteBatch(GraphicsDevice)withstd::make_unique<SpriteBatch>(getGraphicsDeviceProperty()) getContentProperty().Load<Texture2D>("name")can stay as-is — CNA'sContentManagerreads.xnband falls back to loose files. If you would rather drop the pipeline, construct directly:Texture2D("assets/name.png", gd)SongandMediaPlayerneed no replacement —MediaPlayergenuinely plays songs via SDL3_mixer. Swapping to a loopingSoundEffectInstanceis an option, not a requirement- Replace
List<T>withstd::vector<T>or sharp-runtime'sSystem::Collections::Generic::List<T>(whose count isgetCountProperty(), notCount) - Replace
nullwithnullptr,stringwithstd::string - Replace
(float)gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty()withstatic_cast<float>(gt.getElapsedGameTimeProperty().getTotalSecondsProperty()) - XACT
AudioEngine/WaveBank/SoundBanknow work directly in CNA (real.xgs/.xsb/.xwbparsing) — no replacement needed unless you prefer the simplerSoundEffectconstructor API
API compatibility lessons
Rectangle.Intersects()works identically in CNA and MonoGame — no changes neededColorconstructornew Color(r,g,b,a)maps toColor(r,g,b,a)— identicalSpriteBatch.Drawoverloads are matched 1:1 in CNA — no changes to draw callsVector2.Zero,Vector2.Oneare static members in CNA exactly as in XNAMathHelper.Clamp,MathHelper.Lerpbehave identicallyGamePad.GetState(PlayerIndex.One)maps toGamePad::GetState(PlayerIndex::One)— identical semantics
Content adaptation
You have two routes, and they can be mixed asset by asset.
Keep compatible .xnb files. CNA reads them through 50 built-in readers with FFmpeg (49 without video). Call CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() once at startup. There is no reflection-based discovery, but custom ContentTypeReader<T> creators can be registered explicitly. Compiled effects additionally require a capable renderer build.
Or export to loose files. Export PNGs from the original XNB textures with the MonoGame Content Builder into an assets/ directory and construct textures directly. CNA loads PNG through SDL3_image, and ContentManager falls back to loose files when no .xnb is present.
Custom shaders still need a deliberate portability decision. Alpha.1 can load XNA/FNA D3D9 Effect Framework binaries on FNA3D and explicitly enabled SDL_GPU, EasyGL-family or Vulkan builds, but it does not compile HLSL .fx source or automatically convert DXBC/MGFX. Effects outside that compatible binary path must use the active renderer's ShaderEffect source or binary format. A purely 2D SpriteBatch game like this one is usually unaffected.
Key porting patterns
// Pattern 1: Convert content loading
// C# MonoGame:
// texture = getContentProperty().Load<Texture2D>("Blupi/blupi");
// CNA C++:
// texture_ = std::make_unique<Texture2D>("assets/Blupi/blupi.png", gd);
// Pattern 2: Convert Song/MediaPlayer to looping SoundEffectInstance
// C# MonoGame:
// MediaPlayer.Play(getContentProperty().Load<Song>("music/theme"));
// MediaPlayer.IsRepeating = true;
// CNA C++:
SoundEffect music("assets/music/theme.ogg");
auto musicInst = music->CreateInstance();
musicInst->setIsLoopedProperty(true);
musicInst->Play();
// Pattern 3: Convert string interpolation to std::string
// C# MonoGame:
// string path = $"sprites/{name}.png";
// CNA C++:
std::string path = "assets/sprites/" + name + ".png";
// Pattern 4: Convert foreach to range-for
// C# MonoGame:
// foreach (var tile in tiles) tile.Draw(spriteBatch);
// CNA C++:
for (auto& tile : tiles_) tile.Draw(*spriteBatch_);
// Pattern 5: Convert nullable references to std::optional
// C# MonoGame:
// Texture2D? overrideTexture = null;
// CNA C++:
std::optional<Texture2D*> overrideTexture_ = std::nullopt;
// Pattern 6: Convert C# events to sharp-runtime System::EventHandler<TEventArgs>
// C# MonoGame:
// public event Action<int> OnScoreChanged;
// CNA C++ with sharp-runtime: the payload travels in an EventArgs subclass.
struct ScoreChangedEventArgs : System::EventArgs {
SharpRuntime::intcs NewScore = 0;
};
System::EventHandler<ScoreChangedEventArgs> OnScoreChanged;
// Subscribe with += (the handler takes sender + args):
OnScoreChanged += [](System::Object* sender, const ScoreChangedEventArgs& e) {
// react to e.NewScore
};
// Raise it (Invoke() is an alias for Raise()):
ScoreChangedEventArgs args;
args.NewScore = score_;
OnScoreChanged.Raise(this, args);