Custom Behaviours
A Behaviour is a user-authored TypeScript class that runs per-frame logic on a single entity. Behaviours are attached to entities through the ScriptComponent, and are dispatched each frame by the ScriptBehaviourSystem.
Anatomy of a script
import {
Behaviour,
type FloatInput,
type DropdownInput,
} from "@relu-interactives/spatial-ecs";
export default class Spinner extends Behaviour {
data = {
speed: 1 as FloatInput,
axis: {
value: "y",
options: [
{ label: "X", value: "x" },
{ label: "Y", value: "y" },
{ label: "Z", value: "z" },
],
} as DropdownInput,
};
init() {
// Called once before the first update().
}
onUpdate() {
// Called every frame while the script is enabled.
const axis = this.data.axis.value as "x" | "y" | "z";
this.transform.rotation[axis] += this.data.speed * this.deltaTime;
}
dispose() {
// Optional cleanup when the entity is destroyed
// or the script is detached.
}
}IMPORTANT
For the runtime to recognize the class make sure the default export of the class subclasses Behaviour.
Available context
When init() / onUpdate() runs, the following fields are populated:
| Field | Description |
|---|---|
this.world | The owning World. |
this.entityId | Numeric entity id. |
this.entity | A WorldEntityView for the entity (id + components). |
this.transform | Mutable TransformComponent (position/rotation/scale). |
this.threeRef | The Object3DRef wrapping the live three.js object. |
this.data | The ScriptComponent.data payload (your inspector fields). |
this.deltaTime | Seconds since last frame. |
Helper methods
Behaviour exposes a small ergonomics surface so scripts rarely need to touch three.js directly:
- World-space math:
getWorldPosition,getWorldDirection,directionTo,distanceTo. - Smooth motion:
lerpTo,slerpTo,lookAt. - Component access:
getComponents,getComponent. - Hierarchy:
getEntity,getParent,getChildren.
See the full Behaviour API.
Lifecycle hooks
| Hook | When |
|---|---|
init() | Once, before the first onUpdate(). |
onUpdate() | Every frame while enabled. |
dispose() | When the script is detached or the entity destroyed. |
onSelect() / onDeselect() | Events fired when a user clicks or taps an object in the scene. |
onTargetFound() / onTargetLost() | Image-tracking target visibility changed. |
update(deltaTime) is implemented by the base class, do not override it. Override onUpdate() instead.
Where to put scripts
In a Relu Spatial project, scripts live under the project's assets/ directory. The editor compiles them through esbuild and exposes them via the script registry.
Inspector data binding
The data object is the bridge between the editor inspector and your script. Use typed input annotations FloatInput, ColorInput, DropdownInput, etc. to control which inspector control renders for each field:
import {
Behaviour,
type FloatInput,
type DropdownInput,
} from "@relu-interactives/spatial-ecs";
export default class Spinner extends Behaviour {
data = {
speed: 1 as FloatInput,
axis: {
value: "y",
options: [
{ label: "X", value: "x" },
{ label: "Y", value: "y" },
{ label: "Z", value: "z" },
],
} as DropdownInput,
};
onUpdate() {
const axis = this.data.axis.value as "x" | "y" | "z";
this.transform.rotation[axis] += this.data.speed * this.deltaTime;
}
}See Behaviour Data Binding for the full reference of all input types (TextInput, ColorInput, Vector2Input, Vector3Input, SliderInput, CheckboxInput, ToggleInput, AssetInput, and more).
See Accessing Components to read and mutate built-in components such as PhysicsComponent, AudioComponent, AnimationComponent, LightComponent, and MaterialComponent from script.
See Reading Input for the full keyboard and mouse input API, KeyboardKeys reference, and scripting examples.
Behaviours run at preview/runtime only
Behaviour scripts execute only at preview and runtime, never inside the editor. Gameplay logic, input handling, physics, and anything that should not run while authoring belongs in a Behaviour.
For a visual that must render and animate inside the editor (custom shaders, procedural geometry, grass, particle systems, anything the object palette can't produce), use a Script Object instead. A ScriptObject builds a THREE.Object3D that renders in the editor AND at runtime.

