ecspresso
    Preparing search index...

    Systems

    Systems use a fluent builder API: world.addSystem().addQuery().setProcess() — systems are automatically registered via deferred finalization. No explicit termination call is needed.

    world.addSystem('physics')
    .addQuery('moving', { with: ['position', 'velocity'] })
    .setProcess(({ queries, dt }) => {
    // Physics logic
    });

    world.addSystem('rendering')
    .addQuery('visible', { with: ['position', 'sprite'] })
    .setProcess(({ queries }) => {
    // Rendering logic
    });

    For the common case of one query iterated entity-by-entity, setProcessEach collapses the query definition, callback wiring, and outer for…of into a single chain step:

    world.addSystem('movement')
    .setProcessEach({ with: ['position', 'velocity'] }, ({ entity, dt }) => {
    entity.components.position.x += entity.components.velocity.x * dt;
    entity.components.position.y += entity.components.velocity.y * dt;
    });

    The callback receives { entity, dt, ecs }, plus resources when .withResources() is chained:

    world.addSystem('bounce')
    .withResources(['bounds'])
    .setProcessEach(
    { with: ['position', 'velocity', 'radius'] },
    ({ entity, dt, resources: { bounds } }) => { /* ... */ },
    );

    setProcessEach is valid only on a builder with zero prior queries or process function — TypeScript narrows this to never otherwise, and a runtime guard throws for untyped callers. For multi-query systems, keep using addQuery + setProcess.

    The inline query definition accepts the full query shape (with, without, optional, changed, parentHas). Phase / priority / group / lifecycle chains still compose around it.

    Use SystemProcessFn and SystemLifecycleFn when system callbacks are extracted into named helpers:

    import type { ConfigOf, QueryDefinition, SystemLifecycleFn, SystemProcessFn } from 'ecspresso';

    type GameConfig = ConfigOf<typeof game>;
    type MovementQueries = {
    moving: QueryDefinition<GameConfig['components'], 'position' | 'velocity'>;
    };

    const processMovement: SystemProcessFn<GameConfig, MovementQueries> = function processMovement({ queries, dt }) {
    queries.moving.forEach(entity => {
    entity.components.position.x += entity.components.velocity.x * dt;
    });
    };

    const initializeMovement: SystemLifecycleFn<GameConfig> = function initializeMovement(ecs) {
    ecs.updateResource('systemStatus', current => ({
    ...current,
    movementReady: true,
    }));
    };

    game.addSystem('movement')
    .addQuery('moving', { with: ['position', 'velocity'], mutates: ['position'] })
    .setOnInitialize(initializeMovement)
    .setProcess(processMovement);

    Systems are organized into named execution phases that run in a fixed order:

    preUpdatefixedUpdateupdatepostUpdaterender
    

    Each phase's command buffer is played back before the next phase begins, so entities spawned in preUpdate are visible to fixedUpdate, and so on. Systems without .inPhase() default to update.

    world.addSystem('input')
    .inPhase('preUpdate')
    .setProcess(({ queries, dt, ecs }) => { /* Read input, update timers */ });

    world.addSystem('physics')
    .inPhase('fixedUpdate')
    .setProcess(({ queries, dt, ecs }) => {
    // dt is always fixedDt here (e.g. 1/60)
    // Runs 0..N times per frame based on accumulated time
    });

    world.addSystem('gameplay')
    .inPhase('update') // default phase
    .setProcess(({ queries, dt, ecs }) => { /* Game logic, AI */ });

    world.addSystem('transform-sync')
    .inPhase('postUpdate')
    .setProcess(({ queries, dt, ecs }) => { /* Transform propagation */ });

    world.addSystem('renderer')
    .inPhase('render')
    .setProcess(({ queries, dt, ecs }) => { /* Visual output */ });

    The fixedUpdate phase uses a time accumulator for deterministic simulation. A spiral-of-death cap (8 steps) prevents runaway accumulation.

    const world = ECSpresso.create()
    .withComponentTypes<Components>()
    .withEventTypes<Events>()
    .withResourceTypes<Resources>()
    .withFixedTimestep(1 / 60) // 60Hz physics (default)
    .build();

    Use ecs.interpolationAlpha (0..1) in the render phase to smooth between fixed steps.

    Move systems between phases at runtime with world.updateSystemPhase('debug-overlay', 'render').

    Within each phase, systems execute in priority order (higher numbers first). Systems with the same priority execute in registration order:

    world.addSystem('physics')
    .inPhase('fixedUpdate')
    .setPriority(100) // Runs first within fixedUpdate
    .setProcess(() => { /* physics */ });

    world.addSystem('constraints')
    .inPhase('fixedUpdate')
    .setPriority(50) // Runs second within fixedUpdate
    .setProcess(() => { /* constraints */ });

    Organize systems into groups that can be enabled/disabled at runtime:

    world.addSystem('renderSprites')
    .inGroup('rendering')
    .addQuery('sprites', { with: ['position', 'sprite'] })
    .setProcess(({ queries }) => { /* ... */ });

    world.addSystem('renderParticles')
    .inGroup('rendering')
    .inGroup('effects') // Systems can belong to multiple groups
    .setProcess(() => { /* ... */ });

    world.disableSystemGroup('rendering'); // All rendering systems skip
    world.enableSystemGroup('rendering'); // Resume rendering
    world.isSystemGroupEnabled('rendering'); // true/false
    world.getSystemsInGroup('rendering'); // ['renderSprites', 'renderParticles']

    // If a system belongs to multiple groups, disabling ANY group skips the system

    Systems can have initialization, cleanup, and post-update hooks:

    world.addSystem('gameSystem')
    .setOnInitialize(async (ecs) => {
    console.log('System starting...');
    })
    .setOnDetach((ecs) => {
    console.log('System shutting down...');
    });

    await world.initialize();

    Register a callback that fires when an entity first matches a query:

    world.addSystem('onSpawn')
    .addQuery('enemies', { with: ['enemy', 'health'] })
    .setOnEntityEnter('enemies', ({ entity, ecs }) => {
    console.log(`Enemy ${entity.id} entered query`);
    })
    .setProcess(({ queries }) => { /* ... */ });

    Post-Update Hooks

    Register callbacks that run between the postUpdate and render phases:

    // Returns unsubscribe function; multiple hooks run in registration order
    const unsubscribe = world.onPostUpdate(({ ecs, dt }) => {
    console.log(`Frame completed in ${dt}s`);
    });

    unsubscribe();