Skip to content

ScriptApi Reference

Every ScriptBehavior hook receives one api object - this page documents every member of it, plus every lifecycle hook.

The api is your complete gameplay surface. You never import engine internals; everything you need - moving the object, finding others, animating a character, playing sound, timers, physics, persistence - hangs off api. Monaco autocompletes all of it (type api.) with the same doc comments shown below.

Vec3 is { x, y, z }; Quat is { x, y, z, w }; Entity is a number handle.

Source of truth

This page mirrors the runtime ScriptApi in packages/runtime/src/script.ts and the in-editor IntelliSense (AWAKEN_DTS). A drift test keeps the two in lock-step, so what autocompletes on api. is exactly what is documented here.

ScriptBehavior hooks

The methods your class implements. All are optional.

HookSignatureDescription
onStart(api: ScriptApi): voidRuns once, the first frame this slot is live. Set up state, register on handlers, cache refs.
onUpdate(api: ScriptApi, dt: number): voidRuns every frame. dt is seconds since the last frame - multiply motion by it for frame-rate independence.
onDestroy(api: ScriptApi): voidRuns when the slot tears down (entity destroyed, script disabled, or Play stops). Clean up.
onCollision(api: ScriptApi, other: Entity): voidOnce per frame for each other entity this one's collider currently overlaps ("stay").
onCollisionEnter(api: ScriptApi, other: Entity): voidThe first frame this collider begins overlapping other (the hit/enter edge).
onCollisionExit(api: ScriptApi, other: Entity): voidThe first frame the overlap ends. Not called if other was destroyed while overlapping.
ts
export default class implements ScriptBehavior {
  onStart(api: ScriptApi) { api.log("spawned", api.entity); }
  onUpdate(api: ScriptApi, dt: number) { api.rotate(0, 1, 0, dt); }
  onCollisionEnter(api: ScriptApi, other: Entity) { api.playSound("beep"); }
  onDestroy(api: ScriptApi) { api.log("gone"); }
}

An entity can carry several scripts; each is its own slot with its own instance, timers, and event handlers, so tearing one down doesn't disturb its siblings.

Collision hooks fire for both solid collisions and trigger (sensor) overlaps - the collider's trigger flag governs physical blocking, not which hook runs. When physics is running they use real engine contacts; otherwise a built-in AABB overlap test. See Physics: Queries & Events.

Core

Read-only handles to the current frame and world, plus the console and session bag.

MemberTypeDescription
api.entityEntityThe object this script is attached to.
api.worldWorldThe live ECS world. Advanced/rare - most gameplay goes through the helpers below.
api.timenumberSeconds elapsed since Play started.
api.inputInputThis frame's input state - see Input below.
api.stateRecord<string, unknown>Session bag surviving scene loads (score, lives, level) - see Persistence.
log(...args): voidWrite a line to the console - see Log.
ts
onUpdate(api: ScriptApi) {
  if (api.time > 5) api.destroy(); // self-destruct after 5 s
}

Input

api.input is this frame's input snapshot. Covered in full on Input.

MemberSignatureDescription
input.isDown(key): booleanIs a key currently held? e.g. "w", " " (space), "shift".
input.mouseDXnumberPer-frame mouse-look delta (X). Accumulated this frame, cleared after scripts run.
input.mouseDYnumberPer-frame mouse-look delta (Y).
input.pointerLockedbooleanWhether the pointer is locked (mouse-look active - click the canvas to lock).
ts
onUpdate(api: ScriptApi, dt: number) {
  if (api.input.isDown("w")) api.translate(0, 0, -this.speed * dt);
  if (api.input.pointerLocked) this.yaw -= api.input.mouseDX * 0.002;
}

Transform

Read and write this object's position, scale, and rotation. All writes mark the object moved so the renderer and physics pick it up.

MemberSignatureDescription
getPosition(): Vec3Current position.
setPosition(x, y, z): voidSet absolute position.
translate(x, y, z): voidAdd to position (world delta).
getScale(): Vec3Current scale.
setScale(x, y, z): voidSet scale.
rotate(x, y, z, angle): voidRotate around world-space axis (x,y,z) by angle radians.
getMeshCenter(): Vec3This object's mesh geometric centre (AABB midpoint), in the same space as getPosition. Equals the position for a centred origin; differs for an off-centre pivot.
worldMatrix(): number[]This entity's 4×4 world matrix (16 numbers, column-major - translation at indices 12/13/14), composing all parents.
ts
onUpdate(api: ScriptApi, dt: number) {
  api.translate(0, Math.sin(api.time) * dt, 0); // bob up and down
  api.rotate(0, 1, 0, dt);                       // spin about world-Y
}

