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.

A ScriptBehavior class open in the Code panel: its fields at the top, then onUpdate reading api.input and moving with api.moveAndSlide.

HookSignatureDescription
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): voidRuns every frame. dt is seconds since the last frame - multiply motion by it for frame-rate independence.
onFixedUpdate(api: ScriptApi, dt: number): voidRuns once per fixed step (Project Settings → Time: 60 a second by default, at most 5 a frame), before that step's physics (when physics is on the fixed clock) and Simulations. dt is always 1 / Fixed rate, so logic here gives the same result at any frame rate. See Time & the Fixed Clock.
onDestroy(api: ScriptApi): voidRuns when the slot tears down (entity destroyed, script disabled, or Play stops). Clean up.
onParams(api: ScriptApi): voidA 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): 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("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.

A hook that throws does not stop the frame. Each call into your script (onStart, onUpdate, onParams, onDestroy, a collision hook, an after/every timer, an onBack handler) is caught on its own: the other scripts, the frame and the game carry on, and the throwing script keeps being called. The error is logged as <Script> <hook> failed: <message>, once, then at most every 10 seconds with a count of the repeats in between (Mover onUpdate failed: x is undefined (59 more times since it was last reported)). In a shipped mobile app each report also goes to the shell as awaken:report.

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.

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, in game time (it runs at timeScale). A live clock: a handler registered in onStart reads the time it runs at.
api.timeScalenumberHow fast game time runs: 1 normal, 0 stopped (a pause card), between for slow motion. Scales fixed steps, dt, api.time, timers, physics, animation and particles; input and UI do not follow it. Clamped to 0–100; lasts across scene loads. See Time & the Fixed Clock.
api.requestDraw(): voidAsk for the next frame to be drawn. A shipped game draws only frames that change what is on screen and sees every change made through the api by itself; call this after writing a component field directly through api.world. A no-op in the editor.
api.quality{ level, levels, set(name) }The project's quality levels (Project Settings → Quality): level is the one this device runs ("" with none), levels every name in order, set(name) switches, keeps the choice for the next launch and returns false for an unknown name. A level that resizes render textures restarts the Simulations using them.
api.viewPosition(): Vec3 | nullWhere the scene is viewed from while editing (the Scene view camera), for an edit-time streamer that keeps chunks near the viewer. null at Play, where the game follows its own player or camera.
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.wasKeyPressed(key): booleanDid the key go down this frame? One true per physical press, even a tap released before the frame ran; auto-repeat is not a press.
input.isButtonDown / input.wasPressed(button): booleanMouse button held / went down this frame (0 left, 1 middle, 2 right).
input.pointerX / pointerYnumberPointer position in canvas CSS px. Also presses, pointerSamples, downX/Y, upX/Y, wheelX/Y, pinch, viewWidth/Height and safeArea: see Input.
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.
setRotation(x, y, z, w): voidSet the rotation to the quaternion (x, y, z, w), replacing it outright (same space as the Transform). Use rotate for an incremental turn.
screenToWorld(px, py): { origin, dir } | nullWorld ray through a canvas pixel (CSS px, as input.pointerX/Y report) from the active camera; dir is normalised. null with no camera (edit-time scripts, headless tests). Intersect it with your own plane or a raycast.
worldToScreen(p: Vec3): { x, y } | nullCanvas CSS pixel of a world point from the active camera; null when the point is behind the camera.
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(e?: Entity): voidDestroy this entity, or e, and everything under it. Each destroyed script's onDestroy fires the same frame; its timers and event handlers are cancelled. api.world.destroyEntity(e) does the same.
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?, parent?, name? }: parent puts the new root under that entity (position and rotation are then local to it; a UI prefab spawned into a screen is gated and scaled by it), name renames the new root. Returns the new root entity, or null for an unknown prefab. A dead parent leaves it at the scene root.
destroy(e?): voidDestroy this entity, or e, with everything under it (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.
getPositionOf(e: Entity): Vec3 | nullAnother 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 | nullAnother entity's local Transform rotation. Same null rule.
setRotationOf(e, x, y, z, w): voidSet another entity's rotation.
setVelocity(e, x, y, z): voidSet 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.

api.world ​

api.world is the live ECS world. These members are declared in the Code panel's IntelliSense (type is a component type from api.world.registry.get("UINode")):

MemberSignatureDescription
createEntity(): EntityA new, empty entity.
destroyEntity(e): voidDestroy an entity and everything under it.
isAlive(e): booleanWhether the handle still names a live entity.
getName / setName(e) / (e, name)An entity's name, which api.find and api.ui address.
get(e, type): anyThe component of that type on the entity, or undefined. Write its fields directly.
add(e, type, init?): anyAdd a component with its defaults, overridden by init.
has / remove(e, type)Whether it carries the component / take it off.
query(...types): Entity[]Every entity carrying all these types.
findNamed(name, type): Entity | nullThe first entity carrying type with this name. Names are indexed.
findAllNamed(name, type): Entity[]Every entity carrying type with this name (a UI element and its layout variants).
getParent / getChildren(e)The hierarchy; 0 is the scene root.
setParent(child, parent): voidReparent; 0 = the root.
isActive / setActive(e) / (e, active)The object's active flag.
isStatic(e): booleanThe static flag.
markMoved(e): voidTell the renderer a script moved this Transform.
registry.get(name)A component type by name.

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.
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 | nullThe named bone entity's local rotation. null when no descendant of this entity has that name.
setBoneRotation(bone, x, y, z, w): voidSet 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?): 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: it skips every collider of that object (a character's capsule, or all the shapes of a body).
getVelocity(entity): Vec3 | nullLinear velocity of a dynamic RigidBody after the last physics step, or null when it has no live body.
setVelocity(entity, x, y, z): voidSet the linear velocity (world units/sec) of a dynamic RigidBody - this one or one you just spawned.
applyImpulse(entity, x, y, z, px?, py?, pz?): voidPush a dynamic body (an impulse: force x seconds) through its centre of mass, or at a world point so it turns too.
getAngularVelocity(entity): Vec3 | nullSpin of a dynamic body, radians a second about each world axis.
setAngularVelocity(entity, x, y, z): voidSet it.
getPointVelocity(entity, px, py, pz): Vec3 | nullVelocity of a dynamic body at a world point, its spin included.
getMass(entity): number | nullMass in kg.
teleport(entity, x, y, z, rotation?): voidMove a dynamic body (and its Transform) to a new pose at once.
debugLine(x1, y1, z1, x2, y2, z2, r?, g?, b?): voidDraw a world-space line while the editor's Physics debug is on. From onFixedUpdate it stays until the next fixed step; from onUpdate, one frame. Not drawn in a shipped game.

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?): number | nullPlay an imported sound by id. opts = { volume?, loop?, pitch?, lowpass?, at?, minDistance?, maxDistance?, rolloff?, doppler? }; at (an entity or a point) makes it 3D. Returns a handle for stopSound, or null if the project has no such sound. See Audio.
playSynth(SynthClass, opts?): number | nullPlay a synth: a class exported from a script that makes its sound in code. Takes playSound's options plus params (starting values) and options (for its constructor).
stopSound(handle): voidStop one sound started by playSound. A null handle is a no-op; Stop and a scene load stop everything anyway.
setSoundTempo(handle, tempo): voidChange the speed of a playing MIDI without restarting it (1 = as written). Pitch is unaffected; a recording ignores it. See Audio.
setSoundVolume(handle, volume): voidChange a playing sound's own volume (0 to 1) without restarting it, as a settings slider moves. Glides over about 30 ms. The master volume still applies on top. A finished handle, or null, is a no-op.
setSoundPitch(handle, pitch): voidChange a playing sound's pitch: 1 as recorded, 2 an octave up (a recording plays faster with it). Glides.
setSoundLowpass(handle, hz): voidMuffle a playing sound with a low-pass filter at hz; 0 removes it. Glides.
setSoundParam(handle, name, value): voidSet a playing synth's param by the name the synth reads it by. A recording ignores it.
setMasterVolume(v): voidMaster volume 0 to 1, applied to everything playing now and later; each sound's own volume is untouched. 0, then 1 to restore, is a pause-menu mute. Persists until changed.
setHud(text): voidSet the on-screen HUD overlay text (top right of the Scene and Game views in the editor). 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.setSoundVolume(music, 0.25); // quieter, from where it is
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 ​

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 (text, panels, buttons, images) and screens, addressed by their entity name. Every element verb acts on every element with the name, and every screen verb on every screen with it, so a layout variant follows without the script knowing which one is showing. Covered in full on UI from Scripts.

