Converting a Microsoft XNA 3.1 game to MonoGame : Andrew Lock

Converting a Microsoft XNA 3.1 game to MonoGame
by: Andrew Lock
blow post content copied from  Andrew Lock | .NET Escapades
click here to view original post


In this post I describe how I took a Microsoft XNA Framework 3.1 game, written over 15 years in 2009, and updated it to run using MonoGame, on .NET 8, in just a few hours. This is a follow on from my previous post in which I took a brief look at the history of XNA and MonoGame and the getting started story. This post continues on where that post left off, starting from the MonoGame sample app to describe the steps I took to port my XNA game to MonoGame.

Background: Porting a game from XNA to MonoGame

MonoGame was one of those technologies I had vaguely heard of, but never paid much attention to, and certainly didn't know any of the history or capabilities around it. It came to my attention a couple of weeks ago when James Montemagno and Frank Kruegar were talking about "vibe coding a MonoGame game from scratch" on an episode of Merge Conflict. When James mentioned that MonoGame was an implementation of XNA, it really piqued my interest.

I was interested because back in 2009, when I was trying to decide whether to work on a PhD or get a job, I dabbled with the idea of getting into game development. I really didn't pursue the gaming side at all, but I worked on a small game as a way of trying it out, and sure enough I used the XNA framework! So I wondered: how easy would it be to port the game I wrote all those years ago to use MonoGame instead?

As you'll see in this post, the answer is actually really quite easy. The main issues I ran into were due to the fact that the game I wrote targeted version 3.1 of the XNA framework (and .NET Framework 3.5!😮), whereas MonoGame is based on the 4.0 version of XNA.

The game I wrote was a clone of "Trash", which was a game I had for the Amiga 600 that I spent hours playing with my Dad when I was a kid:

Screenshot of Trash

Trash was itself a shameless clone of Nintendo's Dr. Mario, so when I was learning the ropes, it seemed like a good game to clone to get my bearings. The end result was definitely not polished (including crudely built models and sprites using a copy of 3DS Max of…questionable provenance), but it worked:

Screenshot of my Trash clone

So that's the history of my Trash clone. I still had the files knocking around, and after I heard about MonoGame, I decided to give myself an evening to see if I could port the game to MonoGame. Not to actually develop the app or to do anything more with it; just to see if I could update it to MonoGame. I decided my general approach would be:

  1. Get a basic MonoGame sample running locally.
  2. Replace the content of the MonoGame sample with my Trash source code.
  3. Fix any issues that arose.

I had no interest in distributing or improving the code. And I have to say, looking back at code I wrote 15 years ago was a horrifying experience 😂

Creating the basic MonoGame sample

As in my previous post, I started with the "MonoGame Cross-Platform Desktop Application" sample application and used the new slnx format for the solution file.

# Create the new sln file
dotnet new sln
# Convert the sln file to a slnx file
dotnet sln migrate
# Delete the original sln file
rm *.sln

Next I created a "MonoGame Cross-Platform Desktop Application" using the mgdesktopgl template:

# Create the template in a "Trash" subfolder
dotnet new mgdesktopgl --output Trash
# Add the new project to the template
dotnet sln add ./Trash/

If you run the app at this point, you'll get a Cornflower Blue "game":

The default MonoGame sample 'game'

This serves as the starting point for porting Trash.

Porting an existing XNA game to MonoGame

The MonoGame sample supports .NET 8 and targets the Microsoft XNA 4.0 API, but my game targeted XNA 3.1 and .NET Framework 3.5, as you can see from the (ugly, non-SDK) project file:

<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<XnaFrameworkVersion>v3.1</XnaFrameworkVersion>

As best as I can tell, the fact that my old app targets .NET Framework 3.5 really didn't factor in to the port much; the only issues I had were related to MonoGame using version 4.0 of the XNA API rather than 3.1.

For the remainder of this post I'll walk through the steps I took updating the app

1. Copying the files across

The first step was a basic one, I simply copied all the files across from the old project to the new one. That included both .cs files and all the content files like .wav sound files and .png sprite files. The only files I initially left out were:

  • The .csproj file. I used the MonoGame generated file instead.
  • The AssemblyInfo.cs file. The assembly attributes in this file such as [AssemblyTitle] are generated automatically by the default SDK instead.

At this point I optimistically tried to build and… not quite.

2. Fixing 3.1 to 4.0 conversion issues

After copying across the old files, I (amazingly) only had a single build error:

Error CS1061 : 'GraphicsDevice' does not contain a definition for 'RenderState'
and no accessible extension method 'RenderState' accepting a first argument of
type 'GraphicsDevice' could be found (are you missing a using directive or an
assembly reference?)

A very quick googleing of the error led to this SO post indicating that this was a breaking change in the move from XNA 3.1 to XNA 4.0: RenderState was removed in favour of RasterizerState, so to fix the build issue, I simply need to replace these calls. For example

// Before                                               👇
bool defaultUseScissorTest = spriteBatch.GraphicsDevice.RenderState.ScissorTestEnable;