getMeshCenter is handy for rotating an off-centre object about its visual centre: read it before and after a rotate, then translate by the difference. worldMatrix places mesh-space points in world (invert it to go back) - e.g. to pin cloth to a moving bone. See Transforms.

Mesh deformation

Read this entity's rest geometry and push per-frame deformed vertices - the basis for cloth, jello, and water. This is a low-level primitive: the starter Cloth script builds a Verlet/position-based solver on top of it.

MemberSignatureDescription
meshData(): { positions, indices } | nullThis entity's geometry: rest vertex positions (3 floats/vertex, local space) + triangle indices; null if meshless. Read once, build your constraints.
deformMesh(positions): voidReplace this frame's vertex positions (same count/order as meshData().positions, local space). The engine recomputes normals and uploads to the GPU. No-op if meshless.
ts
onStart(api: ScriptApi) {
  const mesh = api.meshData();          // rest positions + indices
  if (mesh) this.rest = mesh.positions;
}
onUpdate(api: ScriptApi, dt: number) {
  // ...integrate constraints into this.positions...
  api.deformMesh(this.positions);       // push the deformed frame
}

Give a cloth entity its own mesh

deformMesh is keyed by mesh id. Don't share the id with other objects - deforming one would deform them all. Duplicate the mesh so the cloth entity owns a unique copy.

Lifecycle & destruction

MemberSignatureDescription
destroy(): voidDestroy this entity. Its onDestroy fires the same frame; its timers and event handlers are cancelled.
ts
onCollisionEnter(api: ScriptApi, other: Entity) {
  api.destroy(); // a projectile that dies on impact
}

Objects & references

Find other objects, spawn new ones, and reach any component field by name.

MemberSignatureDescription
find(name): Entity | nullFirst entity with this exact name, or null. Name matching is brittle - prefer findByTag.
findByTag(tag): Entity[]Every active entity carrying a Tag component with this value. Deactivated entities are excluded.
spawn(prefab, opts?): Entity | nullInstantiate a prefab from the game's library. opts = { position?, rotation? }. Returns the new root entity, or null for an unknown prefab.
destroy(): voidDestroy this entity (see above).
getField(component, field): unknownRead a field of any component on this entity by component + field name. undefined if absent.
setField(component, field, value): voidWrite a field of any component on this entity. No-op if the component/field is absent.
ts
const doors = api.findByTag("door");
const boss  = api.find("Boss");
const bullet = api.spawn("Bullet", { position: api.getPosition() });

// Reflection - the escape hatch to components the API doesn't wrap.
api.setField("RigidBody", "velocity", { x: 0, y: 6, z: 0 });
const col = api.getField("MeshMaterial", "color");

Add a Tag component to objects in the Inspector, then look them up by tag - this survives renames and duplicates where find would not. Component and field names match the Inspector labels; see Components for the full list. Spawned colliders are registered with the running physics sim automatically - see Prefabs.

Animation

Drive a character's skinned animation. Each call reaches the Animator on this entity or a child - so a controller on a parent capsule reaches the skinned character below it (the usual third-person layout).

MemberSignatureDescription
playClip(clip, opts?): voidPlay a clip on this entity's Animator, optionally crossfading over opts.fade seconds. opts = { fade?, speed?, loop? }. Idempotent - calling with the clip already playing is a no-op, so it's safe every frame.
setAnimParam(name, value): voidSet an animation-graph param (number or boolean) on the entity's AnimStateMachine. The graph's transitions read it - e.g. setAnimParam("speed", 3) drives idle→walk→run.
setAnimTrigger(name): voidFire a one-shot graph trigger (a bool that auto-resets once a transition consumes it) - e.g. setAnimTrigger("jump").
rootMotion(): { dx, dz }The current clip's horizontal displacement this frame, in the character's local space (dx = right, dz = forward). {0,0} for an in-place clip.
setRootMotionVertical(enabled): voidToggle whether the animation owns the character's vertical. ON by default.
ragdoll(vx?, vy?, vz?): voidTurn this character into a ragdoll - hand its skinned skeleton to physics so it goes limp and collides. Optional velocity is inherited by every body. Experimental; one-way.