MemberSignatureDescription
ui.setText(name, text): voidSet the elements' text.
ui.setImage(name, imageId): voidSet the elements' sprite: a UI image id or a texture id.
ui.show(name): voidvisible = true. Warns once if the element's screen is hidden.
ui.hide(name): voidvisible = false.
ui.onClick(name, cb): voidClick handler for a button element (never fires for a panel, text or image). One handler per name: the last registration wins.
ui.onPress(name, cb): voidPointer-down handler for any element; registering makes it take presses.
ui.onPointer(name, cb(x, y, clicks)): voidClick handler for any element, x / y 0..1 across it, clicks the burst count.
ui.hitTest(x, y): string | nullThe current name of the element a press at canvas point (x, y) would land on, or null over the game.
ui.rect(name): { x, y, width, height } | nullWhere the showing element of that name is drawn, canvas CSS px from the top-left; null when none shows.
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).
ui.showScreen / ui.hideScreen(name): voidShow / hide every screen with the name.
ui.toggleScreen(name): voidFlip them.
ui.switchScreen(name): voidShow it and hide every other screen that is not pinned; an unknown name does nothing.
ui.screenVisible(name): booleanWhether a screen of that name shows at the current view size.
ts
api.ui.setText("Score", "Score: 10");
api.ui.setImage("Portrait", "hero_face");          // a UI image or texture id
api.ui.onClick("Start", () => api.ui.switchScreen("HUD"));
api.ui.play("Game Over Panel", "slideIn");         // run a UIClip on the widget
const r = api.ui.rect("Side Panel");               // where it is drawn, or null

