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.
| Hook | Signature | Description |
|---|---|---|
onStart | (api: ScriptApi): void | Runs once, the first frame this slot is live. Set up state, register on handlers, cache refs. |
onUpdate | (api: ScriptApi, dt: number): void | Runs every frame. dt is seconds since the last frame - multiply motion by it for frame-rate independence. |
onDestroy | (api: ScriptApi): void | Runs when the slot tears down (entity destroyed, script disabled, or Play stops). Clean up. |
onCollision | (api: ScriptApi, other: Entity): void | Once per frame for each other entity this one's collider currently overlaps ("stay"). |
onCollisionEnter | (api: ScriptApi, other: Entity): void | The first frame this collider begins overlapping other (the hit/enter edge). |
onCollisionExit | (api: ScriptApi, other: Entity): void | The 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.
| Member | Type | Description |
|---|---|---|
api.entity | Entity | The object this script is attached to. |
api.world | World | The live ECS world. Advanced/rare - most gameplay goes through the helpers below. |
api.time | number | Seconds elapsed since Play started. |
api.input | Input | This frame's input state - see Input below. |
api.state | Record<string, unknown> | Session bag surviving scene loads (score, lives, level) - see Persistence. |
log | (...args): void | Write 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.
| Member | Signature | Description |
|---|---|---|
input.isDown | (key): boolean | Is a key currently held? e.g. "w", " " (space), "shift". |
input.mouseDX | number | Per-frame mouse-look delta (X). Accumulated this frame, cleared after scripts run. |
input.mouseDY | number | Per-frame mouse-look delta (Y). |
input.pointerLocked | boolean | Whether 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.
| Member | Signature | Description |
|---|---|---|
getPosition | (): Vec3 | Current position. |
setPosition | (x, y, z): void | Set absolute position. |
translate | (x, y, z): void | Add to position (world delta). |
getScale | (): Vec3 | Current scale. |
setScale | (x, y, z): void | Set scale. |
rotate | (x, y, z, angle): void | Rotate around world-space axis (x,y,z) by angle radians. |
getMeshCenter | (): Vec3 | This 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.
| Member | Signature | Description |
|---|---|---|
meshData | (): { positions, indices } | null | This entity's geometry: rest vertex positions (3 floats/vertex, local space) + triangle indices; null if meshless. Read once, build your constraints. |
deformMesh | (positions): void | Replace 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
| Member | Signature | Description |
|---|---|---|
destroy | (): void | Destroy 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.
| Member | Signature | Description |
|---|---|---|
find | (name): Entity | null | First 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 | null | Instantiate a prefab from the game's library. opts = { position?, rotation? }. Returns the new root entity, or null for an unknown prefab. |
destroy | (): void | Destroy this entity (see above). |
getField | (component, field): unknown | Read a field of any component on this entity by component + field name. undefined if absent. |
setField | (component, field, value): void | Write 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).
| Member | Signature | Description |
|---|---|---|
playClip | (clip, opts?): void | Play 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): void | Set 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): void | Fire 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): void | Toggle whether the animation owns the character's vertical. ON by default. |
ragdoll | (vx?, vy?, vz?): void | Turn 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.
| Member | Signature | Description |
|---|---|---|
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 | null | Cast 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): void | Set the linear velocity (world units/sec) of a dynamic RigidBody - this one or one you just spawned. |
cfg (a CharConfig) tunes the character controller:
| Field | Unit | Meaning |
|---|---|---|
stepHeight | m | Max obstacle height auto-stepped over (stairs, curbs). |
slopeLimit | degrees | Steepest walkable slope. |
skinWidth | m | Gap 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
| Member | Signature | Description |
|---|---|---|
playSound | (clip, opts?): void | Play a sound by clip id. opts = { volume?, loop? }. The built-in clip is "beep". |
setHud | (text): void | Set 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
| Member | Signature | Description |
|---|---|---|
burst | (effect, opts?): void | Spawn 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.
| Member | Signature | Description |
|---|---|---|
ui.setText | (name, text): void | Set a UI element's text. |
ui.setImage | (name, imageId): void | Set an image element's sprite to an imported texture id (e.g. swap a HUD icon or portrait). |
ui.show | (name): void | Make a UI element visible. |
ui.hide | (name): void | Hide a UI element. |
ui.onClick | (name, cb): void | Register a click handler for a UI button, by its element name. |
ui.play | (name, clip): void | Play a UI animation clip on a widget - sets/adds its UIAnimator and restarts it from time 0. |
ui.stop | (name): void | Freeze 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 widgetEvents
Decoupled messaging - one script fires an event, any number of others react, with no direct references between them. See Events & Timers.
| Member | Signature | Description |
|---|---|---|
emit | (event, payload?): void | Fire a named event to every registered handler. |
on | (event, cb): void | Register 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.
| Member | Signature | Description |
|---|---|---|
after | (seconds, cb): () => void | Run cb once after seconds. |
every | (seconds, cb): () => void | Run 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.
| Member | Signature | Description |
|---|---|---|
loadScene | (name): void | Switch to a named scene from the library. |
reloadScene | (): void | Restart 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.
| Member | Signature | Description |
|---|---|---|
save | (key, value): void | Persist a JSON-serialisable value across reloads (high scores, unlocks). |
load | (key): unknown | Read a previously saved value, or undefined. |
state | Record<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 reloadLog
| Member | Signature | Description |
|---|---|---|
log | (...args): void | Write 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
- Scripting Overview - the model and lifecycle these methods live in
- Inspector Parameters - turning public fields into controls
- Examples - complete behaviours using this API
- Components - component names for
getField/setField - Physics: Character Controller -
moveAndSlidein depth - Animation - Animators, Anim Graphs, root motion, and the third-person starter
- Particles - the effect assets
api.burstfires