First script
Write a small script that spins an object when you tap it. Then add settings to the inspector and see what a script can use.
A script is a TypeScript file that you attach to an object. It runs in the player. Every object that uses the script gets its own copy.
Here is a complete script. Tap the object, and it starts spinning:
import { TransformComponent } from '@was/engine';
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: clean up when this object goes away
};
});
The script has four parts:
| Part | What it does |
|---|---|
init | Starts the script and gives you ctx, your access to the running scene. Everything starts here. |
defineProps | Adds settings to the inspector. You fill them in for each object. |
ctx.on | Reacts to a trigger, such as a tap or a collision. |
ctx.tick | Runs every frame. dt is the time since the last frame, in seconds. |
init and defineProps are always available. You never import them. Component classes, such as
TransformComponent, come from @was/engine.
Read and change 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 mistakes catch almost everyone.
update keeps the fields you leave out, such as rotation. But a value you pass replaces the old
one whole. So update({ position: { y: 2 } }) also sets x and z back to their default, zero.
Copy the values you want to keep with ...:
transform.update({ position: { ...transform.$data.position, y: 2 } });
material.update({ color }) does nothing, because color is inside a slot. Change the slot
instead:
material.update({
materials: [{ ...material.$data.materials[0], color: '#ff0000' }],
});
Never change $data directly. It looks like it worked, but the change is lost.
Settings in the inspector
With defineProps, you can put one script on ten objects and set it up differently on each. No
copying and pasting.
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 |
Useful options:
labelandhelp— the text shown in the panel.minandmax— hard limits.softMax— where the slider ends. People can still type a larger number.showWhen— shows a field only when another field has a certain value.group— keeps a long list of settings tidy.
defineProps out in fullWrite your settings right inside the call, or the inspector cannot show them. Do not build them from variables or return them from a function.
Your script always gets the current value: props.speed is whatever the inspector shows right
now.
Imports
import { TransformComponent } from '@was/engine';
import helpers from 'Scripts/helpers';
You can import the engine's component classes. You can also import other resources by their path:
- another script gives you what it exports;
- a patch gives you its code, the same as the Code panel shows;
- anything else gives you a reference,
{ id, type }. Pass it toctx.spawn, or pass itsidtoctx.audio.play.
If you rename or move a resource, these imports update automatically.
What a script cannot use
A heavy script cannot slow down the picture. These are not available to scripts:
window,document, the DOM;fetch,localStorage, or any network access;- rendering libraries — you change the scene through components, not by drawing;
- browser timers — use
ctx.tickinstead.
Use these instead:
- for interface — UI cards;
- for sound —
ctx.audio; - for keeping values —
ctx.storeand globals.
When a script starts and stops
A script starts when its object is live on screen: nothing above it is disabled, and its scene is active. It stops when that is no longer true.
on-launch fires as soon as the script starts, so you never miss it.
The function you return from init runs when the script stops. Use it to clean up: unsubscribe,
stop sounds, clear state.
The editor shows your scene but does not run any logic. Edit the script, then open Preview. There is no hot reload: a running experience picks up your changes the next time it restarts.
Next: Where scripts live — which level to attach a script to.