Runtime 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.

SymbolSignatureDescription
api.mesh.set(data: RuntimeMeshData): voidGive 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): voidRegister a runtime mesh under an id you choose. For the case where one script owns many meshes.
api.mesh.update(id, data): voidReplace one. Same call as create; the separate name is so streaming code reads honestly.
api.mesh.destroy(id): voidFree it.
api.mesh.place(id, data, opts?): voidRegister geometry and put an object in the scene to draw it, at opts.position relative to this entity.
api.mesh.remove(id): voidRemove 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 ​

SymbolSignatureDescription
api.texture.create(id, data: RuntimeTextureData): voidRegister an image built at runtime, addressable by id from any material's texture field.
api.texture.update(id, data): voidReplace it.

RuntimeTextureData is { width, height, data: Uint8Array } - raw RGBA, row-major, width * height * 4 bytes - plus two sampling flags:

  • point samples nearest. A palette or a block atlas sampled bilinearly blends swatches that were never adjacent.
  • noMips skips 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.

Simulations and texture probes ​

SymbolSignatureDescription
api.simulation(entity): SimulationHandle | nullThe Simulation running on entity (its Simulation component), or null. reset() clears its textures to their clear colours and its step count to 0; paused = true holds it; step is the number of fixed steps since the last reset.
api.probe(entity): ProbeHandle | nullThe Texture Probe on entity, or null. sample(x, y) reads its render texture at a world point, bilinear, as it was Probe delay fixed steps ago: [r, g, b, a], or null before the first window has landed.
ts
onFixedUpdate(api: ScriptApi) {
  const p = api.getPosition();
  const depth = api.probe(api.entity)?.sample(p.x, p.y);
  if (depth && depth[0] > 0.5) api.setVelocity(api.entity, 0, 2, 0); // float on the wave
}

See Simulations and Time & the Fixed Clock.

GPU compute ​

api.gpu runs WGSL compute shader assets over named textures and buffers the script declares, with async readback. With no GPU, available is false and every call is a no-op except read, which rejects.