// After                                                👇
bool defaultUseScissorTest = spriteBatch.GraphicsDevice.RasterizerState.ScissorTestEnable;

The fact that this was the only initial build error is a testament to Microsoft's general commitment to compatibility. But even more impressive in my eyes is how seamless MonoGame's support is for the XNA API. I didn't even have to rename any namespaces or anything, the code just compiled. Very neat.

MonoGame doesn't actually support all the APIs include in XNA 4.0. Most notably, the .Storage and .Net namespaces are not included. The storage and multi-platofrm APIs were deemed too tricky to port to be multiplatform in their original format.

So with those changes the game compiled. Time to try running it!

3. Fixing the content pipeline

Unfortunately, even though the game compiled, as soon as I ran it, it crashed with a FileNotFound exception:

System.IO.FileNotFoundException: Could not find file 'D:\repos\TrashMonoGame\artifacts\bin\Trash\debug\Content\Audio\Trash.xgs'.

When the app is initializing, it attempts to create an instance of AudioEngine, but the file it's looking for, Content\Audio\Trash.xgs doesn't exist. The .xgs format is a XACT Game Studio file that contains the details of the audio files. This file is normally generated by the XNA content pipeline, but as far as I can tell, MonoGame doesn't use the same process.

Rather than try to force MonoGame to use the AudioEngine, WaveBank, and SoundBank types that I was using originally, I decided it would make more sense to switch to the "typical" MonoGame approach to playing sound effects and music. In the simplest case this looks like the following:

SoundEffect soundEffect = Content.Load<SoundEffect>("path_to_sound.wav");
soundEffect.Play();

Or for looping effects:

SoundEffect effect = Content.Load<SoundEffect>("path_to_sound.wav");
SoundEffectInstance instance = effect.CreateInstance();
instance.IsLooped = true;
instance.Play();

and for music:

Song song = Content.Load<Song>("rock_loop_stereo.wav");
if(MediaPlayer.State != MediaState.Stopped)
{
    MediaPlayer.Stop();
}

MediaPlayer.Play(song);

In the example above, Content is an instance of ContentManager, which relies on the MonoGame content pipeline to load content. This in turn relies on the MonoGame Content Builder (MGCB) tools to process the assets and make them available to the game at runtime.

To run the MGCB content editor and to import all your assets, you can run the .NET tool that's installed as part of the manifest:

dotnet mgcb-editor

This runs the graphical content editor that you can use to add content to MonoGame. You can open the .mgcb file that's included in the default template, add your content (.wav files and sprite textures etc), and build the pipeline.

The MGCB Editor

After adding all of the content, the MGCB editor managed to process most of the content, however there was one file in particular that it couldn't process: a .xap file. This file is an XACT Audio Project, which defines how audio content is handled and processed, but the XactImporter and XactProcessor it requires is not supported in MonoGame yet.

Luckily, the .xap file is easily readable in a text editor, for example:

Cue
{
    Name = Navigate;

    Variation
    {
        Variation Type = 3;
        Variation Table Type = 1;
        New Variation on Loop = 0;
    }

    Sound Entry
    {
        Name = Navigate;
        Index = 1;
        Weight Min = 0;
        Weight Max = 255;
    }
}

Using this file as a basis, the MGCB editor to process the existing .wav files, and the SoundEffect and SoundEffectInstance types to load the sounds in the game, I replaced the previous sound handling with the updated MonoGame approach. With the sound loading issue fixed, I tried building and running the app again and…nope, still not quite there,

4. Adding a missing font

The next issue I ran into actually was a broken build after using the MGCB editor to add all my content files. When trying to build I would get the following error:

