# 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#.

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](https://www.fradiation.games/kits/fradiation-unity-kit.zip) and unzip it into your project folder, so the kit's `Assets` folder merges with yours. The files land here:

   | Path | What it is |
   | --- | --- |
   | `Assets/Fradiation/Editor/FradiationBuild.cs` | The build menu item and the batch method. |
   | `Assets/Fradiation/Runtime/Fradiation.cs` | The C# API. |
   | `Assets/Plugins/WebGL/Fradiation.jslib` | The 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](/docs/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](https://www.fradiation.games/skills/fradiation/check-build.mjs)), then upload the `Builds/Fradiation` folder or a zip of it on `/dev/<slug>`. See [Packaging](#packaging) and [Publish a game](/docs/publish).

## What the kit sets, and doing it by hand

If you skip the kit, set these yourself.

| Setting | Value | Where |
| --- | --- | --- |
| Build target | Web | Unity 6: File → Build Profiles → Web. Earlier versions: File → Build Settings → WebGL. |
| WebGL Template | A template that fills the window | Player Settings → Resolution and Presentation |
| Compression Format | Brotli (or Gzip) | Player Settings → Publishing Settings |
| Decompression Fallback | Off | Player Settings → Publishing Settings |
| Data Caching | On | Player Settings → Publishing Settings |
| Name Files As Hashes | Not needed | Player 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](https://www.fradiation.games/sdk/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.

| Call | What 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](/docs/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.

> [!WARNING]
> Threaded builds don't run on the site yet: the game pages that frame them aren't cross-origin isolated. `"threads": true` is accepted and serves the build isolated, ready for when they are. See [Threads](/docs/builds#threads).

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](/docs/builds).

## 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](https://www.fradiation.games/skills/fradiation/check-build.mjs) (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.

> [!WARNING]
> Some versions of Windows PowerShell 5.1 write backslashes into zip entry names with `Compress-Archive`. The uploader refuses them with "paths must be relative, with forward slashes". Use `tar` or `check-build.mjs pack`, and check any zip with `node check-build.mjs <zip>` first.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| Blank frame, or 404s in the network tab | A 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 upload | The 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 error | A `.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 frame | The 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 nothing | The 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 up | You'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

- [SDK](/docs/sdk) for the full API and modes.
- [fradiation.json](/docs/fradiation-json) for `viewport`, `threads`, mutations and boards.
- [Builds and uploads](/docs/builds) for limits, paths and file types.
- [Publish a game](/docs/publish) for the upload steps.
