Skip to content

The World

Every Behaviour script has a this.world reference to the owning World. It is the runtime gateway for finding entities, querying components, reading input, and driving the scene from your scripts.

TIP

Full TypeScript signatures live in the auto-generated reference: World API.

This page summarises the methods most useful from a Behaviour and shows how to combine them.

Inside a Behaviour

ts
import { Behaviour, TransformComponent } from "@relu-interactives/spatial-ecs";

export default class Spinner extends Behaviour {
  onUpdate() {
    // `this.world` is refreshed every frame
    const transform = this.world.getComponent(this.entity.entityId, TransformComponent);
    if (transform) {
      transform.rotation.y += this.deltaTime;
    }
  }
}

Method reference

Entity lifecycle

MethodPurpose
createEntity()Allocate a new entity id.
destroyEntity(entityId)Remove an entity and all of its components.
hasEntity(entityId)true if the entity still exists.
getEntities()Array of every active entity id.
getEntityCount()Number of active entities.

Components

MethodPurpose
addComponent(entityId, component)Attach a single component.
addComponents(entityId, [components])Attach a batch of components.
removeComponent(entityId, ComponentClass)Detach a component by class.
getComponent(entityId, ComponentClass)Read a component or null.
hasComponent(entityId, ComponentClass)true if the entity has the component.
query(...ComponentClasses)Array of entity ids that have all of the given components.

Entity lookups

MethodPurpose
getAllEntities()All entities as WorldEntityView objects ({ entityId, name, type, object, components }).
getEntityById(entityId)One WorldEntityView or null.
getEntityByName(name)First entity whose Name matches, or null.
getEntitiesByName(name)All entities sharing a Name.
getEntitiesByComponent(component)All entities that own a component of the same class.

Scripts

MethodPurpose
getEntityScript<T>(entityId)The live Behaviour instance attached to an entity.
getScript<T>(className)First behaviour instance of the given class anywhere in the world.
getScripts<T>(className)All behaviour instances of the given class.

Input

MethodPurpose
getKey(key)true while a key is held this frame.
getKeyDown(key) / getKeyUp(key)Edge-triggered: true only on the frame the key transitions.
getMouse(button) / getMouseDown(button) / getMouseUp(button)Mouse buttons ('left', 'right', etc.).
getScrollClick() / getScrollClickDown() / getScrollClickUp()Middle-mouse / scroll-wheel click.
getMousePosition()Latest pointer position (canvas-relative).
getMouseDelta() / consumeMouseDelta()Movement since last frame; consume zeroes it after read.
getScrollDelta() / consumeScrollDelta()Wheel deltas; consume zeroes them.
lockMousePointer() / unlockMousePointer() / isMousePointerLocked()Pointer-lock for first-person controls.

Scene & physics access

MethodPurpose
getScene()The live THREE.Scene.
getCamera()The active rendering camera.
getRenderer()The THREE.WebGLRenderer.
getPhysicsWorld()The Rapier World (or null if physics is disabled).

Raycasting

MethodPurpose
raycast(origin, direction, near?, far?)Cast a ray through the scene; returns IntersectionResult[] sorted by distance.
raycastFrom(eid, near?, far?)Cast a ray from an entity's world position along its forward direction; the source entity is filtered out of results.

Both methods accept Vec3 (THREE.Vector3 or { x, y, z }) and return IntersectionResult[] — each entry pairs the owning ECS entityId with the underlying threeResult: THREE.Intersection so you can read point, distance, face, uv, etc. directly from three.js.

Systems

MethodPurpose
getSystem(SystemClass)Resolve a registered system instance.
hasSystem(SystemClass)true if the system is registered.
getObjectManagementSystem()Shorthand for the built-in ObjectManagementSystem. Use this to spawn or destroy entities at runtime (requestCreate, createNow, deleteEntity).
getEnvironmentComponent()Returns the singleton Environment component, or undefined if not yet initialized. Mutate values to change skybox, fog, or gravity at runtime.
getPostprocessingComponent()Returns the singleton Postprocessing component, or undefined if not yet initialized. Mutate values to toggle effects or adjust parameters at runtime.

World lifecycle

