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
| Name | What it is |
|---|---|
ctx.entity | the object this script is attached to |
ctx.scene | the scene the object is in |
ctx.space | the 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 ason-keydownandon-keyup; - Press on the object (
on-mouse) arrives ason-mousedown,on-mouseupandon-click; - Hover (
on-hover) arrives ason-hoverandon-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:
| Trigger | What 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 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.
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.
Navigation
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
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.
awaitThe 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] });
Physics controls where it is. Use teleport to place it, and impulses or forces to move it.
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 in | After a scene change | After moving to another space | After a reload |
|---|---|---|---|
store | yes | no | no |
| globals | yes | yes | no |
| space-scoped globals | yes | no — each space keeps its own, on purpose | no |
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.