Tutorial 95: Speedy Blupi: A Real-World CNA Port

CNA — C++ XNA 4.0 reimplementation

What is Speedy Blupi?

Speedy Blupi is an open-source action platformer originally developed for Windows and ported to Linux via MonoGame. It features a tile-based world, multiple game modes, animated sprites, and SDL-based audio. The game is part of the Blupi game series by EPSITEC SA and is available on GitHub.

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

  1. Replace using Microsoft.Xna.Framework; with using namespace Microsoft::Xna::Framework;
  2. Replace new SpriteBatch(GraphicsDevice) with std::make_unique<SpriteBatch>(getGraphicsDeviceProperty())
  3. Replace Content.Load<Texture2D>("name") with Texture2D("assets/name.png", gd)
  4. Replace all Song / MediaPlayer calls with SoundEffect (loop via SoundEffectInstance::setIsLooped(true))
  5. Replace List<T> with std::vector<T> or ListCS<T>
  6. Replace null with nullptr, string with std::string
  7. Replace (float)gameTime.ElapsedGameTime.TotalSeconds with static_cast<float>(gt.ElapsedGameTime.TotalSeconds())
  8. XACT AudioEngine / WaveBank / SoundBank now work directly in CNA (real .xgs/.xsb/.xwb parsing) — no replacement needed unless you prefer the simpler SoundEffect::FromFile API

API compatibility lessons

  • Rectangle.Intersects() works identically in CNA and MonoGame — no changes needed
  • Color constructor new Color(r,g,b,a) maps to Color(r,g,b,a) — identical
  • SpriteBatch.Draw overloads are matched 1:1 in CNA — no changes to draw calls
  • Vector2.Zero, Vector2.One are static members in CNA exactly as in XNA
  • MathHelper.Clamp, MathHelper.Lerp behave identically
  • GamePad.GetState(PlayerIndex.One) maps to GamePad::GetState(PlayerIndex::One) — identical semantics

Content pipeline adaptation

Speedy Blupi's XNB assets were converted by exporting PNG files from the original XNB textures using the MonoGame Content Builder, then placing them in an assets/ directory. The CNA port loads PNGs directly with Texture2D("assets/sprites.png", gd). No content pipeline tool is needed — CNA uses SDL3_image for PNG loading.

Performance results

The CNA port of Speedy Blupi achieves a stable 60 fps on an Intel integrated GPU (i5-10210U) under Linux, the same as the original MonoGame version on the same hardware. The C++ RAII model eliminated GC pauses that occasionally caused frame drops in the C# version on low-memory devices.

Key porting patterns from Speedy Blupi experience

// Pattern 1: Convert content loading
// C# MonoGame:
//   texture = Content.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(Content.Load<Song>("music/theme"));
//   MediaPlayer.IsRepeating = true;
// CNA C++:
auto music = SoundEffect::FromFile("assets/music/theme.ogg");
auto musicInst = music->CreateInstance();
musicInst->setIsLooped(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 Event<T>
// C# MonoGame:
//   public event Action<int> OnScoreChanged;
// CNA C++ with sharp-runtime:
sharp::Event<int> OnScoreChanged;