Skip to content

The Player Runtime

The player is Awaken's standalone runtime - a small, React-free app (apps/player) that boots a scene and runs your game. It is the same runtime the editor uses for Play, packaged to run on its own.

Every way you run a game - editor Play, Preview, a double-clicked game.html, or a hosted folder - is this player over the same @awaken/runtime. That shared runtime is what makes Awaken's edit == play == ship guarantee real: there is no separate "release build" of the engine that can drift from what you tested.

One runtime, everywhere

The player's main.ts constructs a PlayController over DEFAULT_SYSTEMS, registers your script behaviours into a ScriptRegistry, wires a RapierBackend physics runner, and drives it all from a requestAnimationFrame loop. That is the identical stack the editor Viewport builds for Play mode. The only differences a shipped game shows are cosmetic: no ground grid, no editor gizmos, and it is always "playing".

Two builds from one player

The same player source compiles into two shapes, selected by an environment flag in apps/player/vite.config.ts:

BuildCommandOutputLoads its scene fromTypeScript compiler
Hostedvite builddist/ (normal HTML + assets)fetch("./scene.awaken.json")Bundled (can compile raw TS)
Single-fileAWAKEN_SINGLE=1 vite builddist-single/index.html (everything inlined)Inline globalsDead-code-eliminated

The single-file build is the important one for shipping. It uses vite-plugin-singlefile to inline every asset into one HTML file, and sets __AWAKEN_INLINE_ONLY__ so the esbuild-wasm TypeScript compiler is stripped out (a shipped game carries baked behaviours, so it never needs to compile). It also emits a classic IIFE <script>, not <script type="module"> - because Chrome refuses to execute ES-module scripts from a file:// origin, so a double-clicked module player would silently never boot.

This single-file build is what becomes the player template that game.html is stamped from - see Build & Player Template and the stale-template gotcha.

NOTE

The dev port for the standalone player is 5181 (the editor runs on 5180). You rarely run the player directly - the editor's Preview and exports build it for you.

Booting a scene

On startup the player looks for a scene in a fixed preference order, so one player binary handles every export shape. The first source present wins:

  • __AWAKEN_SCENE_BIN__ - the current single-file game.html: a Zstd-compressed binary container, base64-inlined. Its meshes come out already dequantized, bypassing the JSON mesh path.
  • __AWAKEN_SCENE_GZ__ / __AWAKEN_SCENE__ - older gzip'd or raw JSON SceneFile exports, still supported.
  • ./scene.fg - the folder export sidecar, fetched when the game is served (blocked on file://).
  • ./scene.awaken.json - the legacy hosted JSON scene.

The inflater auto-detects the compression by magic bytes (Zstd 28 B5… vs gzip 1F 8B…) and decompresses in-browser - Zstd via fzstd (pure JS, so it works from file://), gzip via the native DecompressionStream. The whole boot - decode, mesh upload, texture upload, physics prewarm - is driven behind a splash screen with a progress bar that fades out once the first frame is on screen.

Baked behaviours vs compiled TypeScript

Scripts can arrive two ways, and the player prefers the baked form:

  1. Baked (shipped game.html) - behaviour classes are registered onto window.__AWAKEN_BEHAVIORS__ by an inline classic <script> (no dynamic import, so it runs from file://). The player registers these directly.
  2. Compiled TypeScript (hosted/dev) - for scenes that ship raw script source, the hosted player compiles the TS at load using its bundled compiler.

Because a double-clicked game runs from file:// - where ES modules and dynamic imports are blocked - the baked path is what makes single-file export work at all.

📸 Screenshot - save as img/shipping-player-boot.png

The player's boot splash mid-load, showing the progress bar and a label such as "Loading meshes… 340/512".

What the player provides at runtime

Beyond stepping the runtime, the standalone player wires up the pieces a finished game needs:

  • GPU picker - on a hybrid laptop with two GPUs, a small corner dropdown lets a stuck player switch GPUs (single-GPU machines never see it).
  • Input - keyboard events plus click-to-lock pointer for FPS-style mouse-look.
  • In-game UI - a DOM overlay reconciled from UINode entities each frame. The game's imported fonts register as @font-face rules (so a UINode's font renders in the pack's typeface), UI animation clips advance through driveUIAnimators, and the HUD auto-scales to RenderSettings.uiReferenceHeight (0 = raw pixels, off). See In-Game UI.
  • Physics on demand - Rapier's WASM only loads if the scene actually has colliders or rigidbodies, and a physics failure is caught so it never white-screens the game.
  • Persistent save state - per-title saved data (high scores, etc.), namespaced by the game's title.
  • Debug overlays - F3 / ` toggles the stats readout; F4 draws collider wireframes - the same tools as the editor, so you can profile the shipped game.

See also

Awaken — browser-native WebGPU game engine.