Skip to main content

Runtime code

Give your plugin code that runs in the published project: systems that move objects, triggers and steps for events, and settings for the whole project.

Panels and custom components work in the editor. Runtime code runs wherever the project plays: in the published project on the web and in the app, in the editor's preview, and in playtests.

Add runtime code​

Point runtime in the manifest at a .ts or .js file of your plugin folder:

{
"id": "acme.spin",
"name": "Spin",
"version": "1.0.0",
"entry": "main.tsx",
"permissions": [],
"panels": [],
"components": [{ "id": "spin", "title": "Spin", "entry": "spin.tsx" }],
"runtime": "runtime/index.ts"
}

The file runs once when the project starts, next to the project's scripts. Register a system there. A system runs every frame:

// runtime/index.ts
import { Collection, System } from '@was/ecs';
import { TransformComponent, frameTime, pluginComponent } from '@was/engine';

class AcmeSpinSystem extends System.from('AcmeSpinSystem') {
execute() {
const { dt } = frameTime();
const spin = pluginComponent('acme.spin/spin', this.space);
for (const e of spin.entities()) {
const t = e.getComponent(TransformComponent)!;
const r = t.$data.rotation;
t.update({ rotation: { ...r, y: r.y + (spin.props(e)!.speed as number) * dt } });
}
}
}
Collection.registerSystem(AcmeSpinSystem);

This system turns every object that has the plugin's Spin component, at the speed set in the inspector.

Name systems after your plugin

System names are shared by everything in the project. If a name is already taken, your system does not start, and the error says which name. Start each name with your plugin's name, like AcmeSpinSystem.

What runtime code can use​

Runtime code can import what a script can: @was/ecs, @was/engine, @was/utils and @was/svdt. Only the entry file and the files it imports with ./ are shipped, up to 1 MB in total.

It has no page, no DOM, no three.js and no React. It reads and changes components, and the engine draws the result.

FunctionWhat it gives you
frameTime(){ dt, t }: the time since the last frame and since the start, in seconds
pluginComponent(key, space)your custom component on objects: read and change it
pluginEvents(id, space)fire your triggers and answer your steps
pluginSettings(id)the values the project chose for your settings
pluginContext(id, space)the script ctx: input, physics, rays, sound, globals, multiplayer

key is <plugin id>/<component id>, like acme.spin/spin. id is the plugin's id from the manifest.

Your component​

pluginComponent(key, space) gives you:

MethodWhat it does
entities()the objects that have the component
has(e)whether the object has it switched on
props(e)the values from the inspector, with defaults
state(e)the component's own state
setState(e, patch)changes the state
setProps(e, patch)changes the inspector values
setEnabled(e, on)switches the component on or off
add(e, props)adds the component to an object
remove(e)removes it

Scripts get the same thing as ctx.plugin(key).

The script context​

pluginContext(id, space) returns the same ctx a script gets, for input, physics, raycasts, audio, globals and multiplayer. It is null until the project's scripts start, and it can change when the world does, so ask for it when you need it. ctx.on and ctx.tick add a new handler on every call, so subscribe once for each context.

Triggers and steps​

A plugin with runtime code can add its own triggers and steps to the events panel. People then use them without code, like the built-in ones. They appear under your plugin's name, in projects that have the plugin.

Declare them in the manifest:

"triggers": [
{ "id": "reached", "title": "Reached a point",
"params": { "point": { "type": "number", "default": -1, "any": -1, "label": "Point" } } }
],
"steps": [
{ "id": "pause", "title": "Pause",
"params": { "seconds": { "type": "number", "default": 0, "min": 0 } } }
]
  • Ids are in kebab-case. The full name is <plugin id>:<id>, like acme.patrol:reached.
  • params use the field types of defineProps: number, string, boolean, color, select, entity and resource. The panel draws a form for them.
  • A trigger's params filter. An event set to point 2 runs only when the trigger fires with point: 2. A param left empty, or set to its any value, lets every firing through.

Fire the triggers and answer the steps from your runtime code:

import { pluginEvents } from '@was/engine';

class AcmePatrolSystem extends System.from('AcmePatrolSystem') {
events = pluginEvents('acme.patrol', this.space);
off = this.events.onStep<{ seconds: number }>('pause', (targets, params) => {
// stop the objects in `targets` for params.seconds
});

execute() {
// when a guard reaches a point:
// this.events.fire('reached', guard, { point: 2 });
}

dispose() {
this.off();
}
}
  • fire(id, object, payload) runs the events that use this trigger on that object and on the objects above it. Without an object, it fires everywhere. Scripts hear it as ctx.on('acme.patrol:reached', payload => …).
  • onStep(id, handler) runs when an event runs your step. A script can run it too, with ctx.step('acme.patrol:pause', { seconds: 2 }). Subscribe once, and call the returned function in dispose().
  • If the plugin is removed from a project, its triggers never fire and its steps are skipped.

Project settings​

Some values belong to the whole project: a tempo, a difficulty, a colour. Declare them under settings: up to 24 fields of the types number, string, boolean, color and select.

"settings": {
"tempo": { "type": "number", "default": 1, "min": 0, "label": "Tempo" }
}

People set them in Project settings, in a section with your plugin's name.

  • Runtime code reads them with pluginSettings<{ tempo: number }>('acme.patrol'). You get every field: the project's value, or the default. The values are ready before your entry runs and do not change while the project plays.
  • A published project uses the values it was published with.
  • Your panel reads and changes them with editor.pluginSettings.get() and editor.pluginSettings.set({ tempo: 2 }). This needs the settings:write permission.

How the code reaches a project​

Runtime code runs only in projects the plugin is added to.

  • Adding the plugin fixes the version. The project keeps the code it had at that moment. Previews, playtests and publications run that code, even after you publish a newer one.
  • Updating is a choice. When there is newer code, the plugin shows update available in the Plugins panel. Update in project switches the project to it.
  • Your own projects follow you. If you added your own plugin to a project, the project runs each version you save.
  • A publication keeps its code. A published project runs the code it was published with, until it is published again.

Titles, descriptions and labels in triggers, steps and settings can come from your locale files, written as ${…}.

Subscribers-only plugins​

A plugin's runtime code goes to everyone who opens the published project, and anyone can read it in their browser. This is also true for a Subscribers only plugin. The subscription decides who can add the plugin to a project, update it and publish with it. It does not hide the code.

If the subscription ends, a project that uses the plugin cannot be published again. Renew the subscription or remove the plugin. A project that is already published keeps working.

For developers​

  • Runtime code runs in the project's script worker, together with the project's scripts.
  • The version a project keeps is a fingerprint of the runtime files, the triggers, the steps and the settings. A different fingerprint shows update available.

Next: Keyboard shortcuts — the keys that make editing faster.