Your first script
A script is a TypeScript file you attach to an object. It runs in the player, once for every object that uses it.
Here is a complete one — tap the object and it starts spinning:
init((ctx) => {
const props = defineProps({
speed: { type: 'number', default: 1, min: 0, label: 'Speed' },
});
let spinning = false;
ctx.on('on-click', () => {
spinning = !spinning;
});
ctx.tick((dt) => {
if (!spinning) return;
const t = ctx.entity.getComponent(TransformComponent);
t?.update({
rotation: { ...t.$data.rotation, y: t.$data.rotation.y + props.speed * dt },
});
});
return () => {
// optional: tidy up when this object goes away
};
});
Four things are going on:
init | receives ctx, your handle on the running scene. Everything starts here |
defineProps | declares settings the editor shows in the inspector, filled in per object |
ctx.on | subscribes to a trigger — the same triggers events and patches use |
ctx.tick | runs every frame, with dt in seconds |
init and defineProps are always available — you never import them.
Reading and writing objects
const transform = ctx.entity.getComponent(TransformComponent);
transform.position; // read one value
transform.$data; // read the whole thing as a plain object
transform.update({ … }); // write
Two traps, and they are the two that catch everybody:
update({ position: { y: 2 } }) also sets x and z to zero — you handed it a whole new
position with only y filled in.
Spread what you want to keep:
transform.update({ position: { ...transform.$data.position, y: 2 } });
material.update({ color }) does nothing at all, because color lives inside a slot rather
than at the top. Write the slot:
material.update({
materials: [{ ...material.$data.materials[0], color: '#ff0000' }],
});
And never assign into $data directly. It looks like it worked, and the change is dropped.
Settings in the inspector
defineProps is what makes a script worth reusing: the same script on ten objects, configured
differently on each, with no copy-paste.
const props = defineProps({
speed: { type: 'number', default: 1, min: 0, softMax: 10, suffix: 'm/s' },
target: { type: 'entity', label: 'Look at' },
sound: { type: 'resource', resource: 'audio' },
mode: { type: 'select', options: ['chase', 'patrol'], default: 'patrol' },
});
| Type | The editor shows | Your script gets |
|---|---|---|
number | a field or slider | a number |
string | a text field | a string |
boolean | a switch | a boolean |
color | a colour picker | a colour string |
select | a dropdown | one of your options |
entity | an object picker | the object itself, ready to use |
scene | a scene picker | the scene object |
resource | a resource picker | a reference you can pass along |
array | a list | an array |
group | a titled block | a nested object |
Worth knowing: label and help for the panel, min and max for genuine limits, softMax
for where a slider ends without forbidding larger typed values, showWhen to reveal a field
only when another has a particular value, and group to keep a long list tidy.
defineProps has to be written out literallyThe editor reads your settings without running the script, so it needs to see them directly — not built from variables or returned by a function.
Values are read fresh every time, so props.speed always reflects what is in the inspector
right now.
Importing things
import { TransformComponent } from '@was/engine';
import helpers from 'Scripts/helpers';
You can import the engine's component classes, and other resources by path — another script
gives you its exports, a patch gives you its compiled module, and anything else gives you a
reference you can hand to ctx.spawn or ctx.audio.play.
Rename or move a resource and these imports update themselves.
What the sandbox gives you — and does not
Scripts run isolated from the page, which keeps a heavy script from stalling rendering. So these are not available:
window,document, the DOM;fetch,localStorage, network access of any kind;- any rendering library — you change the scene through components, not by drawing;
- browser timers — use
ctx.tickinstead.
What you use instead: UI cards for interface, ctx.audio for
sound, ctx.store and globals for keeping things.
When your script starts and stops
An instance is created when its object is live and on screen — nothing above it disabled, its scene active — and destroyed when that stops being true.
on-launch fires as soon as it is created, so you never miss it. The function you return from
init is your cleanup: unsubscribe, stop sounds, clear state.
The editor draws your scene but does not execute logic. Edit, then open Preview. A running experience picks up an edited script when it next restarts.
Next: Where a script lives