MethodPurpose
pause()Pause simulation. All system ticks stop — physics, animation, scripts, and rendering freeze on the last frame.
resume()Resume simulation after a pause() call. Has no effect if the world is not paused.
isPaused()Returns true while the world is paused.

Examples

Find an entity by name and move it

ts
import { Behaviour, TransformComponent } from "@relu-interactives/spatial-ecs";

export default class TargetMover extends Behaviour {
  onUpdate() {
    const target = this.world.getEntityByName("Goal");
    if (!target) return;

    const transform = this.world.getComponent(target.entityId, TransformComponent);
    if (!transform) return;

    transform.position.x += this.deltaTime * 0.5;
  }
}

Iterate every entity that owns a component

ts
import { Behaviour, LightComponent } from "@relu-interactives/spatial-ecs";

export default class FlickerAllLights extends Behaviour {
  onUpdate() {
    const ids = this.world.query(LightComponent);
    for (const id of ids) {
      const light = this.world.getComponent(id, LightComponent);
      if (!light) continue;
      light.values.intensity = 1 + Math.sin(performance.now() / 200) * 0.2;
    }
  }
}

Talk to another script on a different entity

ts
import { Behaviour } from "@relu-interactives/spatial-ecs";
import type Health from "./Health";

export default class DamageDealer extends Behaviour {
  fire() {
    const target = this.world.getEntityByName("Player");
    if (!target) return;

    const health = this.world.getEntityScript<Health>(target.entityId);
    health?.takeDamage(10);
  }
}

Spawn a new entity at runtime

There are two ways to spawn entities from a script:

  1. Manually — allocate an id, build your own THREE.Object3D, and attach components yourself. Use this when you need full control over the mesh/material.
  2. Through the ObjectManagementSystem — ask the registered factory for a built-in kind ("cube", "sphere", "model", "pointLight", …) to create the entity with all of its baseline components for you. Use this when you want a primitive that behaves exactly like one created from the editor.

Option 1 (Manual: build the Object3D yourself)

ts
import {
  Behaviour,
  createEntityTypeComponent,
  createNameComponent,
  createObject3DRefComponent,
  createTransformComponent,
} from "@relu-interactives/spatial-ecs";
import * as THREE from "three";

export default class Spawner extends Behaviour {
  init() {
    const id = this.world.createEntity();
    const mesh = new THREE.Mesh(
      new THREE.BoxGeometry(1, 1, 1),
      new THREE.MeshStandardMaterial({ color: "tomato" }),
    );
    this.world.getScene().add(mesh);

    this.world.addComponents(id, [
      createNameComponent({ value: "RuntimeCube" }),
      createEntityTypeComponent({ value: "cube" }),
      createTransformComponent({ position: { x: 0, y: 1, z: 0 } }),
      createObject3DRefComponent({ object: mesh }),
    ]);
  }
}

Option 2 (Use ObjectManagementSystem)

ObjectManagementSystem is the same system the editor uses to create objects, so the new entity comes pre-wired with the correct components for its kind.

ts
import { Behaviour } from "@relu-interactives/spatial-ecs";

export default class CubeSpawner extends Behaviour {
  init() {
    const objects = this.world.getObjectManagementSystem();
    if (!objects) return;

    // Supported kinds:
    //   "cube", "sphere", "plane", "capsule", "cylinder", "empty",
    //   "model", "image", "imageTarget", "sprite", "audio", "camera", "video",
    //   "ambientLight", "directionalLight", "pointLight", "spotLight", "areaLight"
    objects.requestCreate(
      "cube",
      {
        name: "RuntimeCube",
        transform: {
          position: { x: 0, y: 1, z: 0 },
          rotation: { x: 0, y: 0, z: 0 },
          scale: { x: 1, y: 1, z: 1 },
        },
      },
      (entityId) => {
        // Optional: runs once the entity has been created.
        console.log("spawned cube", entityId);
      },
    );
  }
}

For the payload accepted by each kind (audio paths, camera projection, light colors, video chroma-key, image targets, etc.), see the Spawning reference.

Cast a ray and read the hit entity

