Skip to content

Writing & Compiling

The Code panel is a full in-browser TypeScript workspace - write a ScriptBehavior, press Compile, and it is instantly runnable in Play.

Everything here runs in your browser tab. There is no toolchain to install: TypeScript is transpiled by esbuild-wasm and the editor is a real Monaco instance (the same editor as VS Code) with full IntelliSense over the Awaken API.

The Code panel

Open the Code tab (or click + New Game Script → Code from a Script component in the Inspector). In a project with no scripts yet the Code tab isn't shown as a first-class tab - find it under the menu on the tab bar, or create your first script from the Asset Browser; see when a panel is shown. The panel has three regions:

  • Script list (left) - every script asset in the project, by display name, in the shared asset-editor sidebar: click to open, double-click or right-click to Rename, right-click to Delete, and collapse it to an icon strip to reclaim width. The footer offers + New Game Script, + New Editor Script, and + New Plugin, each creating a script from its template and dropping you straight into naming it.
  • Editor (centre) - the Monaco editor for the open script's source.
  • Toolbar (top) - the display-name field, the Compile button, and a status line (Compiling…, Compiled ✓, or Error - see console).

📸 Screenshot - save as img/scripting-code-panel.png

The Code panel with a script open: the script list on the left, Monaco in the centre showing the template, and the Compile button + "Compiled ✓" status in the top bar.

Stable ids, renamable names

Each script has a stable id (a UUID) that never changes, and a display name you can rename freely. Components reference the id, so renaming a script in the list - or in the top bar's name field - never breaks the objects running it. This is why the list key and the on-object reference are decoupled. See Scripting Overview for the data model.

Deleting a script unregisters it: any component still referencing that id shows a ⚠ missing script marker in its picker, and the behaviour simply stops running.

The ScriptBehavior template

Every new script starts from this template (SCRIPT_TEMPLATE):

ts
// Component behavior - runs during Play. Type 'api.' for autocomplete.
export default class implements ScriptBehavior {
  onUpdate(api: ScriptApi, dt: number) {
    api.translate(dt * 1.5, 0, 0); // drift along +X
    if (api.input.isDown(" ")) api.setField("RigidBody", "velocity", { x: 0, y: 6, z: 0 });
  }
}

The rules a script must follow:

  • export default a class. The compiler expects the module's default export to be a class it can new. Anything else fails with script must export default a class.
  • implements ScriptBehavior is optional but recommended - it makes Monaco check your hook signatures.
  • Implement only the hooks you need - onStart, onUpdate, onDestroy, onCollision, onCollisionEnter, onCollisionExit. See the ScriptApi Reference for the full set.
  • Public class fields become Inspector controls - see Inspector Parameters.

Monaco IntelliSense

The editor ships the full Awaken scripting API as an ambient type library, so api. autocompletes every method with its doc comment, and mistyped calls are underlined before you ever compile. The bundled definitions cover:

  • ScriptApi - the api argument every hook receives.
  • ScriptBehavior - the hook signatures.
  • Input, UIApi, and a lightweight World for advanced use.
  • Vec3, Quat, Entity, and the entity-reference alias types (Camera, Light, MeshRenderer, MeshMaterial, RigidBody, Collider, AudioSource, Animator, UINode, Tag, GameObject).

You do not import anything - these types are global in the script scope. The alias types (like camera: Camera) double as Inspector hints; a field typed Camera becomes an object picker filtered to cameras (see Inspector Parameters).

The editor uses a 2-space tab size and a dark theme, and lays out automatically as you resize the panel.

Compiling

Pressing Compile runs the pipeline that makes your class runnable:

Under the hood:

  1. compileTs transpiles your TypeScript to ES2022 JavaScript (ESM) with esbuild-wasm. Types are erased; only the JS remains.
  2. loadBehavior dynamically imports that JS as a module - normally via a blob: URL, falling back to a data: URL where blob: is unsupported - and returns a factory that constructs a fresh instance.
  3. The class is registered into the shared ScriptRegistry under the script's stable id.
  4. The script's public fields are re-discovered so the Inspector's param controls stay in sync with the code.

On success the status reads Compiled ✓ and the compile is logged to the Console. On failure the error message is logged and no class is registered - the old one (if any) keeps running.

The ScriptRegistry - one registry, two consumers

The registry is shared: the editor compiles into it and the PlayController runs from it. When you press Play, the controller's ScriptHost looks up each attached script by id and instantiates it. This is why compiling is a prerequisite to seeing behaviour - an attached-but-never-compiled script shows Compile the script to expose its fields in the Inspector and does nothing in Play.

Loading a project recompiles every script on open, so a freshly opened .awaken file is immediately playable.

Live source mirroring

You do not need to Compile to save your work. Every keystroke mirrors the current buffer into the project and marks it dirty, so Save Project always captures exactly what is on screen - even a half-written script you never compiled. Compile is only about producing the runnable class; it is orthogonal to persistence.

The practical consequence: your source is never lost, but your running behaviour only updates when you Compile.

ESM in the editor, IIFE for ship

The editor and a shipped game load compiled scripts differently, for a security reason worth knowing:

ContextFormatWhy
Editor Play / PreviewESM via blob:/data: import (compileTs)Works from any real browser origin
Exported game.htmlSelf-contained IIFE (compileTsGlobal)A double-clicked file:// page cannot import() a blob:/data: URL - Chrome blocks it as a unique origin

When you export a game, every runtime script is recompiled as an IIFE that attaches its class to a global, so the game runs offline from a plain double-click. You do not do anything to trigger this - it is part of the export pipeline. Editor-kind scripts are excluded entirely (see Editor-Only Scripts). See Export game.html for the full pipeline.

See also

Awaken — browser-native WebGPU game engine.