Behaviour Data Binding
The ScriptComponent.data field is the bridge between the editor inspector and your Behaviour subclass. Anything you place on data is:
- Saved with the project.
- Rendered as a live inspector control in the editor.
- Available at runtime as
this.datainside the script.
The type annotation on each field tells the editor which control to render. Import the input types from @relu-interactives/spatial-ecs and use them as as casts on the initial values in your data object.
Input types
| Type | Inspector control | Runtime value |
|---|---|---|
FloatInput | Number input | number |
TextInput | Text input | string |
ColorInput | Color picker | string (hex, e.g. "#ff0000") |
Vector2Input | XY number pair | { x: number; y: number } |
Vector3Input | XYZ number triple | { x: number; y: number; z: number } |
SliderInput | Range slider | { min, max, step, value } - read field.value at runtime |
DropdownInput | Select dropdown | { options: DropdownOption[]; value: string } - read field.value |
CheckboxInput | Checkbox | { type: "checkbox"; value: boolean } - read .value |
ToggleInput | Toggle switch | boolean |
AssetInput | Asset file picker | { path: string; type: string } - read field.path |
EntityInput | Entity picker | { type: "entity"; value: WorldEntityView | null } - read field.value |
ComponentInput | Entity + component picker | { type: "component"; componentKind?: ComponentKind; value: Component | null } - read field.value |
Declaring inspector fields
Import the input types you need alongside Behaviour, then declare them directly on the data class property using as casts:
import {
Behaviour,
type FloatInput,
type TextInput,
type ColorInput,
type Vector2Input,
type Vector3Input,
type SliderInput,
type DropdownInput,
type CheckboxInput,
type ToggleInput,
type AssetInput,
type EntityInput,
type ComponentInput,
} from "@relu-interactives/spatial-ecs";
export default class Demo extends Behaviour {
data = {
speed: 1 as FloatInput,
label: "Hello" as TextInput,
tint: "#ffffff" as ColorInput,
offset: { x: 0, y: 0 } as Vector2Input,
origin: { x: 0, y: 0, z: 0 } as Vector3Input,
range: { min: 0, max: 10, step: 0.1, value: 5 } as SliderInput,
axis: {
value: "y",
options: [
{ label: "X", value: "x" },
{ label: "Y", value: "y" },
{ label: "Z", value: "z" },
],
} as DropdownInput,
active: true as ToggleInput,
visible: { type: "checkbox", value: true } as CheckboxInput,
texture: { path: "", type: "image" } as AssetInput,
target: { type: "entity", value: null } as EntityInput,
camera: { type: "component", componentKind: "Camera", value: null } as ComponentInput,
};
}Each field in data maps directly to a live inspector control. The script above produces the following inspector panel:

Reading values at runtime
Most types map directly to their underlying primitive. The exceptions are SliderInput, DropdownInput, AssetInput, EntityInput, and ComponentInput - read the nested value:
onUpdate() {
// Primitives — use directly
this.transform.rotation.y += this.data.speed * this.deltaTime;
// SliderInput → read .value
const volume = this.data.range.value;
// DropdownInput → read .value
const axis = this.data.axis.value as "x" | "y" | "z";
// AssetInput → read .path
const src = this.data.texture.path;
// EntityInput → cast to WorldEntityView | null
const entity = this.data.target.value as WorldEntityView | null;
// ComponentInput → cast to the concrete component type
const cam = this.data.camera.value as CameraComponent | null;
}Practical example (spinner with inspector controls)
import {
Behaviour,
type FloatInput,
type ColorInput,
type DropdownInput,
type ToggleInput,
} from "@relu-interactives/spatial-ecs";
export default class Spinner extends Behaviour {
data = {
speed: 1 as FloatInput,
color: "#00aaff" as ColorInput,
enabled: true as ToggleInput,
axis: {
value: "y",
options: [
{ label: "X", value: "x" },
{ label: "Y", value: "y" },
{ label: "Z", value: "z" },
],
} as DropdownInput,
};
onUpdate() {
if (!this.data.enabled) return;
const axis = this.data.axis.value as "x" | "y" | "z";
this.transform.rotation[axis] += this.data.speed * this.deltaTime;
}
}Defaulting fields
Defaults are set inline on the data property. When a saved project reloads, saved values take precedence, the inline defaults only apply when a field has never been saved. This means:
- Adding a new field with an inline default is safe, existing saves will pick it up on next load.
- The inspector always has a valid initial value when the script is first attached.
- For
SliderInputandDropdownInput, the full object (includingoptions) must be present in the inline default, the editor readsoptionsto build the control.
Persistence
ScriptComponent.data is serialized and saved with the project. On reload, your data payload is restored verbatim. If you rename or restructure a field, migrate it inside init():
init() {
// migrate renamed field
if ((this.data as any).oldSpeed !== undefined) {
this.data.speed = (this.data as any).oldSpeed;
delete (this.data as any).oldSpeed;
}
}