world.raycast accepts any origin/direction in world space and returns every intersected mesh sorted by distance. Each hit carries the owning entityId so you can drive game logic from the result.

ts
import { Behaviour, NameComponent } from "@relu-interactives/spatial-ecs";
import * as THREE from "three";

export default class ClickToInspect extends Behaviour {
  private direction = new THREE.Vector3();

  onUpdate() {
    if (!this.world.getMouseDown("left")) return;

    const camera = this.world.getCamera();
    camera.getWorldDirection(this.direction);

    const hits = this.world.raycast(camera.position, this.direction);
    const first = hits.find((hit) => hit.entityId !== null);
    if (!first || first.entityId === null) return;

    const name = this.world.getComponent(first.entityId, NameComponent);
    console.log(
      "clicked",
      name?.value ?? first.entityId,
      "@",
      first.threeResult.distance.toFixed(2),
      "m",
    );
  }
}

Cast a ray from an entity's forward direction

world.raycastFrom(eid) uses the entity's world position and +Z direction. The source entity is automatically excluded from the results so you don't hit yourself.

ts
import { Behaviour } from "@relu-interactives/spatial-ecs";

export default class LookDetector extends Behaviour {
  onUpdate() {
    const hits = this.world.raycastFrom(this.entityId, 0, 25);
    const target = hits[0];

    if (target && target.entityId !== null) {
      const view = this.world.getEntityById(target.entityId);
      console.log(
        "looking at",
        view?.name,
        "@",
        target.threeResult.distance.toFixed(2),
        "m",
      );
    }
  }
}

Read keyboard and mouse input

ts
import { Behaviour, TransformComponent, KeyboardKeys } from "@relu-interactives/spatial-ecs";

export default class WASDMove extends Behaviour {
  speed = 3;

  onUpdate() {
    const transform = this.world.getComponent(this.entity.entityId, TransformComponent);
    if (!transform) return;

    const dt = this.deltaTime;
    if (this.world.getKey(KeyboardKeys.W)) transform.position.z -= this.speed * dt;
    if (this.world.getKey(KeyboardKeys.S)) transform.position.z += this.speed * dt;
    if (this.world.getKey(KeyboardKeys.A)) transform.position.x -= this.speed * dt;
    if (this.world.getKey(KeyboardKeys.D)) transform.position.x += this.speed * dt;

    if (this.world.getMouseDown("left")) {
      console.log("click at", this.world.getMousePosition());
    }
  }
}

Pointer lock for a first-person camera

ts
import { Behaviour } from "@relu-interactives/spatial-ecs";

export default class FpsLook extends Behaviour {
  init() {
    this.world.lockMousePointer();
  }

  onUpdate() {
    if (!this.world.isMousePointerLocked()) return;

    const { x, y } = this.world.consumeMouseDelta();
    this.entity.object?.rotateY(-x * 0.002);
    this.world.getCamera().rotateX(-y * 0.002);
  }

  dispose() {
    this.world.unlockMousePointer();
  }
}

Reach into the physics world

ts
import { Behaviour, PhysicsComponent } from "@relu-interactives/spatial-ecs";

export default class Jump extends Behaviour {
  onUpdate() {
    if (!this.world.getKeyDown("space")) return;

    const physics = this.world.getComponent(this.entity.entityId, PhysicsComponent);
    physics?.rigidBody?.applyImpulse({ x: 0, y: 5, z: 0 }, true);
  }
}

Pause and resume the world

Use pause() / resume() to freeze and unfreeze simulation. All system ticks stop when paused — physics, animation, scripts, and the render pass. The last rendered frame stays frozen on the canvas.

ts
import { Behaviour } from "@relu-interactives/spatial-ecs";

export default class PauseOnHit extends Behaviour {
  onUpdate() {
    // Pause when the spacebar is pressed, resume when pressed again.
    if (!this.world.getKeyDown("Space")) return;

    if (this.world.isPaused()) {
      this.world.resume();
    } else {
      this.world.pause();
    }
  }
}

TIP

pause() and resume() affect the entire world. If you only want to stop a subset of entities (for example, freeze enemies while a menu is open) use a flag on a custom component instead and guard your onUpdate logic.

See also