Skip to content

Docs / Engines

Unity

Build a Unity Web (WebGL) game for Fradiation Games with the Unity kit, or set it up by hand, and call the SDK from C#.

.md

A Unity web build is a wasm file with a canvas attached. Fradiation serves it as is, once it has the right compression, a template that fills the frame and relative paths.

The Unity kit sets all of that with one menu item. The kit is tested with Unity 6 (6000.x). The APIs it uses exist in Unity 2021.3 and later.

Quick path

  1. Download the Unity kit and unzip it into your project folder, so the kit's Assets folder merges with yours. The files land here:

    PathWhat it is
    Assets/Fradiation/Editor/FradiationBuild.csThe build menu item and the batch method.
    Assets/Fradiation/Runtime/Fradiation.csThe C# API.
    Assets/Plugins/WebGL/Fradiation.jslibThe bridge from C# to window.fradiation.
    Assets/WebGLTemplates/Fradiation/A WebGL template that fills the frame.
  2. Put fradiation.json in the project root, next to Assets/. See fradiation.json for every field.

    json
    {
      "$schema": "https://www.fradiation.games/fradiation.schema.json",
      "viewport": { "width": 1280, "height": 720 },
      "threads": false,
      "mutations": [
        { "key": "boss-down", "name": "Boss Down", "description": "Defeat the first boss.", "tier": "alpha", "icon": "skull" }
      ],
      "boards": [
        { "key": "fastest-clear", "name": "Fastest clear", "order": "asc", "format": "time" }
      ]
    }
  3. Build. In the editor, choose Fradiation → Build for Fradiation. From the command line, with the project closed in the editor:

    bash
    Unity -batchmode -quit -projectPath . -buildTarget WebGL -executeMethod FradiationBuild.Build

    Unity stands for the path to the editor executable of the version your project uses, for example "C:\Program Files\Unity\Hub\Editor\<version>\Editor\Unity.exe". The editor needs its Web (WebGL) build support module installed.

    • The build goes to Builds/Fradiation. Add -fradiationOut <folder> to change that.
    • fradiation.json from the project root is copied into the build if it exists.
    • The process exits with code 1 if the build fails.

    An agent driving the open editor through an editor-automation tool should call FradiationBuild.BuildDeferred() instead. It returns at once, builds on the next editor tick (so a request timeout can't cut the build off part-way), and writes Builds/Fradiation.status.json to poll: queued, building, then succeeded or failed, with the output path and any error.

  4. Check it, then upload. Run node check-build.mjs Builds/Fradiation (download the script), then upload the Builds/Fradiation folder or a zip of it on /dev/<slug>. See Packaging and Publish a game.

What the kit sets, and doing it by hand

If you skip the kit, set these yourself.

SettingValueWhere
Build targetWebUnity 6: File → Build Profiles → Web. Earlier versions: File → Build Settings → WebGL.
WebGL TemplateA template that fills the windowPlayer Settings → Resolution and Presentation
Compression FormatBrotli (or Gzip)Player Settings → Publishing Settings
Decompression FallbackOffPlayer Settings → Publishing Settings
Data CachingOnPlayer Settings → Publishing Settings
Name Files As HashesNot neededPlayer Settings → Publishing Settings

The same settings from an editor script, as the kit sets them:

csharp
PlayerSettings.WebGL.template = "PROJECT:Fradiation";
PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Brotli;
PlayerSettings.WebGL.decompressionFallback = false;
PlayerSettings.WebGL.dataCaching = true;

Compression

The uploader accepts pre-compressed files ending in .br or .gz, such as Fradiation.wasm.br. The play server sends them with the matching Content-Encoding, so the browser unpacks them itself. Brotli and Gzip builds both work.

Unity's own description of Compression Format says it doesn't affect development builds. A development build is uncompressed. Its files are allowed but larger.

Decompression Fallback

Leave it off. Unity describes the option as decompression code for hosts that can't set response headers to match the compression. Fradiation sets them, so the fallback adds loader code you don't use.

Data Caching

On. Unity keeps the data file in the browser's IndexedDB, which belongs to your game's own origin.

Name Files As Hashes

Not needed. Every build is served from its own URL, so file names don't have to change to defeat caches.

The template

Unity's default template gives the canvas a fixed size. On desktop it sets the width and height from Player Settings (960×600 by default), and it adds a footer with a logo and a fullscreen button. That doesn't fit the cabinet, which letterboxes to your viewport and expects the game to fill its window.

Create Assets/WebGLTemplates/Fradiation/index.html, copy fradiation-sdk.js into the same folder, and pick the template in Player Settings. Unity copies every file in the template folder into the build.

html
<!DOCTYPE html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>{{{ PRODUCT_NAME }}}</title>
    <style>
      html, body { margin: 0; height: 100%; background: #000; overflow: hidden; }
      #unity-canvas { display: block; width: 100%; height: 100%; }
      #loading { position: fixed; inset: 0; display: grid; place-items: center; color: #999; font: 12px monospace; }
    </style>
    <script src="fradiation-sdk.js"></script>
  </head>
  <body>
    <canvas id="unity-canvas" tabindex="-1"></canvas>
    <div id="loading">Loading</div>
    <script>
      var canvas = document.querySelector("#unity-canvas");
      var loading = document.querySelector("#loading");
      var config = {
        arguments: [],
        dataUrl: "Build/{{{ DATA_FILENAME }}}",
        frameworkUrl: "Build/{{{ FRAMEWORK_FILENAME }}}",
#if USE_THREADS
        workerUrl: "Build/{{{ WORKER_FILENAME }}}",
#endif
#if USE_WASM
        codeUrl: "Build/{{{ CODE_FILENAME }}}",
#endif
#if SYMBOLS_FILENAME
        symbolsUrl: "Build/{{{ SYMBOLS_FILENAME }}}",
#endif
        streamingAssetsUrl: "StreamingAssets",
        companyName: {{{ JSON.stringify(COMPANY_NAME) }}},
        productName: {{{ JSON.stringify(PRODUCT_NAME) }}},
        productVersion: {{{ JSON.stringify(PRODUCT_VERSION) }}},
      };
      var script = document.createElement("script");
      script.src = "Build/{{{ LOADER_FILENAME }}}";
      script.onload = function () {
        createUnityInstance(canvas, config, function (progress) {
          loading.textContent = "Loading " + Math.round(progress * 100) + "%";
        }).then(function () {
          loading.remove();
          canvas.focus();
          window.fradiation && window.fradiation.ready();
        }).catch(function (message) {
          loading.textContent = String(message);
        });
      };
      document.body.appendChild(script);
    </script>
  </body>
</html>

This does what the kit's template does, without the segmented loading bar. All URLs are relative. Set the viewport in fradiation.json to your game's aspect ratio.

Calling the SDK from C#

The kit's Fradiation class is static and fire-and-forget. It returns nothing, so a call can't tell you whether a mutation unlocked or what rank a score got. In the editor and on other platforms it only logs.

CallWhat it does
Fradiation.UnlockMutation(string key)Unlocks a mutation declared in fradiation.json. Repeat calls are harmless.
Fradiation.SubmitScore(string board, double value)Submits a score to a declared board. The site keeps each player's best.
Fradiation.Track(string name, string json = null)Marks a moment on the playtest timeline. Extra data goes in as a JSON string.
Fradiation.AskForFeedback(string prompt = null)Opens the feedback panel in playtests. The prompt is up to 200 characters.
csharp
using UnityEngine;

public class RunEvents : MonoBehaviour
{
    // Unlock on an event.
    public void OnBossDefeated()
    {
        Fradiation.UnlockMutation("boss-down");
    }

    // Submit a time score. A board with "format": "time" takes milliseconds.
    public void OnLevelComplete(float seconds)
    {
        Fradiation.SubmitScore("fastest-clear", System.Math.Round(seconds * 1000.0));
        Fradiation.Track("level-complete", "{\"level\":3}");
    }

    // Ask for feedback at game over.
    public void OnGameOver()
    {
        Fradiation.AskForFeedback("How was that boss?");
    }
}
  • Declare every mutation and board key in fradiation.json. Scores above a board's max are rejected as implausible.
  • Track is ignored outside playtests. AskForFeedback opens the panel only in playtests.
  • While the game is a draft, calls are checked but not kept, and a toast says so. Developers get no rads from their own games.
  • Outside the site, for example a build opened from your own disk, every call does nothing. The SDK page has the full behavior.

Without the kit

The bridge is a .jslib file and a small C# wrapper. Skip this if you installed the kit.

js
// Assets/Plugins/WebGL/FradiationBridge.jslib
mergeInto(LibraryManager.library, {
  Fradiation_UnlockMutation: function (keyPtr) {
    var key = UTF8ToString(keyPtr);
    if (window.fradiation) window.fradiation.unlockMutation(key);
  },
  Fradiation_SubmitScore: function (boardPtr, value) {
    var board = UTF8ToString(boardPtr);
    if (window.fradiation) window.fradiation.submitScore(board, value);
  },
});
csharp
using System.Runtime.InteropServices;
using UnityEngine;

public static class FradiationBridge
{
#if UNITY_WEBGL && !UNITY_EDITOR
    [DllImport("__Internal")] private static extern void Fradiation_UnlockMutation(string key);
    [DllImport("__Internal")] private static extern void Fradiation_SubmitScore(string board, double value);
#endif

    public static void UnlockMutation(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        Fradiation_UnlockMutation(key);
#else
        Debug.Log("[Fradiation] mutation: " + key);
#endif
    }

    public static void SubmitScore(string board, double value)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        Fradiation_SubmitScore(board, value);
#else
        Debug.Log("[Fradiation] score " + board + ": " + value);
#endif
    }
}

Multithreading

Leave it off. Unity 6 has an experimental option in Player Settings, labelled Enable Native C/C++ Multithreading in 6000.5 (Unity's description says it doesn't multithread C# code). A build made with it needs SharedArrayBuffer, and that isn't available inside the cabinet yet.

The kit leaves the setting alone, and warns in the console if it's on while fradiation.json doesn't set "threads": true.

What the build folder looks like

text
Builds/Fradiation/
  index.html
  fradiation-sdk.js
  fradiation.json
  Build/
    Fradiation.data.br
    Fradiation.framework.js.br
    Fradiation.loader.js
    Fradiation.wasm.br
  StreamingAssets/        only if your project has any
  • Unity names the files in Build/ after the output folder, so a folder called Web gives Web.data.br.
  • index.html sits at the top. Every path in it is relative.
  • Limits: 1,000 files, 500 MB in total, 200 MB per file. The data file is usually the largest. One shipped Unity game has a 93 MB .data.br.
  • Files in StreamingAssets follow the same file type rules as everything else. An extension that isn't on the allowed list is refused. See Builds and uploads.

Packaging

Pick one.

  • The uploader. On /dev/<slug>, drop a .zip or pick the Builds/Fradiation folder. The browser reads and checks it before anything goes up.

  • check-build.mjs. Download the script (Node 20 or later). This checks the folder and zips it the way the uploader expects:

    bash
    node check-build.mjs pack Builds/Fradiation Fradiation.zip
  • tar. Windows 10 and 11 ship a tar that writes zips. Run it in PowerShell or cmd from the project root:

    powershell
    tar -a -c -f Fradiation.zip -C Builds Fradiation

    The zip holds one top-level folder, Fradiation/, and the uploader drops it. In Git Bash, tar is GNU tar and doesn't write zips. On macOS and Linux, run zip -r Fradiation.zip Fradiation from inside Builds.

Zip the build folder or its contents. Don't zip Builds itself: the uploader drops one top-level folder, so index.html would end up one level too deep.

Troubleshooting

SymptomCauseFix
Blank frame, or 404s in the network tabA template with root-absolute URLs such as /Build/.... Builds are served from https://<slug>.containment.cloud/<buildId>/, so the root is the wrong place.Use relative URLs (Build/...). Use the Fradiation template.
"No index.html at the top of the build" on uploadThe zip has an extra folder level, or index.html has another name.Zip the build folder or its contents. If the entry file has another name, set entry in fradiation.json.
"Unable to parse Build/..." or a content-encoding errorA .br or .gz file that isn't in that format, because it was renamed or re-compressed by another tool. Or files from builds with different compression settings mixed in one folder.Rebuild with Compression Format Brotli or Gzip and Decompression Fallback off. Upload the folder Unity wrote, without re-compressing anything.
The game doesn't fill the frameThe default template's fixed canvas size.Use the Fradiation template, or set the canvas to 100% width and height. Set viewport in fradiation.json.
SDK calls do nothingThe template didn't load fradiation-sdk.js, so window.fradiation doesn't exist. Or the game runs outside the site. Or the key isn't declared in fradiation.json.Check that fradiation-sdk.js is in the build root and the template has its script tag. On the site, open DevTools, switch the console to the game's frame and run await fradiation.ready(). connected: true means the handshake worked. In a draft, a successful call shows a toast. No toast means the site rejected the call.
An upload is refused with "file type not allowed"A file with an extension off the allowed list, often under StreamingAssets.Convert or remove it. Run check-build.mjs to list every problem at once.
An old version shows upYou're looking at a different build. An upload made with Upload only doesn't replace the live build.Make the build live from the builds list. Each build has its own URL, and index.html is revalidated on every load, so a hard reload rarely matters.

See also