MemberSignatureDescription
availablebooleanWhether a GPU is there to run on.
generationnumberChanges with each Play session and each executor. When it changes, resources declared under the old value are gone: declare them again. 0 with no GPU.
texture(name, { width, height, format }): voidCreate a texture, or recreate it if the description changed. Formats: rgba16float, rg16float, r16float, r32float, rg32float, rgba8unorm, r32uint.
buffer(name, { size }): voidCreate a buffer (size rounds up to 16 bytes).
drop(name): voidRelease one. Ops already queued stay valid.
write(name, data, byteOffset?): voidUpload bytes. A texture write fills the whole texture with tightly packed rows.
clear(name, value?): voidA texture to value (default zeros); a buffer to zeros.
copy(src, dst): voidCopy the overlapping region.
dispatch(shader, entry, bindings, [x, y?, z?]): voidRun a compute entry point over workgroups. Each binding names a texture (sampled, write, read_write), a buffer (uniform, read, read_write) or a sampler (linear-clamp, nearest-clamp, linear-repeat), with its binding and optional group. read_write storage textures exist only in r32float and r32uint.
read(name, byteOffset?, byteLength?): Promise<ArrayBuffer>The bytes as of this point in the op order, a frame or two late.

Ops run in call order and are flushed once per tick after scripts run, whether or not the frame draws. A write after a dispatch in the same tick is seen only by later dispatches. For per-pixel simulation steps authored as graphs, use Simulations instead.

Terrain ​

api.terrain answers questions about the terrain surface under a world-space point. Every call returns null off every terrain.

MemberSignatureDescription
heightAt(x, z): number | nullWorld Y of the terrain surface.
normalAt(x, z): Vec3 | nullWorld-space unit surface normal.
biomeAt(x, z): string | nullThe biome name the terrain's generator gave this point; null on a terrain without biomes.
waterAt(x, z): number | nullWorld Y of the water surface (lake, river or sea), or null where the ground is dry.

See Terrain.

Jobs ​

api.jobs.run(entry, input) runs an entry point of a compiled WebAssembly module on a worker and returns a Job at once: { result: Promise<Uint8Array>, cancel() }. run(moduleId, entry, input) names one module explicitly. Cancel a job whose result is no longer wanted; its result then rejects with JobCancelled. See Modules, Jobs & Libraries.

Shader globals ​

SymbolSignatureDescription
api.setGlobalFloat(name, value): voidSet a named float every shader graph can read.
api.setGlobalVec(name, x, y?, z?, w?): voidSet 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.

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; });
}

The engine raises its own events on the same bus: awaken:launch, awaken:background, awaken:foreground, awaken:close and awaken:error ({kind, message, stack}). See Outbox & App Events.

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

Two calls for a game in the mobile app, which also work in a browser.

MemberSignatureDescription
openUrl(url: string): booleanOpen a web page (https only) or an email (mailto:) outside the game: in the mobile app the phone's browser or mail app, in a browser a new tab, in the editor's Play a new tab. For a privacy policy, a support page, a contact address. Anything else (http, other schemes, a host with a user name in it, control characters) is refused and logged. Spaces become %20. Returns whether the link was handed on.
onBack(handler: () => boolean): voidHandle the Android Back button. Return true when the handler dealt with the press (closed a page, went back a screen), false when it did not. The newest handler is asked first, then older ones; when none returns true, the app goes to the background, as Back does in any app. A handler goes with its script, like on. Never called on iOS or in a browser.
ts
export default class Menu implements ScriptBehavior {
  open = false;
  onStart(api: ScriptApi) {
    api.ui.onClick("Privacy", () => api.openUrl("https://example.com/privacy"));
    api.ui.onClick("Support", () => api.openUrl("mailto:help@example.com?subject=Help with the game"));
    // Back closes the menu when it is open; otherwise it is not ours to handle.
    api.onBack(() => {
      if (!this.open) return false;
      this.open = false;
      api.ui.hideScreen("Menu");
      return true;
    });
  }
}

In the mobile app the page posts {"type": "awaken:open", "url"} and the shell checks the link again before Linking.openURL; for Back the shell raises an awaken:back event in the page and waits up to 300 ms for the page's awaken:back-handled answer. See Export for Mobile.

In-app purchases ​

api.store: the products App Settings (Store) list, what the player owns, buying and restoring. In the mobile app the App Store or Google Play answer; in the editor's Play, Preview and a web export the test store does, as App Settings' test mode says. See In-app purchases.

