Skip to content

World, queries and systems ​

The ecs family is for games with many similar things: bullets, enemies, tiles, particles. A World owns entities and keeps one store per component key, so a query can visit every entity with a given set of components. Systems are plain functions that a PhaseSchedule runs in order.

The World ​

ts
const world: World = new World();
world.registerStore(POSITION);           // one store per key, before use
const e: Entity = world.createEntity();  // an active root the world owns
world.put(e, POSITION, new Position(0.0, 0.0));
MethodWhat it does
registerStore(key)Creates the store for a key. Registering a key twice throws.
createEntity()Creates an entity, adopts it and activates it.
adoptEntity(entity)Takes ownership of an entity you built yourself (or an Entity subclass). Returns its new EntityId.
put(entity, key, value)Adds a component and files it in the key's store.
get, getRequired, removeComponentRead or remove one entity's component. getRequired throws when it is missing.
query(key), query2(a, b)Queries over one or two keys.
removeEntity(entity)Releases the entity without disposing it, so it can move to another world.
disposeEntity(entity)Removes and disposes it.
idOf(entity), resolve(id)Map between entities and EntityIds. A stale id resolves to null.
dispose()Disposes every entity and closes the world.

World takes an optional IComponentStoreFactory. The default, DenseStoreFactory, makes a DenseStore per key; implement IComponentStore if a game needs a different layout.

Queries ​

ts
world.query(POSITION).each((e: Entity, p: Position): void => { /* ... */ });
world.query2(POSITION, VELOCITY).each((e: Entity, p: Position, v: Velocity): void => { /* ... */ });

each takes a snapshot of matching entities first, so the order is stable and the visit is safe. While a query or a phase is running, the world rejects direct structural changes.

Deferred commands ​

Inside a phase, create, dispose and change entities through the defer methods. They queue the change, and flush() applies the batch:

Deferred callApplies at flush
deferCreateEntity()Returns the entity at once; it joins the world at flush.
deferDisposeEntity(entity)Disposes it.
deferPutComponent(entity, key, value)Adds the component.
deferRemoveComponent(entity, key)Removes the component.

Phase schedules ​

ts
const schedule: PhaseSchedule = new PhaseSchedule();
schedule.addPhase("input", [], true);
schedule.addPhase("simulate", ["input"], false);
schedule.addPhase("present", ["simulate"], true);
schedule.addSystem("simulate", (w: World, dt: float, tick: int): void => { /* ... */ });
schedule.run(world, dt, tick);
  • addPhase(name, after, flushAfter) adds a phase. after lists phases that must run first. Phases with no ordering between them run in the order you added them.
  • flushAfter: true makes the phase a command boundary: its systems run, then world.flush() applies their deferred work. So an entity created in simulate is visible to the next phase in the same tick.
  • Each system runs inside World.runPhase, which guards against direct structural changes.
  • Duplicate names, unknown prerequisites and cycles throw before anything runs. The schedule cannot be changed while it is running. A system that throws stops that run, and the phase's commands stay unflushed.

createConventionalSchedule() returns a ready schedule with the phases input, simulate, resolve, present and flush, each with flushAfter: true. You are free to use any phases you like instead, or none.

If you need different timing, drive the world yourself:

ts
world.runPhase((): void => { collectInput(world); });
world.flush();
world.runPhase((): void => { simulate(world); });
world.runPhase((): void => { present(world); });
world.flush();

Spawning inside a phase ​

ts
import { ComponentKey, DefinitionRegistry, Entity, PhaseSchedule, ResolvedDefinition, World } from "gamekit";

class Enemy {
    kind: string;
    hp: int;
    constructor(kind: string, hp: int) {
        this.kind = kind;
        this.hp = hp;
    }
}
const ENEMY: ComponentKey<Enemy> = new ComponentKey<Enemy>("enemy");

const defs: DefinitionRegistry = new DefinitionRegistry();
defs.define("slime", { hp: 3 });
defs.define("big_slime", { hp: 9 }, ["slime"]);

const world: World = new World();
world.registerStore(ENEMY);

const schedule: PhaseSchedule = new PhaseSchedule();
schedule.addPhase("spawn", [], true);        // flushAfter: deferred work lands at the end of the phase
schedule.addPhase("count", ["spawn"]);
schedule.addSystem("spawn", (w: World, dt: float, tick: int): void => {
    // Inside a phase the world rejects direct structural changes. Defer them instead.
    const e: Entity = w.deferCreateEntity();
    const enemy: Enemy = defs.instantiate("big_slime",
        (def: ResolvedDefinition, context: any): Enemy => new Enemy(def.id, def.data.hp as int)) as Enemy;
    w.deferPutComponent(e, ENEMY, enemy);
});
schedule.addSystem("count", (w: World, dt: float, tick: int): void => {
    let total: int = 0;
    w.query(ENEMY).each((e: Entity, enemy: Enemy): void => { total += enemy.hp; });
    console.log("tick " + tick + ": " + w.entityCount + " enemies, " + total + " hp");
});

for (let tick: int = 0; tick < 3; tick++) {
    schedule.run(world, 0.1, tick);
}
text
tick 0: 1 enemies, 9 hp
tick 1: 2 enemies, 18 hp
tick 2: 3 enemies, 27 hp

Stepping the world: WorldTicker ​

In a window game, let the runtime's fixed tick drive the schedule. new WorldTicker(node, world, schedule, tickDelta) sets the node's onTick to run schedule.run(world, tickDelta, tickNumber) once per tick. The runtime owns the tick rate (App.tps), the catch-up cap and App.timeScale.

Because a scene is a node, this ties the world to its scene: when SceneStack pushes a scene on top, it pauses the node below, so that world stops; when a scene is popped, its node is disposed. ticker.dispose() detaches the hook. Getting started shows a full example.

In a console program, call schedule.run from App.onTick yourself. In a headless test, use a SimulationClock from the testing family.

Reference: World, PhaseSchedule, Query, Query2, WorldTicker, createConventionalSchedule.