No description
  • C# 99.7%
  • Shell 0.3%
Find a file
2026-04-13 11:01:23 -05:00
.forgejo/workflows Move sample projects from src/ to samples/ 2026-04-13 09:47:38 -05:00
.github refactor(M8.1-S0): extract GlTestHelper + EglHeadlessContext to TestHelpers project 2026-04-01 21:50:29 -05:00
assets M14 planning: update milestone doc + icon assets 2026-04-10 07:35:43 -05:00
docs docs: post-project assessment 2026-04-10 10:30:40 -05:00
libs Fixed submodules 2026-03-13 20:45:40 -05:00
samples Move sample projects from src/ to samples/ 2026-04-13 09:47:38 -05:00
src fix: update build path for sample project in tests 2026-04-13 10:08:44 -05:00
templates/Thunder.MonoGame.Avalonia.Templates fix: update PackageProjectUrl in project files 2026-04-13 09:53:42 -05:00
test [TDD] GREEN: Project structure validated and builds 2026-03-14 06:48:37 -05:00
.gitignore Add .git-BACKUP to .gitignore 2026-03-13 20:29:20 -05:00
.gitmodules Removed non-library code (e.g. game code, prototype references, etc). I'm splitting up MonoGame.Platforms.Blazor from my actual game code now that the Platform code is well-established and can technically run my game code (even if it has a jitter issue still). 2026-03-25 10:26:32 -05:00
CLAUDE.md docs(claude): surface decisions during planning, not deferred into implementation 2026-03-31 14:55:18 -05:00
MonoGame.Framework.Avalonia.slnx Move sample projects from src/ to samples/ 2026-04-13 09:47:38 -05:00
nuget-README.md Fix nuget README: add template install step and link to how-to guide 2026-04-13 10:12:28 -05:00
README.md Update README.md 2026-04-13 11:01:23 -05:00
templates-nuget-README.md Add NuGet badges and improve first-visit experience in READMEs 2026-04-13 10:16:30 -05:00

Thunder.MonoGame.Avalonia

NuGet NuGet

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 in clr-namespace: declarations (mirrors MonoGame's own assembly name so existing MonoGame using statements compile without changes)
  • C# control namespace: Thunder.MonoGame.Avalonia.Controls — the namespace of MonoGameControl, 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

  1. Install the NuGet package:

    dotnet add package Thunder.MonoGame.Avalonia
    
  2. Follow the Minimal Setup guide — it covers project flags, XAML namespace, wiring patterns, and timing requirements.

  3. Explore the sample project which has three tiers of increasing complexity:

    Tier What it demonstrates
    Minimal Bare minimum: GraphicsDeviceManager, SpriteBatch, keyboard/mouse
    Overlay Avalonia pause button over a MonoGame surface, MVVM binding
    Advanced Full demo: 3D, audio, gamepad, sub-viewports, multi-screen spanning
  4. 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: Int32 not int, Single not float, String not string
  • Nullable reference types enabled — annotate all nullable fields with ?
  • Implicit usings enabled
  • Use var only 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 Avalonia UserControl backed by OpenGlControlBase
  • OnOpenGlRender(GlInterface gl, Int32 fb) is both the render callback and the game loop tick
  • MonoGame must render into the framebuffer fb provided 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, no package.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