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 | Promise<void> | Runs once, before the first onUpdate. Set up state, register on handlers, cache refs. May be async: the host holds onUpdate back until it resolves, so setup that awaits a job reads as a sequence instead of a chain of .then callbacks. A rejection is reported against the script and that instance never updates. |
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. |
onParams | (api: ScriptApi): void | A setting changed in the Inspector while the game is running, after the new values have been written onto this instance. See below. |
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("hit"); }
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.
onParams: live-tunable settings
Changing a script's setting while the game runs restarts that instance by default, which is the right thing for most scripts. But a restart destroys the instance, so for a script holding something expensive or irreplaceable - a streamed world holding the player's own edits - a slider nudge would throw it away.
Define onParams and the instance takes the new values in place instead: the host writes them onto the instance, then calls the hook so you can react to what changed.
ts
export default class implements ScriptBehavior {
viewDistance = 8;
onParams(api: ScriptApi) { this.retarget(api); } // no restart, streamed chunks survive
}See Script Parameters.
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. |
getPositionOf | (e: Entity): Vec3 | null | Another entity's world position - world, because the caller is asking where that thing is, and a local position under a moved parent does not answer that. null for a dead, absent, or unassigned (0) entity, so a script can guard-and-skip empty list entries. |
getRotationOf | (e: Entity): Quat | null | Another entity's local Transform rotation. Same null rule. |
setRotationOf | (e, x, y, z, w): void | Set another entity's rotation. |
setVelocity | (e, x, y, z): void | Set another entity's rigidbody velocity. |
The three …Of calls take any entity, not only this one's subtree. Pair them with an Entity or Entity[] script field the user assigns in the Inspector - see Script Parameters.
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. |
bones | (): string[] | The bone names of this entity's live-bones skin, in skeleton order. These are the names the two calls below accept, and the names of the bone child entities the importer created. [] when there is no SkinBones. |
getBoneRotation | (bone): Quat | null | The named bone entity's local rotation. null when no descendant of this entity has that name. |
setBoneRotation | (bone, x, y, z, w): void | Set the named bone entity's local rotation. The mesh follows on this frame's live-bones skinning pass, and anything parented under the bone rides along by ordinary transform composition. |
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?): number | null | Play an imported sound by id. opts = { volume?, loop? }. Returns a handle for stopSound, or null if the project has no such sound. See Audio. |
stopSound | (handle): void | Stop one sound started by playSound. A null handle is a no-op; Stop and a scene load stop everything anyway. |
setSoundTempo | (handle, tempo): void | Change the speed of a playing MIDI without restarting it (1 = as written). Pitch is unaffected; a recording ignores it. See Audio. |
setHud | (text): void | Set the on-screen HUD overlay text. Cleared automatically on Stop. |
ts
const music = api.playSound("theme", { volume: 0.5, loop: true, midi: { instrument: 10 } });
api.setSoundTempo(music, 1.3); // MIDI only: same music, faster, same pitch
api.stopSound(music);
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 widgetRuntime geometry
Geometry your script generates, uploaded to the GPU and drawn without ever touching the project file. Runtime meshes are transient: they are never written to the project, because regenerable geometry in a save is bloat. Persist the inputs (a seed, the player's edits), not the output.
| Symbol | Signature | Description |
|---|---|---|
api.mesh.set | (data: RuntimeMeshData): void | Give this object the geometry you just generated. The id is derived from the calling script slot, so two objects running the same script never collide, and this entity's MeshRenderer is pointed at it for you. |
api.mesh.create | (id, data): void | Register a runtime mesh under an id you choose. For the case where one script owns many meshes. |
api.mesh.update | (id, data): void | Replace one. Same call as create; the separate name is so streaming code reads honestly. |
api.mesh.destroy | (id): void | Free it. |
api.mesh.place | (id, data, opts?): void | Register geometry and put an object in the scene to draw it, at opts.position relative to this entity. |
api.mesh.remove | (id): void | Remove a placed object and free its geometry. |
create uploads geometry, and geometry with nothing pointing at it draws nothing - which is why a generator that owns many meshes at once (a chunk streamer, a road network, scattered debris) needs place rather than set. One entity can only show one mesh.
Placed objects are pooled and marked transient by the engine. Pooling matters: World.structureRev bumps on entity create/destroy and a bump costs the renderer a full draw-cache rebuild, so a streamer that spawned and destroyed objects would pay that every time you crossed a chunk boundary. They are children of the calling entity, so moving that entity moves the whole generated world, and saving the scene writes your script, not the hundreds of objects it built.
place is idempotent per id: call it again with new geometry and that object's mesh is replaced in place. Material comes from this entity's MeshRenderer when it has one, so the container is where you author how its output looks; pass opts.material to override per placement.
ts
export default class implements ScriptBehavior {
onUpdate(api: ScriptApi) {
for (const c of this.chunksToBuild()) {
api.mesh.place(`chunk:${c.x},${c.z}`, buildChunk(c), { position: { x: c.x * 32, y: 0, z: c.z * 32 } });
}
}
}Runtime textures
| Symbol | Signature | Description |
|---|---|---|
api.texture.create | (id, data: RuntimeTextureData): void | Register an image built at runtime, addressable by id from any material's texture field. |
api.texture.update | (id, data): void | Replace it. |
RuntimeTextureData is { width, height, data: Uint8Array } - raw RGBA, row-major, width * height * 4 bytes - plus two sampling flags:
pointsamples nearest. A palette or a block atlas sampled bilinearly blends swatches that were never adjacent.noMipsskips the mip chain. A mipped atlas averages neighbouring cells into each other at distance.
Runtime textures are transient for the same reason meshes are. Generating one at runtime is also what makes it tunable: a procedural texture baked at build time is a picture, while one built from parameters can be regenerated the moment those parameters change.
Shader globals
| Symbol | Signature | Description |
|---|---|---|
api.setGlobalFloat | (name, value): void | Set a named float every shader graph can read. |
api.setGlobalVec | (name, x, y?, z?, w?): void | Set a named vector. |
Use these to drive a shader graph from game state - wind strength, a damage flash, time of day - without a per-object material edit.
Events
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