Three Start

Quick Start (A)

Three core concepts of three-start in one cohesive example — components, modules, lifecycle control.

Three concepts cover almost everything you'll write with three-start:

  • Components — pieces of behaviour you attach to a THREE.Object3D (Object3DBehaviour)
  • Modules — global systems on the shared context (ContextModule)
  • Lifecycle control — turning behaviour on/off without ripping objects out of the scene

The snippet below shows them working together in a tiny shooter scene. Each line of the example exists because the design needs it — there's no API tour for its own sake.

main.ts
import * as THREE from "three/webgpu";
import {
  ThreeStart,
  Object3DBehaviour,
  ContextModule,
  addComponent,
  getComponent,
  setActive,
  getIsActive,
  destroy,
} from "three-start";

// ── Module: turn pointer clicks into "hit" events on whatever's under them ──
class PointerModule extends ContextModule {
  private raycaster = new THREE.Raycaster();
  private ndc = new THREE.Vector2();

  onAwake() {
    this.ctx.renderer.domElement.addEventListener("pointerdown", this.handle);
  }

  private handle = (e: PointerEvent) => {
    const r = this.ctx.renderer.domElement.getBoundingClientRect();
    this.ndc.x = ((e.clientX - r.left) / r.width) * 2 - 1;
    this.ndc.y = -((e.clientY - r.top) / r.height) * 2 + 1;
    this.raycaster.setFromCamera(this.ndc, this.ctx.camera);

    const [hit] = this.raycaster.intersectObjects(this.ctx.scene.children, true);
    if (!hit) return;

    // The module doesn't know which objects are damageable — it asks.
    getComponent(hit.object, Health)?.takeDamage(25);                  
  };
}

declare module "three-start" {
  interface ThreeStartRegister {
    modules: { pointer: PointerModule };
  }
}

// ── Component: idle rotation. Pure visual. ──
class Spin extends Object3DBehaviour {
  speed = 1;
  onUpdate() {
    this.object.rotation.y += this.speed * this.ctx.getDeltaTime();
  }
}

// ── Component: HP + reactions. Coexists with Spin on every enemy. ──
class Health extends Object3DBehaviour {
  hp = 100;
  private mat!: THREE.MeshStandardMaterial;

  onAwake() {
    this.mat = (this.object as THREE.Mesh).material as THREE.MeshStandardMaterial;
  }

  takeDamage(amount: number) {
    this.hp -= amount;
    this.mat.color.setRGB(1, this.hp / 100, this.hp / 100); // bleed red

    // Wounded enemies stop rotating — disable the sibling Spin component.
    const spin = getComponent(this.object, Spin);                          
    spin?.setEnabled(this.hp > 30);                                        

    // Dead enemies are removed from the scene entirely.
    if (this.hp <= 0) destroy(this.object);                                
  }

  onDestroy() {
    console.log("enemy down, hp was", this.hp);
  }
}

// ── Bootstrap ──
const starter = new ThreeStart()
  .addModules({ pointer: new PointerModule() });

const enemies = new THREE.Group();
starter.ctx.scene.add(enemies);

for (let i = 0; i < 5; i++) {
  const cube = new THREE.Mesh(
    new THREE.BoxGeometry(),
    new THREE.MeshStandardMaterial({ color: 0xffffff }),
  );
  cube.position.x = i * 1.5 - 3;
  enemies.add(cube);

  addComponent(cube, Spin);
  addComponent(cube, Health);
}

starter.ctx.scene.add(new THREE.AmbientLight(0xffffff, 1));
starter.ctx.camera.position.set(0, 2, 8);
starter.ctx.camera.lookAt(0, 0, 0);

starter.mount(document.getElementById("app")!);
starter.start();

// Pause/resume the entire enemies branch with `P`.
window.addEventListener("keydown", (e) => {                                
  if (e.code === "KeyP") setActive(enemies, !getIsActive(enemies));        
});

Walk through what each concept does in this scene:

ConceptWhere it livesWhy the example needs it
Component (Object3DBehaviour)Spin, HealthPer-cube state and per-frame logic. Two components on the same cube prove they coexist independently — Spin doesn't care about HP, Health doesn't care about rotation.
Module (ContextModule)PointerModuleOne global piece of state (the raycaster + canvas listener). Every cube reuses it; the module doesn't need to know how many cubes exist or which ones are alive.
getComponent(obj, Class)PointerModule.handle, Health.takeDamageBoth cases the caller only has an Object3D and needs to discover whether a particular behaviour is attached. The pointer module finds Health on the hit object; Health finds its sibling Spin to stun it.
comp.setEnabled(false)Health.takeDamage (stun)Pause one behaviour on an object without affecting others. Cube keeps rendering; only its rotation halts.
setActive(group, false)KeyP handler (pause)Freeze an entire subtree — every component under group runs onDisable. Reverse with setActive(group, true). (setActive is lifecycle-only; set group.visible = false separately if you also want the subtree to stop rendering.)
destroy(object)Health.takeDamage (death)Permanently remove. Fires onDestroy on every component on the object before unparenting.

These three layers cover the whole library. The rest of the docs goes deep into each:

Edit on GitHub