Skip to main content

The ctx API

ctx is how your script works with the running scene. This page lists everything it can do.

init passes ctx to your script. The API is small on purpose. If you cannot find something here, look for a built-in step that does it.

Where you are​

NameWhat it is
ctx.entitythe object this script is attached to
ctx.scenethe scene the object is in
ctx.spacethe space — the whole world the visitor is in

Frames and triggers​

ctx.tick((dt, t) => {});          // every frame; dt and t in seconds
ctx.effect(() => {}); // runs again when what it reads changes; may return a cleanup
ctx.on(trigger, (payload) => {}); // react to a trigger
ctx.emit(trigger, payload); // send a trigger from this object

Most triggers have the same names as in events. Three input triggers with branches arrive in a script as separate triggers:

  • Key (on-key) arrives as on-keydown and on-keyup;
  • Press on the object (on-mouse) arrives as on-mousedown, on-mouseup and on-click;
  • Hover (on-hover) arrives as on-hover and on-blur.

The triggers for time, distance, visibility, signals and variables work only in events. In a script, use ctx.tick, ctx.subscribeGlobal and ctx.handleMessage instead.

These triggers give you useful data:

TriggerWhat 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-localizedwhere the visitor turned out to be

on-launch arrives as soon as your script starts, so you cannot miss it.

Find 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();

Create and remove objects​

ctx.create({ name: 'Bullet', parent, components: [] });
ctx.spawn(props.bulletModel, { parent });
ctx.destroy(entity);

spawn is the easy one. Give it a resource, and it adds the right components for you:

  • a model becomes a model object;
  • an image becomes a flat plane with the image on it;
  • a sound becomes an audio source.

Run built-in steps​

ctx.step('set_visibility', { visible: false }, { targets: [enemy] });

ctx.startTransition({ durationMs: 400, easing: 'ease-out' }, () => {
// changes made in here animate smoothly instead of jumping
});

targets picks the objects the step acts on, like the Target object field of an event. Without it, the step acts on this script's object.

Use a step before you write it yourself

ctx.step runs any of the built-in steps, the same ones your events use. Animations, state changes and scene transitions are one call away.

Send the visitor to another scene or space:

ctx.openScene(sceneOrId);
await ctx.openSpace(spaceRefOrId);
ctx.scenes();

Input​

There are two ways to read input.

Named actions use the project's key bindings. If a visitor changes their keys, your script still works:

ctx.input.pressed('jump');
ctx.input.justPressed('fire');
ctx.input.axis('moveX');

Raw keys read the keyboard directly. A press lasts exactly one frame, so read it inside ctx.tick:

ctx.keyboard.down('KeyW');
ctx.keyboard.press('Space');
ctx.keyboard.press('ArrowLeft', { every: 200 }); // auto-repeat, every 200 ms
ctx.keyboard.axis('KeyA', 'KeyD'); // -1, 0 or 1
Modifier keys work differently in each

A named action ignores modifiers it does not mention, so holding Shift to sprint does not stop "forward". A key trigger is the opposite: a modifier that is not part of its key must not be held.

When the window loses focus, all held keys are released, so no key gets stuck.

Camera​

ctx.camera.entity();           // the active camera object
ctx.camera.setActive(target); // switch cameras; null goes back to the default
ctx.camera.pose(); // { position, rotation, forward } in world space

The camera's transform works the same in every control mode. Write to it to move the camera. Read it to see where the controls put the camera.

In orbit and firstPerson modes, the controls set the rotation, so a rotation you write is replaced. The position you write is kept.

Raycasting​

A ray is a line from a point in a direction. Cast one to find what lies along it.

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 gives you the object, the distance, and the point and surface normal in world space. Hits are sorted nearest first, one per object.

Use await

The answer arrives in the next frame.

Invisible objects are skipped, and so are objects marked to be ignored, like reticles and gizmos. You get whatever is behind them instead of 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] });
Changing a dynamic body's transform does nothing

Physics controls where it is. Use teleport to place it, and impulses or forces to move it.

Skip your own body

When you cast a ray from inside your own body, pass skip. Otherwise the ray hits your own body every time.

Readings are about one frame behind. That is fine for "am I moving?", but not for exact calculations.

Audio​

ctx.audio.play(props.hitSound.id, { at: enemy, volume: 0.6, positional: true });

play takes the sound's id. For a sound setting from defineProps, that is props.hitSound.id.

Each call plays its own sound, so footsteps, hits and shots never cut each other off. The audio component plays one sound at a time and cuts itself off, so do not use it for effects.

Keep and share values​

Three ways to keep values and share them with other scripts:

// 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 }) => {});
Stored inAfter a scene changeAfter moving to another spaceAfter a reload
storeyesnono
globalsyesyesno
space-scoped globalsyesno — each space keeps its own, on purposeno
Closing the tab clears everything

None of these is saved between visits. If you need a value next time, send it somewhere yourself while the visitor is still connected.

Plugins​

Read and change a plugin's custom component, and use the triggers and steps a plugin adds:

const health = ctx.plugin('acme.health/health');   // '<plugin id>/<component id>'
for (const e of health.entities()) {
const max = health.props(e)!.max as number;
health.setState(e, { hp: max });
}

ctx.on('acme.patrol:reached', ({ point }) => {}); // a plugin's trigger
ctx.step('acme.patrol:pause', { seconds: 2 }); // a plugin's step

ctx.plugin has entities(), has(e), props(e), state(e), setState, setProps, setEnabled, add and remove. Its types come from the plugin, so your code editor knows the fields. See Runtime code.

Interface​

Read and change values on a UI card, and react to its buttons:

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: Recipes — complete scripts to copy.