Skip to content

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

ts
getComponent<T extends Component>(componentClass: ComponentClass<T>): T | null

Returns 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:

ts
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.transform is a pre-cached TransformComponent already available on every Behaviour — no need to call getComponent(TransformComponent) manually.


Component examples

TransformComponent

Position, rotation (radians, Euler XYZ), and scale in local space. Available on every entity as this.transform.

ts
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).

ts
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:

FieldTypeDescription
rigidBodyRapierRigidBody | nullLive physics body handle. null until simulation starts.
colliderRapierCollider | nullLive collider handle.
rigidbodyRigidbodyStateSerialized authoring state (type, mass, damping, gravity scale, lock flags).
colliderStateColliderStateSerialized collider state (shape, size, offset, friction, restitution, sensor).

AudioComponent

Plays a stereo or 3D positional audio clip.

ts
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:

MemberDescription
pathSource URL or localasset:// path of the audio file.
options.volumeLinear gain [0, 1].
options.loopWhether the clip loops.
options.pitchPlayback rate multiplier.
options.positionalEnable 3D spatial audio.
isPlayingRuntime-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.

ts
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:

MemberDescription
activeStateNameName of the currently playing state.
timeScalePlayback speed multiplier (1 = real-time).
pausedPause all playback.
crossFadeDurationDefault blend time when switching states (seconds).
switchState(name)Transition to a named state (cross-fades).
addState(entry)Register a new state at runtime.
clipCatalogArray of all loaded clips with metadata.

LightComponent

Color, intensity, and type-specific light parameters. The live light property is the underlying three.js object.

ts
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:

MemberDescription
lightLive THREE.Light instance placed in the scene.
typeThree.js class name ("DirectionalLight", "PointLight", "SpotLight", etc.).
values.colorHex color string.
values.intensityBrightness multiplier.
values.castShadowWhether the light casts shadows.
values.distanceMax range for point/spot lights.
values.angleCone 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.

ts
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:

MemberDescription
valuesArray of MaterialValueState objects, one per mesh slot.
values[i].colorBase color as #rrggbb.
values[i].roughnessPBR roughness [0, 1].
values[i].metalnessPBR metalness [0, 1].
values[i].opacitySurface opacity [0, 1]. Pair with transparent: true.
values[i].emissiveEmissive color as #rrggbb.
values[i].emissiveIntensityEmissive multiplier.
values[i].wireframeRender as wireframe.
applyValues(slotIndex, patch)Patch fields and sync to the live three.js material.

VideoComponent

Video-texture playback with optional green-screen keying.

ts
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:

MemberDescription
pathSource URL or localasset:// path of the video file.
options.loopLoop playback.
options.volumeLinear gain [0, 1].
options.isGreenScreenEnable chroma-key compositing.
options.backgroundColorColor to key out.
videoElementUnderlying HTMLVideoElement. null until built.
isPlayingRuntime-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:

ts
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:

ts
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

ts
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:

ts
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:

KindComponent
"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:

ScenarioUse
Tagging with name, type, or other pure-data componentworld.addComponent(id, new ComponentClass(...))
Adding physics with Rapier body/collideroms.createComponentNow("physics", world, id, config)
Adding audio with Web Audio nodeoms.createComponentNow("audio", world, id, path, options, ctx)
Adding a material with Three.js material instanceoms.createComponentNow("material", world, id, values)
Detaching any componentworld.removeComponent(id, ComponentClass)

Post-spawn example: add physics to a spawned cube

ts
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