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.
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.
| Function | What 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:
| Method | What 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>, likeacme.patrol:reached. paramsuse the field types ofdefineProps:number,string,boolean,color,select,entityandresource. 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 itsanyvalue, 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 asctx.on('acme.patrol:reached', payload => …).onStep(id, handler)runs when an event runs your step. A script can run it too, withctx.step('acme.patrol:pause', { seconds: 2 }). Subscribe once, and call the returned function indispose().- 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()andeditor.pluginSettings.set({ tempo: 2 }). This needs thesettings:writepermission.
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.