Docs / Engines
Three.js and web engines
Ship a Vite, Three.js or plain HTML and JavaScript game on Fradiation Games. Covers relative paths, the SDK, resizing, browser permissions and what to watch for in other web engines.
If it runs in a browser, it runs here, provided every URL in it is relative. This page covers games that are already HTML and JavaScript: Vite and Three.js, other bundlers, and plain canvas games.
A build is served from https://<slug>.containment.cloud/<buildId>/. A root-absolute URL such as /assets/game.js points at the root of that origin, not at your build, and returns a 404. assets/game.js and ./assets/game.js work.
Vite
Set base to "./". Vite's default is "/", which writes root-absolute URLs into index.html.
// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
// Relative asset URLs. A build is served from /<buildId>/, not from the site root.
base: "./",
build: { outDir: "dist", emptyOutDir: true, target: "es2022" },
});Containment Breach, a Three.js game on Fradiation, builds with this base. Then:
-
Put
fradiation.jsoninpublic/. Vite copiespublic/to the top ofdist/, which is where the uploader reads it. See fradiation.json. -
Build.
-
Open
dist/index.html. Script and link URLs should start with./, not/:html<script type="module" crossorigin src="./assets/index-DgJRjt5S.js"></script> -
Check and zip. Download check-build.mjs (Node 20 or later):
bashbun run build # or: npm run build node check-build.mjs pack dist game.zip
Upload game.zip, or pick the dist folder, on /dev/<slug>. See Publish a game.
Loading assets in Three.js
Three.js loaders resolve a relative URL against the page. Put static files in public/ and refer to them with ./:
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// public/models/ship.glb becomes dist/models/ship.glb
new GLTFLoader().load("./models/ship.glb", (gltf) => scene.add(gltf.scene));"/models/ship.glb" breaks. To let Vite fingerprint a file instead, import its URL:
import shipUrl from "./ship.glb?url";Files that a loader fetches at run time need to be inside the build too:
- Draco. Copy
draco_decoder.js,draco_decoder.wasmanddraco_wasm_wrapper.jsfromthree/examples/jsm/libs/draco/intopublic/draco/, and calldracoLoader.setDecoderPath("./draco/"). Leave theREADME.mdin that folder behind. It isn't an allowed type. - KTX2.
.ktx2textures are allowed. Copybasis_transcoder.jsandbasis_transcoder.wasmfromthree/examples/jsm/libs/basis/intopublic/basis/, and callktx2Loader.setTranscoderPath("./basis/"). - Fonts.
woffandwoff2are allowed. Self-host them instead of loading them from another site, especially if you use threads. - Formats off the list.
.hdr,.exr,.fbx,.obj,.mtland.drcaren't allowed. Convert models to.glb. Loaders that read raw bytes without checking the extension, such as three'sRGBELoaderandDRACOLoader, work with the file renamed to.bin.
The full list is in Builds and uploads.
Other bundlers
The rule is the same everywhere: asset URLs are relative.
| Tool | Setting |
|---|---|
| Vite | base: "./" |
| webpack | output.publicPath of "" |
| Parcel | --public-url ./ |
| Rollup, esbuild and others | Don't write a leading / in asset URLs. |
After a build, open the output index.html and search it for src="/ and href="/. Then check every URL your code builds at run time. A local server that serves the output folder at its root hides this mistake. A draft on the site shows it: a draft always runs its newest build.
Plain HTML and canvas
A game with no build step needs no tooling. Keep everything in one folder with relative references, and zip the folder.
my-game/
index.html
game.js
fradiation.json
fradiation-sdk.js
sprites/
ship.png
sounds/
shot.ogg<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Game</title>
<style>
html, body { margin: 0; height: 100%; background: #000; overflow: hidden; }
canvas { display: block; width: 100%; height: 100%; }
</style>
<script src="fradiation-sdk.js"></script>
</head>
<body>
<canvas id="game"></canvas>
<script src="game.js"></script>
</body>
</html>Zip the my-game folder. The uploader drops a single top-level folder, so index.html ends up at the top. Or run node check-build.mjs pack my-game.
Load the SDK
The SDK is not on npm. Download a file and ship it inside your build. See SDK for the API.
ES module. Save fradiation-sdk.mjs in your source folder and import it by path:
import { fradiation } from "./fradiation-sdk.mjs";
const { connected, mode } = await fradiation.ready();
await fradiation.unlockMutation("first-blood");
await fradiation.submitScore("high-score", 12345);
fradiation.track("reached-level", { level: 3 });For TypeScript, save fradiation-sdk.d.mts next to the module. TypeScript picks it up for the .mjs import.
Script tag. Save fradiation-sdk.js next to index.html, load it before your game's scripts, and use window.fradiation:
<script src="fradiation-sdk.js"></script>Declare mutations and boards in fradiation.json first. Outside the site, every call resolves with defaults, so the same code runs in local dev.
Fill the frame
The game runs in an iframe. The cabinet letterboxes it to the viewport aspect ratio in fradiation.json, and the frame changes size when the player resizes the window or goes fullscreen. Fill the window and handle resize. Don't hard-code a canvas size.
<style>
html, body { margin: 0; height: 100%; overflow: hidden; }
canvas { display: block; position: fixed; inset: 0; width: 100%; height: 100%; }
</style>function resize() {
const w = innerWidth;
const h = innerHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
addEventListener("resize", resize);
resize();If you use a post-processing composer, call composer.setSize(w, h) in the same function.
Audio, pointer lock and fullscreen
The frame is sandboxed. It allows pointer lock, popups, modal dialogs and orientation lock, and it delegates fullscreen, gamepad, autoplay, accelerometer, gyroscope and xr-spatial-tracking.
-
Audio. Browsers can still block sound until the player interacts, even with
autoplaydelegated. Create or resume yourAudioContextinside the first click or key handler.tsaddEventListener("pointerdown", () => audioContext.resume(), { once: true }); -
Pointer lock. Request it from a click handler:
canvas.requestPointerLock(). -
Fullscreen. Request it from a click or key handler:
canvas.requestFullscreen(). -
Navigation and forms. The game can't navigate the site or submit forms. Scripts,
fetchto external APIs and popups work.
Storage
Each game has its own origin, <slug>.containment.cloud. Its localStorage, IndexedDB and caches belong to that origin and persist across builds. Other games can't read them. Browsers can block storage in an embedded frame, so wrap it and keep the game playable without it:
try {
localStorage.setItem("best", String(best));
} catch {
// storage may be blocked
}Other engines
Every engine follows the same two rules: relative paths, and file types on the allowed list. This table covers what to watch for. See Unity and Godot for their own guides.
| Engine | Watch for |
|---|---|
| Phaser | Relative paths in loader calls: this.load.image("ship", "assets/ship.png"), not "/assets/ship.png". Bitmap font .fnt files aren't allowed: use the XML format with an .xml extension. Tiled maps must be JSON with a .json extension. .tmx and .tmj aren't allowed. |
| Babylon.js | .glb and .gltf are allowed. .babylon, .env and .dds aren't on the list. |
| Emscripten and custom WebAssembly | .html, .js, .wasm and .data are allowed. Build without pthreads: threaded builds don't run on the site yet (see Threads). |
| Anything else | Run node check-build.mjs <folder>. It lists every file the rules refuse. A file type that isn't on the allowed list stays refused until it's added. |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Blank frame, 404s for scripts or assets | Root-absolute URLs. | Use relative URLs. In Vite, set base: "./". |
| "No index.html at the top of the build" | An extra folder level in the zip, or another entry file name. | Zip the build folder or its contents, or set entry in fradiation.json. |
| An upload is refused with "file type not allowed" | A file with an extension off the allowed list. | Convert it, or store it under an allowed extension if the loader ignores the extension. Run check-build.mjs for the full list. |
| The game doesn't fill the frame | A fixed canvas size. | Size the canvas to 100% and handle resize. |
| SDK calls do nothing | The SDK file isn't in the build, the script tag or import path is wrong, the key isn't declared in fradiation.json, or the game runs outside the site. | 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. |
| No sound | The browser blocks audio until the player interacts. | Start audio from a click or key handler. |