Prefer graph params over playClip. For anything more than a clip or two, gameplay sets params (setAnimParam/setAnimTrigger) and the authored Anim Graph owns the state logic - reusable across NPCs. playClip is for one-off or simple cases.

Root motion drives movement. Feed rootMotion() (rotated by the body's facing) into moveAndSlide so the animation moves the character and the feet don't slide. {0,0} for an in-place clip.

ts
onUpdate(api: ScriptApi, dt: number) {
  api.setAnimParam("speed", speed);          // graph picks idle/walk/run
  const { dx, dz } = api.rootMotion();        // clip's local-space step
  const [wx, wz] = rotateByFacing(dx, dz, this.yaw);
  api.setRootMotionVertical(!airborne);       // hand vertical to physics mid-jump
  const { grounded } = api.moveAndSlide(wx, this.vy * dt, wz);
}

setRootMotionVertical while airborne

Turn it off (false) while your controller owns the vertical - a physics jump. That drops the clip's baked pelvis rise and skips foot IK, so neither stacks on your jump (airborne foot IK would plant the tucked jump feet on the ground plane and shove the pelvis up). Turn it back on when grounded so crouch/bob keep their pelvis motion and the feet plant. Horizontal rootMotion() is unaffected. Typical: call setRootMotionVertical(!airborne) every frame.

Still-true animation limits

CPU skinning only (no GPU skinning path); up to 4 bone influences per vertex; glTF cubicspline tracks are sampled as linear. No blend-spaces / speed-matched locomotion yet. ragdoll is experimental. The full third-person pattern ships as the starter Third Person Controller - see Animation.

Physics & character

Collision-aware movement, ray casts, and direct velocity for the current session. moveAndSlide reports ungrounded, raycast returns null, and setVelocity is a no-op if physics is not running this session. See Character Controller and Queries & Events.

MemberSignatureDescription
moveAndSlide(dx, dy, dz, cfg?): { grounded: boolean }Move this entity's capsule collider by a world delta this frame, sliding along collisions. Returns whether it ended grounded.
raycast(ox, oy, oz, dx, dy, dz, maxDist, ignore?): Hit | nullCast a ray; returns the nearest hit { point, normal, distance, entity } or null. Pass ignore (usually your own entity) so a self-probe doesn't hit your body.
setVelocity(entity, x, y, z): voidSet the linear velocity (world units/sec) of a dynamic RigidBody - this one or one you just spawned.

cfg (a CharConfig) tunes the character controller:

FieldUnitMeaning
stepHeightmMax obstacle height auto-stepped over (stairs, curbs).
slopeLimitdegreesSteepest walkable slope.
skinWidthmGap kept between the capsule and geometry; larger reduces wall clipping.
ts
onUpdate(api: ScriptApi, dt: number) {
  this.vy -= 20 * dt;                                       // gravity
  const { grounded } = api.moveAndSlide(0, this.vy * dt, -3 * dt);
  if (grounded) this.vy = 0;

  const hit = api.raycast(0, 1, 0, 0, -1, 0, 2, api.entity); // ground probe, ignore self
  if (hit) api.log("floor is", hit.entity);
}

An entity gains a capsule character body lazily on its first moveAndSlide call - no extra component needed, just a capsule Collider. setVelocity is the only way to aim a spawned dynamic body: a RigidBody.velocity field is read once at build time, so mutating it afterwards does nothing - call setVelocity on the entity spawn returned instead.

Audio & HUD

MemberSignatureDescription
playSound(clip, opts?): voidPlay a sound by clip id. opts = { volume?, loop? }. The built-in clip is "beep".
setHud(text): voidSet the on-screen HUD overlay text. Cleared automatically on Stop.
ts
api.playSound("beep", { volume: 0.5 });
api.setHud(`Score: ${api.state.score ?? 0}`);

For richer on-screen UI (panels, buttons, named text elements) use api.ui below. See Audio.

Particles

MemberSignatureDescription
burst(effect, opts?): voidSpawn a fire-and-forget one-shot particle effect by effect id - sparks, a hit flash, a dust puff. opts = { position?, rotation?, scale? }, defaulting position/rotation to this entity's world transform.

The burst is not tied to your entity: the particle engine owns its lifecycle, so destroying the caller the same frame it fires won't cut the particles short (a hammer's sparks outlive the hammer). It retires itself once every timed burst has fired and its last particle has died.

ts
onCollisionEnter(api: ScriptApi, other: Entity) {
  api.burst("sparks"); // fires at this object's world transform
}

