Accessing Components
Every entity in the ECS world carries a set of components — typed data containers that describe its appearance, physics, audio, animation, and more. From inside a Behaviour script you can read and mutate any component on the entity at runtime using this.getComponent(ComponentClass).
The getComponent API
getComponent<T extends Component>(componentClass: ComponentClass<T>): T | nullReturns the component instance of the requested type attached to the same entity as the script, or null if the entity does not have that component. Always null-check the result — components are optional and may not be present on every object.
The recommended pattern is to cache the reference in onInit so you avoid the lookup overhead every frame:
import { Behaviour } from "@relu-interactives/spatial-ecs";
import { PhysicsComponent } from "@relu-interactives/spatial-ecs/components";
export default class Example extends Behaviour {
private physics: PhysicsComponent | null = null;
onInit() {
this.physics = this.getComponent(PhysicsComponent);
}
onUpdate() {
if (!this.physics) return;
// use this.physics …
}
}
this.transformis a pre-cachedTransformComponentalready available on everyBehaviour— no need to callgetComponent(TransformComponent)manually.
Component examples
TransformComponent
Position, rotation (radians, Euler XYZ), and scale in local space. Available on every entity as this.transform.
import { Behaviour } from "@relu-interactives/spatial-ecs";
import { TransformComponent } from "@relu-interactives/spatial-ecs/components";
import * as THREE from "three";
export default class MoveUp extends Behaviour {
onUpdate() {
// Mutate position directly — picked up by TransformSyncSystem each frame
this.transform.position.y += this.deltaTime;
// Or use the typed helpers for Vector3 / Euler / Quaternion inputs
this.transform.setPosition(new THREE.Vector3(0, 2, 0));
this.transform.setRotation(new THREE.Euler(0, Math.PI / 2, 0));
this.transform.setScale(new THREE.Vector3(2, 2, 2));
}
}PhysicsComponent
Rigidbody simulation and collider. Exposes the live Rapier rigidBody and collider handles along with their authoring state (rigidbody, collider).
import { Behaviour, type FloatInput } from "@relu-interactives/spatial-ecs";
import { PhysicsComponent } from "@relu-interactives/spatial-ecs/components";
import { Vector3 } from "three";
export default class Jumper extends Behaviour {
data = {
jumpForce: 8 as FloatInput,
};
private physics: PhysicsComponent | null = null;
onInit() {
this.physics = this.getComponent(PhysicsComponent);
}
onUpdate() {
if (!this.physics?.rigidBody) return;
// Apply an upward impulse every frame (combine with input checks in practice)
this.physics.rigidBody.addForce(
new Vector3(0, this.data.jumpForce, 0),
true, // wake the body if sleeping
);
// Change mass at runtime
this.physics.rigidbody.mass = 2;
// Lock rotation so the body can't tip over
this.physics.rigidbody.lockRotations = true;
}
}Key fields:
| Field | Type | Description |
|---|---|---|
rigidBody | RapierRigidBody | null | Live physics body handle. null until simulation starts. |
collider | RapierCollider | null | Live collider handle. |
rigidbody | RigidbodyState | Serialized authoring state (type, mass, damping, gravity scale, lock flags). |
colliderState | ColliderState | Serialized collider state (shape, size, offset, friction, restitution, sensor). |
AudioComponent
Plays a stereo or 3D positional audio clip.
import { Behaviour, type FloatInput } from "@relu-interactives/spatial-ecs";
import { AudioComponent } from "@relu-interactives/spatial-ecs/components";
export default class SoundController extends Behaviour {
data = {
volume: 1 as FloatInput,
};
private audio: AudioComponent | null = null;
onInit() {
this.audio = this.getComponent(AudioComponent);
}
onUpdate() {
if (!this.audio) return;
// Adjust volume at runtime
this.audio.options.volume = this.data.volume;
// Trigger playback
if (!this.audio.isPlaying) {
this.audio.play();
}
}
}Key fields / methods:
| Member | Description |
|---|---|
path | Source URL or localasset:// path of the audio file. |
options.volume | Linear gain [0, 1]. |
options.loop | Whether the clip loops. |
options.pitch | Playback rate multiplier. |
options.positional | Enable 3D spatial audio. |
isPlaying | Runtime-only — true when actively playing. |
play() | Start or resume playback. |
pause() | Pause mid-playback (position is preserved). |
stop() | Stop and reset to the beginning. |
AnimationComponent
Clip catalog and state-machine playback. Use switchState to trigger named transitions.
import { Behaviour, type DropdownInput } from "@relu-interactives/spatial-ecs";
import { AnimationComponent } from "@relu-interactives/spatial-ecs/components";
export default class AnimationController extends Behaviour {
data = {
state: {
value: "idle",
options: [
{ label: "Idle", value: "idle" },
{ label: "Walk", value: "walk" },
{ label: "Run", value: "run" },
],
} as DropdownInput,
};
private anim: AnimationComponent | null = null;
onInit() {
this.anim = this.getComponent(AnimationComponent);
if (this.anim) {
this.anim.switchState(this.data.state.value);
}
}
onUpdate() {
if (!this.anim) return;
// Slow down playback at runtime
this.anim.timeScale = 0.5;
// Switch to a different clip state
if (this.anim.activeStateName !== this.data.state.value) {
this.anim.switchState(this.data.state.value);
}
}
}Key fields / methods:
| Member | Description |
|---|---|
activeStateName | Name of the currently playing state. |
timeScale | Playback speed multiplier (1 = real-time). |
paused | Pause all playback. |
crossFadeDuration | Default blend time when switching states (seconds). |
switchState(name) | Transition to a named state (cross-fades). |
addState(entry) | Register a new state at runtime. |
clipCatalog | Array of all loaded clips with metadata. |
LightComponent
Color, intensity, and type-specific light parameters. The live light property is the underlying three.js object.
import { Behaviour, type FloatInput, type ColorInput } from "@relu-interactives/spatial-ecs";
import { LightComponent } from "@relu-interactives/spatial-ecs/components";
export default class LightPulse extends Behaviour {
data = {
minIntensity: 0.5 as FloatInput,
maxIntensity: 3 as FloatInput,
speed: 2 as FloatInput,
color: "#ffeeaa" as ColorInput,
};
private light: LightComponent | null = null;
private t = 0;
onInit() {
this.light = this.getComponent(LightComponent);
if (this.light) {
this.light.setValues({ color: this.data.color });
}
}
onUpdate() {
if (!this.light) return;
this.t += this.deltaTime * this.data.speed;
const intensity =
this.data.minIntensity +
(Math.sin(this.t) * 0.5 + 0.5) *
(this.data.maxIntensity - this.data.minIntensity);
// setValues patches only the fields you provide
this.light.setValues({ intensity });
}
}Key fields / methods:
| Member | Description |
|---|---|
light | Live THREE.Light instance placed in the scene. |
type | Three.js class name ("DirectionalLight", "PointLight", "SpotLight", etc.). |
values.color | Hex color string. |
values.intensity | Brightness multiplier. |
values.castShadow | Whether the light casts shadows. |
values.distance | Max range for point/spot lights. |
values.angle | Cone half-angle in radians (SpotLight). |
setValues(patch) | Update any subset of values and sync to the live light. |
MaterialComponent
Per-slot PBR material state and texture maps.
import { Behaviour, type ColorInput, type FloatInput } from "@relu-interactives/spatial-ecs";
import { MaterialComponent } from "@relu-interactives/spatial-ecs/components";
export default class MaterialSwap extends Behaviour {
data = {
color: "#ff4400" as ColorInput,
roughness: 0.3 as FloatInput,
metalness: 0.8 as FloatInput,
};
private mat: MaterialComponent | null = null;
onInit() {
this.mat = this.getComponent(MaterialComponent);
}
onUpdate() {
if (!this.mat) return;
// applyValues patches one or more fields on a slot (default slot index 0)
this.mat.applyValues(0, {
color: this.data.color,
roughness: this.data.roughness,
metalness: this.data.metalness,
});
}
}Key fields / methods:
| Member | Description |
|---|---|
values | Array of MaterialValueState objects, one per mesh slot. |
values[i].color | Base color as #rrggbb. |
values[i].roughness | PBR roughness [0, 1]. |
values[i].metalness | PBR metalness [0, 1]. |
values[i].opacity | Surface opacity [0, 1]. Pair with transparent: true. |
values[i].emissive | Emissive color as #rrggbb. |
values[i].emissiveIntensity | Emissive multiplier. |
values[i].wireframe | Render as wireframe. |
applyValues(slotIndex, patch) | Patch fields and sync to the live three.js material. |
VideoComponent
Video-texture playback with optional green-screen keying.
import { Behaviour } from "@relu-interactives/spatial-ecs";
import { VideoComponent } from "@relu-interactives/spatial-ecs/components";
export default class VideoController extends Behaviour {
private video: VideoComponent | null = null;
onInit() {
this.video = this.getComponent(VideoComponent);
}
onUpdate() {
if (!this.video) return;
// Adjust volume while playing
this.video.options.volume = 0.5;
if (!this.video.isPlaying) {
this.video.play();
}
}
}Key fields / methods:
| Member | Description |
|---|---|
path | Source URL or localasset:// path of the video file. |
options.loop | Loop playback. |
options.volume | Linear gain [0, 1]. |
options.isGreenScreen | Enable chroma-key compositing. |
options.backgroundColor | Color to key out. |
videoElement | Underlying HTMLVideoElement. null until built. |
isPlaying | Runtime-only — true when playing. |
play() | Start or resume playback. |
pause() | Pause mid-playback. |
stop() | Stop and reset. |
Querying components on other entities
getComponent only reaches the entity the script is attached to. To read components on another entity, use this.world.getEntityByName(name) for the simplest lookup:
import { Behaviour } from "@relu-interactives/spatial-ecs";
import { LightComponent } from "@relu-interactives/spatial-ecs/components";
export default class RemoteControl extends Behaviour {
onUpdate() {
// Find an entity by name, then read its LightComponent
const entity = this.world.getEntityByName("Sun");
if (!entity) return;
const light = this.world.getComponent(entity.id, LightComponent);
if (light) light.setValues({ intensity: 2 });
}
}You can also use this.world.getEntityById(id) when you already have the entity ID, or this.world.getAllEntities() to iterate over every entity in the scene.
See World API for the full set of entity and component query methods.
Adding and removing components at runtime
You can add or remove components on any entity from a Behaviour script at runtime using this.world or through ObjectManagementSystem.
world.addComponent — attach a data component
Use world.addComponent to attach a simple, data-only component that carries no runtime handles:
import { Behaviour } from "@relu-interactives/spatial-ecs";
import { Name } from "@relu-interactives/spatial-ecs/components";
export default class Tagger extends Behaviour {
protected init() {
const entity = this.world.getEntityByName("Target");
if (!entity) return;
// Give the entity a display name
this.world.addComponent(entity.entityId, new Name({ value: "Tagged" }));
}
}world.removeComponent — detach a component by class
import { Behaviour } from "@relu-interactives/spatial-ecs";
import { PhysicsComponent } from "@relu-interactives/spatial-ecs/components";
export default class Freezer extends Behaviour {
protected init() {
// Remove physics from this entity to freeze it in place
this.world.removeComponent(this.entityId, PhysicsComponent);
}
}oms.createComponentNow — factory-initialized components
For components that require full runtime setup — Rapier physics bodies, Web Audio nodes, Three.js material instances, animation clip catalogs — use createComponentNow on ObjectManagementSystem. It calls the same registered factory used during scene construction, ensuring all runtime handles are correctly wired:
import { Behaviour } from "@relu-interactives/spatial-ecs";
export default class DynamicPhysics extends Behaviour {
protected init() {
const oms = this.world.getObjectManagementSystem();
if (!oms) return;
// Add a physics component with full Rapier rigidbody and collider setup
oms.createComponentNow("physics", this.world, this.entityId, {
rigidbody: { type: "dynamic", mass: 1, gravityScale: 1 },
collider: { shape: "box" },
});
}
}Registered component factory kinds:
| Kind | Component |
|---|---|
"animation" | AnimationComponent |
"audio" | AudioComponent |
"camera" | CameraComponent |
"physics" | PhysicsComponent |
"environment" | EnvironmentComponent |
"entityType" | EntityTypeComponent |
"image" | ImageComponent |
"imageTarget" | ImageTargetComponent |
"imageTargetAnchor" | ImageTargetAnchorComponent |
"light" | LightComponent |
"material" | MaterialComponent |
"mesh" | MeshComponent |
"meshGeometry" | MeshGeometryComponent |
"model" | ModelComponent |
"name" | Name |
"object3DRef" | Object3DRef |
"parentId" | ParentId |
"postprocessing" | PostprocessingComponent |
"script" | ScriptComponent |
"sprite" | SpriteComponent |
"transform" | TransformComponent |
"video" | VideoComponent |
"instancedMesh" | InstancedMeshComponent |
Decision guide:
| Scenario | Use |
|---|---|
| Tagging with name, type, or other pure-data component | world.addComponent(id, new ComponentClass(...)) |
| Adding physics with Rapier body/collider | oms.createComponentNow("physics", world, id, config) |
| Adding audio with Web Audio node | oms.createComponentNow("audio", world, id, path, options, ctx) |
| Adding a material with Three.js material instance | oms.createComponentNow("material", world, id, values) |
| Detaching any component | world.removeComponent(id, ComponentClass) |
Post-spawn example: add physics to a spawned cube
import { Behaviour } from "@relu-interactives/spatial-ecs";
export default class PhysicsCube extends Behaviour {
private spawnedId: number | null = null;
protected init() {
const oms = this.world.getObjectManagementSystem();
if (!oms) return;
oms.requestCreate("cube", {
name: "PhysicsCube",
transform: { position: { x: 0, y: 3, z: 0 } },
}, (entityId) => {
this.spawnedId = entityId;
// Physics factory wires up Rapier body + collider automatically
oms.createComponentNow("physics", this.world, entityId, {
rigidbody: { type: "dynamic", mass: 1, gravityScale: 1 },
collider: { shape: "box" },
});
});
}
dispose(world: any) {
if (this.spawnedId !== null) {
this.world.getObjectManagementSystem()?.deleteEntity(this.world, this.spawnedId, true);
}
}
}For the full guide covering setParent, async loading patterns, and complete worked examples, see Runtime Entity Management.
See also
- Custom Behaviours — script anatomy and lifecycle
- Behaviour Data Binding — exposing fields to the inspector
- World API — querying entities and components across the scene
- Runtime Entity Management — addComponent, createComponentNow, setParent, deleteEntity

