Skip to content

Architecture Overview

Awaken is an engine-first, editor-as-a-client system: a set of framework-free engine packages with a clean public API, driven by a React editor that only ever talks to that API - and by the shipped player through the same API. This section maps how the code is laid out, how it builds, and how it is tested.

Engine-first, editor-as-a-client

The core idea is a strict separation between the engine (what draws, simulates, and runs your game) and the editor (the tool you use to author it).

  • The engine is four TypeScript packages under packages/* - @awaken/core, @awaken/render, @awaken/assets, and @awaken/runtime. They have no UI framework in them at all. They expose a public API (ECS, renderer, importers, play loop) and nothing else.
  • The editor is a React app under apps/editor. It is a client of the engine: it constructs a World, a Renderer, and a Runtime, and drives them through their public methods. It never reaches inside the engine's internals.
  • The player is a second, much thinner client under apps/player. It boots a saved scene and runs it, with no editor UI.

Because the editor authors through the same API the engine exposes to a shipped game, edit == play == ship: the object you drag in the Viewport is the object the runtime ticks in Play, which is the object the player runs after export.

The engine does little, on purpose

The shipped runtime stays small because the engine provides mechanisms and keeps the opinions out. An ECS, a renderer, a script host, seams for physics, audio and effects: everything with a taste to it (a look, a gameplay pattern, an importer's knowledge of another engine, a tool) lives outside the kernel as data, content, or an editor concern, and reaches the engine through a named seam.

Measured (2026-08): the complete player runtime minifies to 416 KB (192 KB gzipped) without physics. Rapier adds 2.2 MB and ships only when a scene uses a collider or rigidbody. The editor is a 46 MB application. The two ship separately and are allowed to be different sizes because they have different jobs.

Four consequences worth knowing as a user of the engine:

  • Policy lives in data. The sky's bodies, stars and grade are scene data (SkyEffect); a material's shading is a data asset with WGSL hooks; an absent effect is simply not applied. A constant in engine code that starts describing taste is treated as a bug.
  • ScriptApi stays primitive. Gameplay is composed from importable game scripts and wasm modules that ship with the game, not from engine API growth.
  • Importers translate, they do not interpret. Unity, Godot and Unreal content maps construct-for-construct into one shared IR, one consumer applies it, and the renderer never learns which engine a mesh came from. Where a source concept has no Awaken surface, the surface gets built rather than approximated.
  • Absence is free. Physics wasm is stripped from exports with no colliders; shader-graph node functions are emitted per material; sideEffects: false lets bundlers drop importer code from the player; SSAO, GPU cull and clustered lighting are opt-in settings.

docs/ARCHITECTURE.md in the engine repo is the full statement of this, and it is the document a change is measured against.

The seams

The supported ways to extend the engine, in order of preference. Anything not reachable through a seam is an engine change.

SeamCarriesShips in games
Components + systems (DEFAULT_SYSTEMS, Runtime)New simulation behaviouryes
Script assets + wasm modulesGameplay, generated worldsyes
Hook materials / shader assetsSurface looks, vertex motionyes
Effect stack (RenderEffects)Scene look: sky, fog, grade, SSAOyes, as data
Editor plugins (sandboxed, signed)Tools, panels, importersno
Decompressor providers (AWAKEN_DECOMPRESSORS)External codecs, kept separate for licensingno
Asset storeContent: prefabs, materials, particlesas content

Two seams are deliberately absent: there is no runtime engine-plugin ABI (third-party code extending the player) and no downloadable render passes. Both would freeze renderer internals as public API and add a security surface to every shipped game, for no current need. Extending the player means contributing upstream or shipping content through the seams above.

The one-way dependency direction

Dependencies flow one way, from the base outward. Nothing in the engine ever depends on anything above it, and nothing anywhere depends on the editor.

  • @awaken/core sits at the bottom with zero dependencies - pure ECS, math, and serialization.
  • @awaken/render depends only on core; @awaken/assets depends on core and render; @awaken/runtime depends on core plus the Rapier physics WASM.
  • Both apps depend on all four packages, and nothing depends on an app.

This is not just tidiness. The one-way graph is what lets Play mode run the actual shipping runtime rather than an editor-flavoured imitation, and it is what keeps the engine reusable outside the editor.

See Monorepo & Packages for the exact package boundaries and dependency rules.

Why the engine never imports React

The engine packages never import React (or any UI framework). This is a hard rule, and it buys three concrete things:

  1. The runtime is portable. @awaken/player - a shipped game - pulls in @awaken/runtime and gets the play loop, scripts, and physics with zero React in the bundle. If the engine imported React, every exported game would ship a UI framework it never uses.
  2. Play mode is honest. The editor's Play button and the shipped player boot the same Runtime class. If Play depended on editor-only React state, "it worked in the editor" would stop meaning "it works when shipped."
  3. The seam is enforceable. The editor's dev server even reloads the whole page when engine code changes, because the editor store (which owns the PlayController and ScriptHost) must be rebuilt from fresh engine code rather than hot-patched - see the awaken-reload-on-engine-change plugin in Monorepo & Packages.

If you find yourself wanting to import React inside packages/*, that is the signal that a responsibility is on the wrong side of the seam.

One runtime, two front-ends

The same @awaken/runtime powers editor Play and the exported player. The difference is only what wraps it.

  • In the editor, Play mode snapshots the world on Play, ticks the runtime, and restores the snapshot exactly on Stop, so play-time changes never persist.
  • In a shipped game, @awaken/player skips the snapshot and just runs - the scene is the game.

Both call into the same code path, which is why a script or physics behaviour that runs in Play runs identically in the exported game.

What this section covers

  • Monorepo & Packages - the pnpm workspace layout, the four @awaken/* packages and the two apps, dependency rules, the shared TypeScript config (raw-source consumption, the ~5.6.3 pin), and the editor's custom Vite plugins.
  • Build & Player Template - the build pipeline, how build:player-template produces the single-file player that exports embed, and the critical rule that a stale template ships stale game code.
  • Testing - Vitest unit tests, the headless Deno WebGPU shader/pipeline check, the golden-frame pixel baseline, the layering and player-size gates, the Puppeteer smoke tests, and the lint / Husky gates, with the exact command for each.

📸 Screenshot - save as img/architecture-overview.png

The editor open with a scene, and a second browser tab showing the same scene running as an exported game.html - side by side to illustrate "edit == play == ship."

See also

Awaken — browser-native WebGPU game engine.