MonoGame itself does not officially support web or WebAssembly targets (issue #8102, closed as "not planned"). To ship a MonoGame-style game to the browser, the community path is KNI — nkast/kni, a MonoGame-compatible fork with a BlazorGL web target. Your Game subclass, SpriteBatch, GraphicsDeviceManager, and input APIs work unchanged — you swap MonoGame.Framework.* package references for nkast.Xna.Framework.* and publish as a Blazor WebAssembly project.
KNI is actively maintained and used in the community by projects like SadConsole, FlatRedBall, and Apos.Gui.
Prerequisites
- .NET 8 SDK (.NET 9 works too)
- The
wasm-toolsworkload if you want AOT compilation:dotnet workload install wasm-tools
Build for web
Scaffold a new project with nkast's template:
dotnet new install nkast.Kni.Templates
dotnet new kni-blazor-gl -n MyGame
Then publish:
dotnet publish -c Release
The Blazor output lands in bin/Release/net8.0/publish/wwwroot/. Point upload_dir at that directory.
SDK integration
Wavedash injects Wavedash into the page before your game loads. Call it from C# via IJSRuntime. A small wavedash-bridge.js exposes thin wrappers on window, and Blazor invokes them:
// wwwroot/wavedash-bridge.js
(function () {
const Wavedash = window.Wavedash;
window.wavedashInit = function () {
Wavedash.init({ debug: true });
};
window.wavedashUpdateLoadProgress = function (value) {
Wavedash.updateLoadProgressZeroToOne(Math.max(0, Math.min(1, Number(value) || 0)));
};
})();
Reference it from wwwroot/index.html:
<script src="wavedash-bridge.js"></script>
Then call it from your Blazor component (typically Pages/Index.razor.cs) once the page renders:
protected override async void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
if (!firstRender) return;
await JsRuntime.InvokeVoidAsync("wavedashUpdateLoadProgress", 1.0);
await JsRuntime.InvokeVoidAsync("wavedashInit");
// hand control to the KNI render loop
await JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
}
To expose more SDK methods, add a handler to wavedash-bridge.js and invoke it with JsRuntime.InvokeVoidAsync("yourMethod", args...).
Load progress
KNI's Blazor template boots the game from TickDotNet inside the Razor page. By the time OnAfterRender(firstRender: true) fires, the runtime is live and the canvas is ready, so reporting 1.0 there is the simplest "first playable" signal. If you want intermediate progress, hook Blazor.start({ loadBootResource }) in index.html to observe resource downloads and call wavedashUpdateLoadProgress() as each one resolves.
wavedash.toml
game_id = "YOUR_GAME_ID_HERE"
upload_dir = "./bin/Release/net8.0/publish/wwwroot"
entrypoint = "index.html"
Notes
- AOT compilation dramatically speeds up startup. Add
<RunAOTCompilation>true</RunAOTCompilation>and<WasmStripILAfterAOT>false</WasmStripILAfterAOT>to your.csproj. First build takes several minutes; subsequent builds are fast. Leaving strip on causes MONO interpreter assertions at runtime. - Routing inside iframes: if
wavedash devserves your game from a subpath, add@page "/{*path:nonfile}"toPages/Index.razorand a[Parameter] public string Path { get; set; }to absorb the matched route. - Content Pipeline (
.mgcbfiles) requires Windows or Wine on macOS. For simple examples, skip it and useTexture2D.SetDataon a 1×1 pixel at runtime.
Other options
- FNA, the other XNA reimplementation, has a community-driven Emscripten path via FNA-WASM-Build — used by Celeste-WASM. Separate ecosystem from MonoGame proper.
- NativeAOT-LLVM is a theoretical alternative but only supported on Windows x64 and Linux x64 build hosts, and is not what MonoGame devs use in practice.