Files
skills/docs/gaming/games/ecs/src/world/world.md
2026-05-29 08:20:56 +01:00

5.2 KiB

World

(no submodules)

Purpose

World is the central container for an ECS instance. It owns the component registry, entity pool, archetype graph, query cache, system scheduler, and the typed EventBus that fires entity-lifecycle events. Every public mutator is instrumented with OpenTelemetry — spawn / despawn increment a counter, execute records into a histogram — so production behaviour is observable through the same in-memory MeterProvider used in tests.

The createWorld(config?) factory is the canonical entry point. World itself is exported so consumers can subclass or interrogate the instance.

Features

  • Archetype-graph mutations: addComponent / removeComponent move entities between archetypes lazily.
  • Idempotent addComponent — no-op when the component already exists.
  • Component validation via descriptor-attached Zod schemas; throws ECS.COMPONENT_VALIDATION_FAILED on rejection.
  • System scheduling with phase + priority ordering through SystemScheduler.
  • snapshot / restore round-trip with Zod-validated payloads.
  • Typed EventBus<WorldEventMap> — single-object payloads matching the @suaveplan/events shape.
  • OpenTelemetry counter (ecs.entity.lifecycle_total) and histogram (ecs.world.execute_duration_ms).

Usage: Basic Example

import { createWorld, defineComponent, defineSystem } from "@suaveplan/ecs";

const Position = defineComponent("Position", { x: 0, y: 0 });
const Velocity = defineComponent("Velocity", { dx: 1, dy: 0 });

const world = createWorld();

world.addSystem(defineSystem({
    name: "physics",
    query: { include: [Position, Velocity] },
    execute: ({ entities, world, dt }) => {
        for (const entity of entities) {
            const p = world.getComponent(entity, Position)!;
            const v = world.getComponent(entity, Velocity)!;
            world.setComponent(entity, Position, {
                x: p.x + v.dx * dt,
                y: p.y + v.dy * dt,
            });
        }
    },
}));

const player = world.spawn();
world.addComponent(player, Position, { x: 0, y: 0 });
world.addComponent(player, Velocity, { dx: 5, dy: 0 });
world.execute(1 / 60);

Usage: Advanced Example

import { createWorld, defineComponent } from "@suaveplan/ecs";

const Position = defineComponent("Position", { x: 0, y: 0 });
const world = createWorld({ maxComponents: 128 });
world.registerComponent(Position);

world.events.on("entitySpawned", ({ entity }) => {
    console.log("spawned", entity);
});
world.events.on("componentAdded", ({ entity, component }) => {
    console.log(component, "->", entity);
});

const snapshot = world.snapshot();
// ... ship snapshot over the wire ...

const replay = createWorld();
replay.registerComponent(Position);
replay.restore(snapshot);

API Reference

Member Description
createWorld(config?) / new World(config) Build a world. maxComponents defaults to 256.
events EventBus<WorldEventMap> for entity lifecycle.
registry Underlying ComponentRegistry.
registerComponent(schema) Pre-register a schema (idempotent).
spawn() Allocate an entity and emit entitySpawned.
despawn(entity) Despawn and recycle the entity. No-op when unknown.
isAlive(entity) Boolean liveness check.
addComponent(entity, schema, data?) Attach a component; validates Zod-attached schemas.
removeComponent(entity, schema) Detach a component.
getComponent(entity, schema) Read a component (or undefined).
setComponent(entity, schema, data) Merge partial data; revalidates.
hasComponent(entity, schema) Membership check.
query(descriptor) One-off entity query.
addSystem(system) / removeSystem(name) System lifecycle.
execute(dt) Run every enabled system; records duration histogram.
snapshot() / restore(snap) Persistence pair. restore Zod-validates the input.
clear() Wipe entities + archetypes, keep systems.
entityCount / archetypeCount / getSystems() Inspection accessors.

Implementation Notes

  • Archetype identity is the bitset key (mask.toKey()); getOrCreateArchetype deduplicates archetypes through a Map<string, Archetype>.
  • addComponent and removeComponent go through a swap-remove from the old archetype, mutate the component map, then insert into the new archetype. The destination archetype is materialised lazily.
  • setComponent does not change archetype membership — it merges into the existing column slot and re-validates against the descriptor's Zod schema.
  • restore is the only persistence boundary that validates input with Zod (WorldSnapshotSchema). Failures throw ECS.SNAPSHOT_INVALID with the Zod issues in context. Unknown component names in the snapshot are silently skipped, so adding new component types to consumer code does not break older snapshots.
  • OTel instruments resolve through the global MeterProvider. In tests, the package's tests/setup/polyfills.ts registers an in-memory MeterProvider before any module imports so the instruments bind to it.
  • The world's EventBus uses single-object payloads ({ entity }, { entity, component }) consistent with the @suaveplan/events contract, rather than upstream's tuple shape.