Skip to content

Entities & GameComponents

There is no separate entity layer in vgai — no ECS, no scene-node wrapper. A Three.js Object3D is the entity. Behavior is attached to it with a GameComponent.

GameComponent (packages/engine/src/ecs/game-component.ts) is an abstract class your behaviors extend:

abstract class GameComponent {
static phase: SystemPhaseName = 'gameLogic'; // which phase update() runs in
static schema?: z.ZodObject<z.ZodRawShape>; // optional authored fields
object3D!: THREE.Object3D; // the entity (set on attach)
rigidBody: RAPIER.RigidBody | null = null; // wired by ComponentManager
collider: RAPIER.Collider | null = null;
init?(ctx: GameContext): void | Promise<void>;
abstract update(dt: number, ctx: GameContext): void;
dispose?(ctx: GameContext): void;
onTriggerEnter?(other: THREE.Object3D, ctx: GameContext): void;
onTriggerExit?(other: THREE.Object3D, ctx: GameContext): void;
}
Lifecycle: init runs once, update runs every tick in its phase, dispose on destroy, with onTriggerEnter/Exit on sensor overlap
The GameComponent lifecycle.
  • init?(ctx) — runs once after the entity is fully constructed (object3D, physics, and joints all exist). May be async.
  • update(dt, ctx) — abstract; runs every fixed tick, in the component’s static phase (default gameLogic). See the game loop & phases.
  • dispose?(ctx) — runs when the entity is destroyed or the component removed.
  • onTriggerEnter? / onTriggerExit? — fire when a sensor collider overlap starts / ends, with the other Object3D.

State lives on the instance (this). Because hot reload swaps the prototype (not the instance), your component’s live state survives a code edit.

A component can declare a static Zod schema describing the fields an author can set in a scene file. Those fields are validated and assigned onto the instance when the scene loads — and they drive the editor’s inspector. (See Scenes & prefabs and the Scene Schema reference.)

The ComponentManager (packages/engine/src/ecs/component-manager.ts) owns every live component instance, indexed by phase (for ticking) and by Object3D (for lookup). It:

  • registers one tick function per phase into the SystemRunner;
  • on attach, wires object3D / rigidBody / collider and queues init;
  • runs initAll() once after the whole scene has loaded;
  • supports hotSwap(name, NewClass) — updating the prototype of all instances of a class while preserving their state (the basis for hot reload).

It is safe to attach() / detach() from inside an update() (a Unity-MonoBehaviour-style spawn/despawn pattern): structural changes are deferred and applied between ticks, and a component detached earlier in a frame is not update()’d again that frame.

The registry maps component names (strings used in scene files) to their classes. applyComponents (packages/engine/src/scene/component-registry.ts) hydrates an entity’s components from scene JSON: it looks up each class, validates/defaults the data through the class’s schema.parse(), assigns it onto a fresh instance, and attaches it.

Unknown component names throw at load time (fail-fast). npm run validate-scenes checks every .vscn.json against the registry offline.