Search documentation

Find pages, sections, and content across all docs.

WavedashDocs

C#

Run C# games in the browser via .NET's managed WebAssembly runtime.

View example project on GitHub Playtest the example project

C# games target the browser through .NET's managed WebAssembly runtime using the [JSImport]/[JSExport] interop pattern. The output is a _framework/ directory plus your game.js and index.html.

If you're using Unity, see the Unity guide instead — it has its own SDK package and build pipeline. If you're using MonoGame, see the MonoGame guide.

Setup

You need the .NET 9+ SDK with the WebAssembly build tools workload:

dotnet workload install wasm-tools        # .NET 9 SDK
dotnet workload install wasm-tools-net9   # .NET 10 SDK building a net9.0 project

Your .csproj should target browser-wasm:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <OutputType>Exe</OutputType>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <InvariantGlobalization>true</InvariantGlobalization>
    <AssemblyName>Pong</AssemblyName>          <!-- matches getAssemblyExports("Pong.dll") below -->
    <WasmMainJSPath>main.js</WasmMainJSPath>   <!-- src/main.js; can be empty, game.js drives startup -->
  </PropertyGroup>
</Project>

SDK integration

Wavedash injects Wavedash into the page before your .NET runtime loads. Use [JSImport] on partial methods to call the SDK, and [JSExport] so game.js can call into your C#:

using System.Runtime.InteropServices.JavaScript;

public static partial class Interop
{
    [JSImport("globalThis.wavedashInit")]
    public static partial void WavedashInit();

    [JSImport("globalThis.wavedashProgress")]
    public static partial void WavedashProgress(double p);

    // Called from game.js once the runtime is up.
    [JSExport]
    public static void WdInit(int width, int height)
    {
        // ... game setup ...
        WavedashProgress(1.0);
        WavedashInit();
    }
}

WdInit fills the progress bar with WavedashProgress(1.0), then calls WavedashInit() last, which signals load completion. Call WavedashProgress(...) with intermediate values during async asset loading.

The web/game.js entrypoint defines the globalThis bridge functions and boots the .NET runtime. It uses top-level await, so load it with <script type="module" src="./game.js">:

const Wavedash = window.Wavedash;

globalThis.wavedashInit = function () {
  Wavedash.init({ debug: true });
};

globalThis.wavedashProgress = function (p) {
  Wavedash.updateLoadProgressZeroToOne(p);
};

import { dotnet } from "./_framework/dotnet.js";

const { getAssemblyExports } = await dotnet.create();
const exports = await getAssemblyExports("Pong.dll");

exports.Interop.WdInit(window.innerWidth, window.innerHeight);

Build script

#!/usr/bin/env sh
set -eu
mkdir -p build

dotnet publish src/Pong.csproj -c Release

cp -R src/bin/Release/net9.0/browser-wasm/AppBundle/_framework build/_framework
cp web/index.html build/index.html
cp web/game.js    build/game.js

wavedash.toml

game_id = "YOUR_GAME_ID_HERE"
upload_dir = "./build"

Other SDK features

Once initialized, Wavedash also exposes leaderboards, achievements, stats, and user data. Add more [JSImport] bindings or call them from game.js:

[JSImport("globalThis.wavedashSubmitScore")]
public static partial void SubmitScore(string id, int score, bool keepBest);

[JSImport("globalThis.wavedashSetAchievement")]
public static partial void SetAchievement(string id);

With corresponding globalThis implementations in game.js that call Wavedash.uploadLeaderboardScore(...) and Wavedash.setAchievement(id, true). See the SDK reference for the full API.