5.2 KiB
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/removeComponentmove entities between archetypes lazily. - Idempotent
addComponent— no-op when the component already exists. - Component validation via descriptor-attached Zod schemas; throws
ECS.COMPONENT_VALIDATION_FAILEDon rejection. - System scheduling with phase + priority ordering through
SystemScheduler. snapshot/restoreround-trip with Zod-validated payloads.- Typed
EventBus<WorldEventMap>— single-object payloads matching the@suaveplan/eventsshape. - 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());getOrCreateArchetypededuplicates archetypes through aMap<string, Archetype>. addComponentandremoveComponentgo through a swap-remove from the old archetype, mutate the component map, then insert into the new archetype. The destination archetype is materialised lazily.setComponentdoes not change archetype membership — it merges into the existing column slot and re-validates against the descriptor's Zod schema.restoreis the only persistence boundary that validates input with Zod (WorldSnapshotSchema). Failures throwECS.SNAPSHOT_INVALIDwith the Zod issues incontext. 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'stests/setup/polyfills.tsregisters an in-memoryMeterProviderbefore any module imports so the instruments bind to it. - The world's
EventBususes single-object payloads ({ entity },{ entity, component }) consistent with the@suaveplan/eventscontract, rather than upstream's tuple shape.