- C# 99.7%
- Shell 0.3%
| .forgejo/workflows | ||
| .github | ||
| assets | ||
| docs | ||
| libs | ||
| samples | ||
| src | ||
| templates/Thunder.MonoGame.Avalonia.Templates | ||
| test | ||
| .gitignore | ||
| .gitmodules | ||
| CLAUDE.md | ||
| MonoGame.Framework.Avalonia.slnx | ||
| nuget-README.md | ||
| README.md | ||
| templates-nuget-README.md | ||
Thunder.MonoGame.Avalonia
A library that enables unmodified MonoGame games to run inside an Avalonia desktop application.
Get started
Option A — use the project template (recommended for new projects):
dotnet new install Thunder.MonoGame.Avalonia.Templates
dotnet new mgavalonia -n MyGame
cd MyGame && dotnet run
Option B — add to an existing Avalonia project:
dotnet add package Thunder.MonoGame.Avalonia
Then follow the Minimal Setup guide to wire up MonoGameControl in your layout.
Overview
Thunder.MonoGame.Avalonia uses the source compilation model: MonoGame's shared platform-agnostic source is compiled together with Avalonia-specific platform partials to produce a single Thunder.MonoGame.Avalonia.dll — a complete MonoGame runtime for Avalonia desktop.
This is not a wrapper. It is MonoGame, compiled with an Avalonia platform backend.
- Target platform: Desktop (Windows, macOS, Linux) via Avalonia 11
- Rendering: OpenGL via Avalonia's
OpenGlControlBase - Game loop: Avalonia render callbacks (
OnOpenGlRender) - UI overlays: Native Avalonia controls layered over the
MonoGameControl - Language: C# / .NET 10
Why Avalonia?
MonoGame + Blazor WebAssembly has a WebGL performance ceiling in .NET 10 that makes a 60fps game loop impractical. Avalonia targets desktop, uses native OpenGL, and provides first-class UI overlay support without any JS interop overhead.
Beyond that baseline, the Avalonia integration unlocks a class of features that are genuinely difficult to achieve with any other MonoGame platform target:
Native UI overlays without a separate process or render pass. Any Avalonia control — buttons, sliders, data-bound panels — composites directly on top of the game surface. No Dear ImGui, no separate overlay window, no render target tricks.
True multi-monitor support via layout. A MonoGameControl sized to one physical display (e.g. 1920×1080) can coexist in the same borderless window with SubViewGameControl instances covering adjacent displays. Each sub-viewport renders a different region of the game world — a map, an inspector, a secondary camera — using the same GL context and the same game tick. The game's main backbuffer is sized only to the primary display; secondary displays are additive, not a stretched copy.
Draggable, snappable sub-viewports. Because sub-viewports are Avalonia controls, players can drag them between monitors at runtime. Avalonia's Screens API exposes every connected display's bounds and DPI, making snap-to-screen-edge straightforward to implement in game code.
MVVM-compatible game binding. The Game instance binds to MonoGameControl via a standard AvaloniaProperty, so a ViewModel can own the game and XAML wires everything declaratively — no code-behind required.
See docs/how-to/09-multi-window.md for the full multi-monitor architecture guide.
Target Developer Experience
Step 1 — Add the package:
dotnet add package Thunder.MonoGame.Avalonia
Step 2 — Add the control to any Avalonia view:
Name callout: three names, one library.
- NuGet package:
Thunder.MonoGame.Avalonia— what you install- Assembly name:
MonoGame.Framework— what you reference inclr-namespace:declarations (mirrors MonoGame's own assembly name so existing MonoGameusingstatements compile without changes)- C# control namespace:
Thunder.MonoGame.Avalonia.Controls— the namespace ofMonoGameControl,SubViewGameControl, etc.
<Window xmlns="https://github.com/avaloniaui"
xmlns:mg="clr-namespace:Thunder.MonoGame.Avalonia.Controls;assembly=MonoGame.Framework">
<Grid>
<!-- MonoGame renders here. Two wiring patterns are available: -->
<!-- Pattern A: code-behind — create the game in code-behind and call SetGame() -->
<mg:MonoGameControl x:Name="GameControl" />
<!-- Pattern B: MVVM — bind the Game property directly from a ViewModel -->
<!-- <mg:MonoGameControl Game="{Binding Game}" /> -->
<!-- Normal Avalonia UI overlaid on top -->
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="16">
<Button Content="Pause" />
<ProgressBar Value="{Binding Health}" />
</StackPanel>
</Grid>
</Window>
Pattern A — code-behind (simple apps):
// In the Window's code-behind constructor:
var game = new MyGame();
GameControl.SetGame(game);
// SetGame() is shorthand for GameControl.Game = game.
// MonoGameControl calls game.Run() automatically on the first render frame — do not call it separately.
Pattern B — MVVM (data-bound apps):
// In the ViewModel:
public MyGame Game { get; } = new MyGame();
<!-- In the XAML: -->
<mg:MonoGameControl Game="{Binding Game}" />
Step 3 — Standard MonoGame game code, completely unchanged:
public class MyGame : Game
{
private GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch;
public MyGame()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
_spriteBatch.Begin();
// ... draw sprites
_spriteBatch.End();
}
}
Getting Started
-
Install the NuGet package:
dotnet add package Thunder.MonoGame.Avalonia -
Follow the Minimal Setup guide — it covers project flags, XAML namespace, wiring patterns, and timing requirements.
-
Explore the sample project which has three tiers of increasing complexity:
Tier What it demonstrates Minimal Bare minimum: GraphicsDeviceManager,SpriteBatch, keyboard/mouseOverlay Avalonia pause button over a MonoGame surface, MVVM binding Advanced Full demo: 3D, audio, gamepad, sub-viewports, multi-screen spanning -
Browse the how-to guides for specific tasks (input, content pipeline, audio, advanced graphics, multi-screen).
Architecture
libs/MonoGame/MonoGame.Framework/ ← pure submodule, zero modifications
├── *.cs ← shared platform-agnostic code (included)
└── Platform/ ← ALL excluded from our build
src/MonoGame.Framework.Avalonia/ ← our project
├── MonoGame.Framework.Avalonia.csproj
├── Platform/Avalonia/ ← Avalonia partial class implementations
├── Graphics/ ← OpenGL partial classes
├── Input/ ← Avalonia input event partial classes
└── Controls/MonoGameControl.axaml
The .csproj includes MonoGame's shared source via glob and excludes Platform/**. Our Platform/Avalonia/ directory provides the platform-specific partial class implementations — the same mechanism MonoGame uses for DesktopGL, WindowsDX, etc.
Comparison with the Blazor Implementation
| Concern | Blazor | Avalonia |
|---|---|---|
| Project SDK | Microsoft.NET.Sdk.Razor |
Microsoft.NET.Sdk |
| Target framework | net10.0 (WASM) |
net10.0 (desktop) |
| Rendering | WebGL via [JSImport] |
OpenGL via OpenGlControlBase |
| Game loop | requestAnimationFrame (JS) |
OpenGlControlBase.OnOpenGlRender |
| Input | DOM events via JS interop | Avalonia PointerPressed, KeyDown, etc. |
| Asset loading | HTTP fetch / sync XHR | File.Open / TitleContainer |
| UI overlays | Razor/Radzen components | Native Avalonia XAML controls |
| JS interop | Full Interop/ layer |
None |
Repository Structure
Thunder.MonoGame.Avalonia/
├── libs/
│ └── MonoGame/ ← git submodule (NEVER MODIFY)
├── samples/
│ └── MonoGame.Framework.Avalonia.Sample/ ← sample project
├── src/
│ ├── MonoGame.Framework.Avalonia/ ← the library
│ └── MonoGame.Framework.Avalonia.Tests/ ← tests
└── docs/
├── how-to/ ← consumer how-to guides
└── monogame-avalonia/
├── 01-FEASIBILITY-DISCOVERY.md
└── milestones/
Building
dotnet build src/MonoGame.Framework.Avalonia/
dotnet test src/MonoGame.Framework.Avalonia.Tests/
The MonoGame submodule must always be clean:
git -C libs/MonoGame status --short # must be empty
Status
Active development. See docs/monogame-avalonia/ for milestone plans and implementation status.
License
MIT
Contact
James — https://code.thundersizzle.tech/james
Code Standards
C# Conventions
- System types over aliases:
Int32notint,Singlenotfloat,Stringnotstring - Nullable reference types enabled — annotate all nullable fields with
? - Implicit usings enabled
- Use
varonly when the type is obvious from the right-hand side
Naming
- Private fields:
_camelCase - Public properties/methods:
PascalCase - Local variables:
camelCase - Constants and enums:
PascalCase
Game Loop Patterns
// Timing — always use Single for deltaTime
Single deltaTime = (Single)gameTime.ElapsedGameTime.TotalSeconds;
// Disable fixed timestep — Avalonia's render callback controls timing
IsFixedTimeStep = false;
_graphics.SynchronizeWithVerticalRetrace = false;
// Asset loading — use TitleContainer (resolves to File.Open on desktop)
using var stream = TitleContainer.OpenStream("Content/sprite.png");
_texture = Texture2D.FromStream(GraphicsDevice, stream);
Avalonia Integration
- The game control is
MonoGameControl— an AvaloniaUserControlbacked byOpenGlControlBase OnOpenGlRender(GlInterface gl, Int32 fb)is both the render callback and the game loop tick- MonoGame must render into the framebuffer
fbprovided by Avalonia - GL state must be saved and restored around each render call to avoid corrupting Avalonia's compositor
- No JS interop — use Avalonia input events (
PointerPressed,KeyDown, etc.) directly - No Razor, no
wwwroot, nopackage.json, no TypeScript
CI / Build
- CI: Forgejo Actions (GitHub Actions-compatible syntax)
- Test runner: NUnit
- Build:
dotnet build,dotnet test - Source integrity tests (submodule clean check) must pass on every PR
References
- MonoGame docs: https://docs.monogame.net/
- Avalonia docs: https://docs.avaloniaui.net/
- Avalonia
OpenGlControlBase: https://docs.avaloniaui.net/docs/guides/graphics-and-animation/custom-rendering - XNA API reference: https://learn.microsoft.com/en-us/previous-versions/windows/xna/