GameComponents in action
site/media/lessons/foundations-components.lesson.ts (5 steps) The whole engine rests on one idea: the Three.js Object3D is the entity, and behavior is
attached to it with a GameComponent. This lesson makes that concrete — first the shape of
a component, then seeing components tick live.
A GameComponent
Section titled “A GameComponent”A GameComponent (packages/engine/src/ecs/game-component.ts) is a small class with a
lifecycle:
// A minimal component: spin an object every frame.import { GameComponent } from '@engine/ecs/game-component';import { z } from 'zod';
export class Spin extends GameComponent { static phase = 'gameLogic' as const; // when update() runs static schema = z.object({ speed: z.number().default(1) }); // authored field speed = 1;
init(ctx) { /* runs once, after the entity + physics exist */ } update(dt, ctx) { this.object3D.rotation.y += this.speed * dt; } dispose(ctx) { /* clean up anything you created */ }}this.object3Dis the entity — there is no separate handle.static phasepicks which phaseupdate()runs in (defaultgameLogic).static schema(Zod) declares fields an author can set in the scene; they’re validated and assigned on load.init/update/disposeare the lifecycle; sensor overlaps also callonTriggerEnter/onTriggerExit.
Register the class by name in the component registry, then reference it from a scene’s
components — the loader validates the data through schema.parse() and attaches an instance
(the ComponentManager ticks it each frame in its phase).
See it run
Section titled “See it run”-
Open a scene whose entities have GameComponents. Run
VGAI_PROJECT=examples/editor-tutorial npm run dev— the project’s scene loads by default.
A scene whose entities carry components. -
Enter play mode — each component’s
update()runs every tick. Press Play; the ComponentManager begins ticking every attached component in its phase.
Components tick once play starts. -
Watch component-driven animation and physics. The motion you see is components mutating their
object3Deach frame.
Per-frame update() in action. -
Components keep ticking in their phase. Each runs in the fixed phase order — input → … → gameLogic → … → render.
-
Stop play mode. Press Stop (or
Escape) —dispose()runs and the runtime tears down.
Try it yourself — in the browser
Section titled “Try it yourself — in the browser”No install needed. Open the Code Playground — it
starts with a Spinner GameComponent and runs your code live in the real engine (your work
autosaves to your browser). Even if you’ve never written code, you can change one number and watch it
happen. The starter’s update(dt) already spins, bobs, and shifts color:
update(dt) { this.t += dt; this.object3D.rotation.y += dt * 1.5; // spin speed this.object3D.rotation.x += dt * 0.6; this.object3D.position.y = Math.sin(this.t * 2) * 0.4; // bob height}Change one thing, press Run, watch it:
- Spin faster — make
1.5bigger, e.g.dt * 6. - Bob higher — make the last number bigger:
Math.sin(this.t * 2) * 1.5. - Bob faster — make the
2bigger:Math.sin(this.t * 6) * 0.4.
Then reload the page — your edited code is still there. That’s the whole loop: edit → Run → see it → it persists. You just wrote and ran real engine code with zero setup.
Recap
New functionality
- Saw a component-rich scene run
- Read the GameComponent lifecycle
New concepts & skills
- The Object3D IS the entity (no separate ECS)
- update() runs each fixed tick in the component's phase
- Static schema authors fields; the registry + ComponentManager wire and tick instances
- State on the instance survives hot reload
Next lesson → Manual: Entities & GameComponents