onUpdate(api: ScriptApi) {
  const hit = api.raycast(0, 1, 0, 0, -1, 0, 3, api.entity);
  if (hit) api.burst("sparks", { position: hit.point, scale: 2 }); // twice as wide a spray
}

The argument is the effect's stable id (the id in its .awakenfx, shown when you export one) - not its display name, so renaming an effect never breaks a call. scale uniformly scales the emitter matrix: it spreads the spawn positions, it does not scale sprite size or particle speed.

Use a one-shot effect

An effect authored with a continuous rate and loop: true never satisfies the retirement test, so it would keep emitting forever with nothing to stop it. Give a burst effect rate: 0 and a bursts entry - the starter sparks is exactly that shape. For a continuous effect, place a ParticleEmitter component instead.

burst is a no-op when the effect id is unknown, or when no particle engine is active this session (headless runs and tests). Effects are content - nothing is built in, so the id has to exist in the project. See Particles and the Particle Editor.

UI

api.ui drives in-game UINode elements - HUD text, panels, buttons, and images - addressed by their entity name. Covered in full on UI from Scripts.

MemberSignatureDescription
ui.setText(name, text): voidSet a UI element's text.
ui.setImage(name, imageId): voidSet an image element's sprite to an imported texture id (e.g. swap a HUD icon or portrait).
ui.show(name): voidMake a UI element visible.
ui.hide(name): voidHide a UI element.
ui.onClick(name, cb): voidRegister a click handler for a UI button, by its element name.
ui.play(name, clip): voidPlay a UI animation clip on a widget - sets/adds its UIAnimator and restarts it from time 0.
ui.stop(name): voidFreeze the widget's UI animation (leaves it on its current frame).
ts
api.ui.setText("scoreLabel", "Score: 10");
api.ui.setImage("portrait", "hero_face");         // swap the sprite by texture id
api.ui.onClick("startButton", () => api.loadScene("Level1"));
api.ui.play("gameOverPanel", "slideIn");          // run a UIClip on the widget

Events

Decoupled messaging - one script fires an event, any number of others react, with no direct references between them. See Events & Timers.

MemberSignatureDescription
emit(event, payload?): voidFire a named event to every registered handler.
on(event, cb): voidRegister a handler (typically in onStart). Handlers reset when the scene changes or Play stops.
ts
// enemy.ts
onDestroy(api: ScriptApi) { api.emit("enemyKilled", { points: 100 }); }

// scoreboard.ts
onStart(api: ScriptApi) {
  api.on("enemyKilled", (p) => { api.state.score = (api.state.score ?? 0) + p.points; });
}

Timers

Schedule callbacks. Both return a cancel function; both are cancelled automatically if the owning entity is destroyed. See Events & Timers.

MemberSignatureDescription
after(seconds, cb): () => voidRun cb once after seconds.
every(seconds, cb): () => voidRun cb every seconds (first fire after one interval).
ts
onStart(api: ScriptApi) {
  api.after(3, () => api.destroy());                // die in 3 s
  const stop = api.every(1, () => api.log("tick")); // once a second
}

Scenes

Move between the game's named scenes. Transitions are deferred - requested now, applied at the top of the next frame. See Scenes & Persistence.

MemberSignatureDescription
loadScene(name): voidSwitch to a named scene from the library.
reloadScene(): voidRestart the current scene (e.g. Restart from Game Over).
ts
if (playerFellOffMap) api.reloadScene();
if (reachedExit) api.loadScene("Level2");

Persistence

Two stores: save/load persist across page reloads (localStorage, namespaced per game); state is an in-memory bag that survives scene loads but resets when the game reloads. See Scenes & Persistence.

MemberSignatureDescription
save(key, value): voidPersist a JSON-serialisable value across reloads (high scores, unlocks).
load(key): unknownRead a previously saved value, or undefined.
stateRecord<string, unknown>Session bag surviving scene loads - score, lives, current level.
ts
api.state.score = (api.state.score ?? 0) + 10; // survives loadScene
const best = (api.load("highScore") as number) ?? 0;
if (api.state.score > best) api.save("highScore", api.state.score); // survives reload

Log

MemberSignatureDescription
log(...args): voidWrite a line to the console - the editor's Console panel in-editor, the browser console in a shipped game.
ts
api.log("player at", api.getPosition(), "grounded", grounded);

Non-string arguments are JSON-stringified, so you can log objects and vectors directly.

See also

Awaken — browser-native WebGPU game engine.