#Unity

Unity's WebGL build works on the arcade, with two export settings you must change from their defaults. Read shipping an engine export first for the rules every engine shares.

#Build settings

File → Build Settings → WebGL, then in Player Settings → Publishing Settings:

Setting Value Why
Compression Format Disabled The deploy sets no Content-Encoding, so Brotli/Gzip files arrive undecodable
Decompression Fallback on, if you keep compression Makes Unity decompress in JS instead of relying on server headers
Enable Exceptions None or Explicitly Thrown Full exception support roughly doubles the build

Leave threads off. WebAssembly threads need cross-origin isolation, which the arcade doesn't provide.

Build into games/<your-slug>/public/, so you get public/index.html plus public/Build/. Then:

"build": { "command": null, "outputDir": "public" }

Commit the whole Build/ folder. It's large; see the note on sizes.

#Calling the arcade from C#

Unity needs a .jslib shim: C# can't reach window directly, but it can call into functions you register on Unity's JS library.

// Assets/Plugins/Arcade.jslib
mergeInto(LibraryManager.library, {
  ArcadeInit: function () {
    if (typeof Arcade !== "undefined") Arcade.init();
  },
  ArcadeSubmitScore: function (score) {
    if (typeof Arcade === "undefined") return;
    // Catch: the SDK rejects when signed out or offline, and an unhandled
    // rejection is just noise in the player's console.
    Arcade.leaderboard.submit(score).catch(function () {});
  },
  ArcadeUnlock: function (idPtr) {
    if (typeof Arcade === "undefined") return;
    Arcade.achievements.unlock(UTF8ToString(idPtr)).catch(function () {});
  },
  ArcadeGuard: function (onFlag, reasonPtr) {
    if (typeof Arcade === "undefined") return;
    var reason = UTF8ToString(reasonPtr);
    Arcade.nav.guard(onFlag ? (reason || true) : false);
  },
});
// Assets/Scripts/ArcadeSDK.cs
using System.Runtime.InteropServices;
using UnityEngine;

public static class ArcadeSDK {
#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")] private static extern void ArcadeInit();
    [DllImport("__Internal")] private static extern void ArcadeSubmitScore(int score);
    [DllImport("__Internal")] private static extern void ArcadeUnlock(string id);
    [DllImport("__Internal")] private static extern void ArcadeGuard(bool on, string reason);

    public static void Init() => ArcadeInit();
    public static void SubmitScore(int score) => ArcadeSubmitScore(score);
    public static void Unlock(string id) => ArcadeUnlock(id);
    public static void Guard(bool on, string reason = "") => ArcadeGuard(on, reason);
#else
    // The editor has no browser, so these are no-ops. Without this the game
    // can't run in the editor at all.
    public static void Init() { }
    public static void SubmitScore(int score) => Debug.Log($"[arcade] score {score}");
    public static void Unlock(string id) => Debug.Log($"[arcade] unlock {id}");
    public static void Guard(bool on, string reason = "") { }
#endif
}

Then:

void Start() => ArcadeSDK.Init();
void GameOver() {
    ArcadeSDK.SubmitScore(score);
    ArcadeSDK.Unlock("first-run");
}

UTF8ToString(ptr) is required: strings arrive from C# as pointers into the heap, and using the pointer directly gives you a number.

#Getting a value back

.jslib functions are synchronous and can't return a promise, so pass results back through SendMessage:

ArcadeFetchTop: function () {
  if (typeof Arcade === "undefined") return;
  Arcade.leaderboard.top(10)
    .then(function (rows) {
      // "Leaderboard" is the GameObject name, "OnTopScores" a public method.
      unityInstance.SendMessage("Leaderboard", "OnTopScores", JSON.stringify(rows));
    })
    .catch(function () {});
},

Unity's default template stores the instance as unityInstance; confirm the name in your index.html if you use a custom template, since it's a template variable rather than a guarantee.

#Leave room for the top bar

The simplest answer for a full-viewport export is to collapse the bar. Add to game.json:

"navBar": "collapsed"

It then sits off-screen behind a small handle the player can pull down, and --arcade-nav-h becomes the handle's height rather than the bar's.

If you'd rather keep it expanded: the arcade already reserves the bar's strip as padding-top on <body>, so a template that lays out in normal flow — Unity's default centred canvas included — clears it with no work from you.

A full-window template is the exception, because a height of 100%/100vh ignores that padding and runs under the bar. In a custom WebGL template, subtract the strip instead:

#unity-container { height: calc(100vh - var(--arcade-nav-h, 0px)); }

The 0px fallback matters: outside the arcade there is no bar, and the container should fill the window.

#Common problems

What you see Cause
Loads in the editor, blank when deployed Compression on with no decompression fallback
"Unable to parse Build/…" Same: the browser got compressed bytes it couldn't decode
Arcade is not defined Guard every .jslib function; the SDK may not have loaded yet in a game's own dev server
A number instead of your string Missing UTF8ToString

The arcade side of this is verified: the injected bar, window.Arcade, and .wasm serving are all exercised against an export-shaped game. The .jslib and C# above follow Unity's documented interop but aren't run by this repo's CI, since CI has no engine installed. Check it on your first build and please correct this page if anything differs.

See also: shipping an engine export · the arcade nav bar · SDK reference

View docs/engines/unity.md on GitHub ↗