The ctx API
ctx is your handle on the running scene — it is passed to init and everything below hangs
off it.
It is deliberately small. If you are looking for something and cannot find it here, there is a good chance the answer is a built-in step rather than an API.
Where you are
ctx.entity | the object this script is attached to |
ctx.scene | the scene it belongs to |
ctx.space | the world |
Lifecycle and events
ctx.tick((dt, t) => {}); // every frame; dt and t in seconds
ctx.effect(() => {}); // re-runs when what it reads changes; may return a cleanup
ctx.on(trigger, (payload) => {}); // subscribe to a trigger
ctx.emit(trigger, payload); // raise one from this object
Triggers use the same names as events and patches. The ones that carry useful information:
| Trigger | You get |
|---|---|
on-keydown · on-keyup | { code, ctrl, shift, alt, meta } |
on-state-active · on-state-inactive | { stateId } |
on-collide | { other } — what you hit |
on-divkit-action | { id, … } — which button |
on-game-control | { state } — idle, move, run or jump |
on-drag · on-pinch · on-rotate | { dx, dy } · { scale } · { angle } |
on-vps-localized | where the visitor turned out to be |
on-launch arrives the moment your instance is created, so you cannot miss it by starting late.
Finding objects
ctx.get(id);
ctx.findByName('Door');
ctx.find(LightComponent); // the first object with these components
ctx.query(LightComponent, TagsComponent); // all of them
ctx.all();
Creating and removing
ctx.create({ name: 'Bullet', parent, components: [] });
ctx.spawn(props.bulletModel, { parent });
ctx.destroy(entity);
spawn is the convenient one: hand it a resource and it assembles the right components for it.
A model becomes a model object, an image becomes a textured plane, a sound becomes an audio
source.
Running built-in behaviour
ctx.step('play_animation', { presetId }, { targets: [enemy] });
ctx.startTransition({ durationMs: 400, easing: 'ease-out' }, () => {
// changes made in here ease instead of snapping
});
ctx.step runs any of the built-in steps — the same ones your events
use. Animation, state switching, scene transitions and transitions are one call away, so you
almost never need to reimplement them.
Navigation
ctx.openScene(sceneOrId);
await ctx.openSpace(spaceRefOrId);
ctx.scenes();
Input
Two layers, for two different jobs.
Named actions read the project's key bindings, so a visitor who rebinds their keys is respected:
ctx.input.pressed('jump');
ctx.input.justPressed('fire');
ctx.input.axis('moveX');
Raw keys read the keyboard directly. Presses last exactly one frame, so read them inside
ctx.tick:
ctx.keyboard.down('KeyW');
ctx.keyboard.press('Space');
ctx.keyboard.press('ArrowLeft', { every: 200 }); // auto-repeat, in ms
ctx.keyboard.axis('KeyA', 'KeyD'); // -1, 0 or 1
A named binding ignores modifiers it does not mention — sprinting with Shift held must not cancel "forward". A key trigger is the opposite: a modifier you did not tick means "must not be held".
Losing window focus clears held keys, so nothing gets stuck down.
Camera
ctx.camera.entity(); // the active camera object
ctx.camera.setActive(target); // switch cameras; null restores the default
ctx.camera.pose(); // { position, rotation, forward } in world space
The camera's transform is the source of truth in every control mode: write to it to move the camera, read it to see where the controls put it. In orbit and first-person modes the controls own the orientation, so a rotation you write is overwritten — position is respected.
Raycasting
await ctx.raycast(); // from the camera centre
await ctx.raycast({ screen: { x: 0.5, y: 0 }, all: true }); // a screen point, every hit
await ctx.raycast({ origin, direction }); // any ray you like
await ctx.raycast({ from: entity }); // from an object, along its forward
Each hit tells you the object, the distance, and the point and surface normal in world space — sorted nearest first, one hit per object.
The ray is cast against real geometry on the rendering side, so the answer arrives next frame. Invisible objects and things marked to be ignored (reticles, gizmos) are skipped, so whatever is behind them answers instead of the ray reporting a miss.
Physics
ctx.physics.applyImpulse(target, { x: 0, y: 5, z: 0 });
ctx.physics.applyForce(target, vec, point);
ctx.physics.setVelocity(target, vec);
ctx.physics.teleport(target, position, { rotation, keepVelocity });
ctx.physics.setGravity(vec);
ctx.physics.getSpeed(target);
ctx.physics.isSleeping(target);
await ctx.physics.raycast(from, to, { skip: [ctx.entity] });
Physics owns its position. Use teleport to place it and impulses or forces to move it.
And pass skip when firing a ray from inside your own body, or you will hit yourself every
single time.
Readings come from the last synchronised state and lag about a frame — fine for "am I moving?", wrong for exact instantaneous maths.
Audio
ctx.audio.play(props.hitSound, { at: enemy, volume: 0.6, positional: true });
Every call starts an independent sound, which is exactly what footsteps, impacts and gunfire need. The audio component is a single voice and will cut itself off — do not use it for effects.
Keeping things
Three stores, differing in how far they reach:
// 1. this script's own values
const store = ctx.store('game', { score: { type: 'number', default: 0 } });
store.set('score', (v) => v + 1);
store.subscribe('score', (v) => {});
// 2. globals — shared with every script, patch and event
ctx.setGlobal('level', 3);
ctx.getGlobal('level');
ctx.subscribeGlobal('level', (v) => {});
// 3. messages between scripts
ctx.postMessage('enemy-died', { id });
ctx.handleMessage('enemy-died', ({ id }) => {});
| Survives a scene change | Survives moving between spaces | Survives reload | |
|---|---|---|---|
store | yes | no | no |
| globals | yes | yes | no |
| space-scoped globals | yes | no — deliberately isolated | no |
None of these three is saved between visits. If something must persist, send it somewhere yourself while you still have a connection.
Interface
const ui = ctx.getDivKit(entity);
ui.get('score');
ui.set('score', (v) => v + 1);
ui.subscribe('lives', (v) => {});
ui.onAction('restart', () => {});
See UI cards.
Next: Things you will actually build — complete scripts to copy.