Skip to main content

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:

Scripts/spinner.ts
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:

PartWhat it does
initStarts the script and gives you ctx, your access to the running scene. Everything starts here.
definePropsAdds settings to the inspector. You fill them in for each object.
ctx.onReacts to a trigger, such as a tap or a collision.
ctx.tickRuns 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.

Changing one nested value clears the rest

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 } });
A material is a list of slots

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' },
});
TypeThe editor showsYour script gets
numbera field or slidera number
stringa text fielda string
booleana switcha boolean
colora colour pickera colour string
selecta dropdownone of your options
entityan object pickerthe object itself, ready to use
scenea scene pickerthe scene object
resourcea resource pickera reference you can pass along
arraya listan array
groupa titled blocka nested object

Useful options:

  • label and help — the text shown in the panel.
  • min and max — 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.
Write defineProps out in full

Write 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 to ctx.spawn, or pass its id to ctx.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.tick instead.

Use these instead:

  • for interface — UI cards;
  • for sound — ctx.audio;
  • for keeping values — ctx.store and 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.

Scripts do not run in the editor

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.