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:
| Build | Command | Output | Loads its scene from | TypeScript compiler |
|---|---|---|---|---|
| Hosted | vite build | dist/ (normal HTML + assets) | fetch("./scene.awaken.json") | Bundled (can compile raw TS) |
| Single-file | AWAKEN_SINGLE=1 vite build | dist-single/index.html (everything inlined) | Inline globals | Dead-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-filegame.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 JSONSceneFileexports, still supported../scene.fg- the folder export sidecar,fetched when the game is served (blocked onfile://)../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:
- Baked (shipped
game.html) - behaviour classes are registered ontowindow.__AWAKEN_BEHAVIORS__by an inline classic<script>(no dynamic import, so it runs fromfile://). The player registers these directly. - 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
UINodeentities each frame. The game's imported fonts register as@font-facerules (so aUINode'sfontrenders in the pack's typeface), UI animation clips advance throughdriveUIAnimators, and the HUD auto-scales toRenderSettings.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
- Build & Player Template - how the single-file template is produced
- Export game.html - what gets baked into the player template
- Export Folder (for hosting) - the
scene.fgboot path - Preview - run the player from the editor without saving
- Data Formats & Versions - the scene container the player decodes