Narkisim Italic.spritefont: Error  : Processor 'FontDescriptionProcessor' had unexpected failure!. System.IO.FileNotFoundException: Could not find "Narkisim" font file.
   at Microsoft.Xna.Framework.Content.Pipeline.Processors.FontDescriptionProcessor.Process(FontDescription input, ContentProcessorContext context) in /home/runner/work/MonoGame/MonoGame/MonoGame.Framework.Content.Pipeline/Processors/FontDescriptionProcessor.cs:line 67
   at Microsoft.Xna.Framework.Content.Pipeline.ContentProcessor`2.Microsoft.Xna.Framework.Content.Pipeline.IContentProcessor.Process(Object input, ContentProcessorContext context) in /home/runner/work/MonoGame/MonoGame/MonoGame.Framework.Content.Pipeline/ContentProcessor.cs:line 60
   at MonoGame.Framework.Content.Pipeline.Builder.PipelineManager.ProcessContent(PipelineBuildEvent pipelineEvent) in /home/runner/work/MonoGame/MonoGame/MonoGame.Framework.Content.Pipeline/Builder/PipelineManager.cs:line 846
D:/repos/TrashMonoGame/Trash/Content/Fonts/Narkisim_16.spritefont

This shows that the MonoGame content builder couldn't process the Narkisim_16.spritefont content, because it can't find the Narkisim font that the spritefont is using. And sure enough, on my Windows 11 machine, the font isn't installed by default. As it turns out, that's because Narkisim is an optional font, that's installed as part of the Hebrew language pack.

No, I'm not exactly sure why I chose to use a Hebrew font😅 I'm guessing it's because it was the closest match I could fine to the original Trash font.

I could have chosen to use a different font, but instead I chose to simply install the missing font using Windows update. After installing Narkisim, the build finally worked!

Unfortunately, we're not out of the woods yet.

5. Fixing the RasterizerState behaviour

After installing the missing font, the game builds, and the splash screen even displays:

The Trash splash screen

However, if you press any key as it says, the app immediately crashes with an exception:

Unhandled exception. System.InvalidOperationException: You cannot modify a default rasterizer state object.
   at Microsoft.Xna.Framework.Graphics.RasterizerState.ThrowIfBound()
   at Microsoft.Xna.Framework.Graphics.RasterizerState.set_ScissorTestEnable(Boolean value)
   at Trash.ScrollingFont.DrawScissored(GameTime gameTime) in D:\repos\TrashMonoGame\Trash\ScrollingObject.cs:line 250
   at Trash.ScrollingFont.Draw(GameTime gameTime) in D:\repos\TrashMonoGame\Trash\ScrollingObject.cs:line 222
   at Trash.CreditsScreen.Draw(GameTime gameTime) in D:\repos\TrashMonoGame\Trash\CreditsScreen.cs:line 96
   at Microsoft.Xna.Framework.Game.SortingFilteringCollection`1.ForEachFilteredItem[TUserData](Action`2 action, TUserData userData)
   at Trash.TrashGame.Draw(GameTime gameTime) in D:\repos\TrashMonoGame\Trash\TrashGame.cs:line 105
   at Microsoft.Xna.Framework.Game.DoDraw(GameTime gameTime)
   at Microsoft.Xna.Framework.Game.Tick()
   at Microsoft.Xna.Framework.SdlGamePlatform.RunLoop()
   at Microsoft.Xna.Framework.Game.Run(GameRunBehavior runBehavior)
   at Program.<Main>$(String[] args) in D:\repos\TrashMonoGame\Trash\Program.cs:line 2

The mistake here is using code like this to enable "scissor testing":

spriteBatch.GraphicsDevice.RasterizerState.ScissorTestEnable = true;

Instead, you need to create a new RasterizerState object and set it on the GraphicsDevice:

spriteBatch.GraphicsDevice.RasterizerState = new () { ScissorTestEnable = true };

After making the change above, the game finally runs! But it doesn't quite run correctly

6. Fixing the scissor test

After the previous change, the app runs, but the "scrolling object" code that was setting ScissorTestEnable=true originally doesn't work as expected. The scrolling content is meant to be "constrained" by the pill bottle, but as you can see in the screenshot below, the scrolling text isn't constrained at all i.e. the scissor test isn't working.

The scrolling text is not constrained by the pill bottle

The mistake I made in the code was attempting to set RaterizerState after calling ResizedSpriteBatch.Begin(). Instead, you must pass the ResizedSpriteBatch object into the Begin() call:

// Pass the RaterizerState into the Begin() call
spriteBatch.Begin(rasterizerState: new RaterizerState { ScissorTestEnable = true });

And with that final change, the scissor test works as expected, and the game runs!

The scrolling text is correctly constrained and the game runs as expected

And there we have it, the conversion of an XNA 3.1 game to MonoGame in just a couple of hours!

As I said at the start of this post, the original Trash clone I wrote was just a proof of concept to try out the XNA framework, so it's very rough and ready. The graphics are clearly dubious at best, but the game contains all the gameplay elements of the original. I've put the code on GitHub, but I have no intention of developing it any further at this stage. I haven't even fixed the horrendous coding style I apparently had 15 years ago so don't judge me! 🙈

Summary

In this post I describe how I took a 15 year old, .NET Framework 3.5 game built using Microsoft's XNA 3.1 framework, and updated it to use MonoGame instead. In the previous post I described some of the history of MonoGame and explored the getting started template. By using that post as a starter, and copying in all my old files, I managed to convert my old codebase in just a few hours.

I ran into a few issues during the conversion, mostly stemming from the fact that MonoGame targets the XNA 4.0 API, but most of the issues were fixable relatively easily. The main issue was the lack of XactProcessor support in MonoGame, so I resorted to rewriting the sound processing to use SoundEffect instead. Ultimately this didn't require too many changes, thanks to existing abstractions in my code. Once all the issues were fixed, the game ran great!


June 10, 2025 at 02:30PM
Click here for more details...

=============================
The original post is available in Andrew Lock | .NET Escapades by Andrew Lock
this post has been published as it is through automation. Automation script brings all the top bloggers post under a single umbrella.
The purpose of this blog, Follow the top Salesforce bloggers and collect all blogs in a single place through automation.
============================

Salesforce