Skip to main content

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.entitythe object this script is attached to
ctx.scenethe scene it belongs to
ctx.spacethe 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:

TriggerYou 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-localizedwhere 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
});
This is the shortcut most people miss

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.

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
The two treat modifiers differently, on purpose

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.

It returns a promise, and that is not a mistake

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] });
Setting a dynamic body's transform does nothing

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 changeSurvives moving between spacesSurvives reload
storeyesnono
globalsyesyesno
space-scoped globalsyesno — deliberately isolatedno
Nothing here survives closing the tab

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.