MemberSignatureDescription
store.availablebooleanA store can take purchases right now.
store.ready(): Promise<void>Resolves once the products and ownership are known, or the store is known unavailable.
store.products(): { key, title, price, owned, type }[]Every product, with the store's localised price (null until it answers).
store.owns(key: string): booleanOwnership as the store reports it. Never decide it from the game's own save alone.
store.buy(key: string): Promise<"purchased" | "cancelled" | "pending" | "failed" | "unavailable">Buy a product. "pending": onChange runs when it goes through.
store.restore(): Promise<{ restored: string[] }>Ask the store what the player owns. Apple requires a Restore Purchases button.
store.onChange(handler: () => void): () => voidOwnership or a price changed. Returns the unsubscribe; the handler goes with its script.
ts
await api.store.ready();
if (!api.store.owns("unlock") && (await api.store.buy("unlock")) === "purchased") api.ui.hide("BuyButton");

Outbox ​

api.outbox: JSON for a server of the game's own (usage statistics, error reports, scores), kept until it is sent. The engine sends nothing of its own. See Outbox & App Events.

MemberSignatureDescription
outbox.post(url: string, body: object): booleanQueue a plain object or array (at most 32 KB as JSON) for an https URL (http only to localhost). Sent in batches as {"batch": [body, ...]} when the app goes to the background or closes, once 20 wait, every 30 minutes and at launch. In the editor's Play nothing is sent; each post is written to the Console.
outbox.flush(): voidSend what waits now.
outbox.clear(url: string): voidForget everything waiting for url, unsent.

App ​

api.app: the app this game runs as, and what the game asks of it. See Release channel.

MemberSignatureDescription
app.channel"release" | "beta" | "dev" (read-only)Who this copy is for: players (a store, a web host), testers (TestFlight) or you (the editor's Play, a build run from Xcode or Android Studio, a page on localhost, an automated browser). Fixed before the first frame.
app.keepAwake(on: boolean): voidKeep the screen on while the game shows (true, as every game starts) or let it dim and lock as the device's settings say (false). Holds until changed, across scene loads.
app.haptic(kind: "select" | "tap" | "heavy" | "success" | "warning" | "error"): voidPlay a haptic the player feels. Fire-and-forget, at most one every 40 ms. See Haptics.

Haptics ​

api.app.haptic(kind) plays a short tap or buzz, named by what it means: "select" (a choice moved, a picker's tick), "tap" (a light press), "heavy" (a firm thud), and the outcomes "success", "warning" and "error".

ts
api.app.haptic("tap");                    // a piece placed
if (solved) api.app.haptic("success");    // plays even straight after the tap
  • Rate limit. At most one plays every 40 ms of wall-clock time; a call sooner than that is dropped, so a script that calls every frame buzzes 25 times a second at most. A call inside the 40 ms still plays when its kind is stronger than the last one played (select, then tap, then heavy, then the three outcomes together), so the tap that also wins a level gets its success.
  • Checked. Anything but one of the six kinds is refused and logged, once for each value. The call returns nothing.
  • The phone decides. With the phone's own haptics setting off (iOS System Haptics, Android touch feedback), nothing is felt, and the engine adds nothing that overrides it.
  • Your game's switch. The engine has no haptics setting. If your game offers a vibration option, keep it in your save and do not call haptic while the player has it off.
KindiOS appAndroid appWeb export (navigator.vibrate, ms)
selectUISelectionFeedbackGeneratorCLOCK_TICK8
tapUIImpactFeedbackGenerator, lightVIRTUAL_KEY12
heavyUIImpactFeedbackGenerator, heavyLONG_PRESS30
successUINotificationFeedbackGenerator, successCONFIRM12, 60, 12
warningUINotificationFeedbackGenerator, warningLONG_PRESS24, 60, 24
errorUINotificationFeedbackGenerator, errorREJECT30, 50, 30, 50, 30

The iOS app makes and readies its feedback generators when the game's page loads, and readies each again after it plays, so the first haptic plays without the Taptic Engine's start-up delay. The Android app calls View.performHapticFeedback, which needs no VIBRATE permission; warning and heavy feel the same there. A web export vibrates only where the browser has navigator.vibrate (Chrome on Android, after the player's first tap, and not while the phone is set to silent); iOS Safari has no vibration, so nothing happens there. The editor's Play plays nothing and writes each haptic to the Console.

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.