Skip to content

Systems

Systems contain game logic. They declare what data they need via parameters, and Estella injects the data automatically. Systems operate on all entities in the scene that match the query.

Defining Systems

import { defineSystem, Res, Time, Query, Mut, Transform, Velocity } from 'esengine';
const movementSystem = defineSystem(
[Res(Time), Query(Mut(Transform), Velocity)],
(time, query) => {
for (const [entity, transform, velocity] of query) {
transform.position.x += velocity.linear.x * time.delta;
transform.position.y += velocity.linear.y * time.delta;
}
}
);

This system automatically processes every entity in the scene that has both Transform and Velocity components.

defineSystem takes two arguments:

  1. Parameters — an array of Query, Res, ResMut, Commands, GetWorld, EventWriter, or EventReader descriptors
  2. Function — receives the resolved parameters in the same order

Registering Systems

Register systems with top-level functions:

import { addSystem, addStartupSystem, addSystemToSchedule, Schedule } from 'esengine';
addStartupSystem(setupSystem); // Schedule.Startup
addSystem(movementSystem); // Schedule.Update
addSystemToSchedule(Schedule.FixedUpdate, physicsSystem);

Schedule Types

ScheduleWhen it runs
StartupOnce at the beginning
FirstEvery frame, before PreUpdate
PreUpdateEvery frame, before Update
UpdateEvery frame (main game logic)
PostUpdateEvery frame, after Update
LastEvery frame, after PostUpdate
FixedPreUpdateAt fixed intervals, before FixedUpdate
FixedUpdateAt fixed intervals (physics)
FixedPostUpdateAt fixed intervals, after FixedUpdate

System Ordering

By default, systems within the same Schedule run in registration order. If you need to enforce execution order between systems, use the third argument to defineSystem to specify runBefore or runAfter:

import { defineSystem, Query, Res, Time, Mut, Transform, Velocity, Sprite } from 'esengine';
const movementSystem = defineSystem(
[Res(Time), Query(Mut(Transform), Velocity)],
(time, query) => { /* ... */ },
{ name: 'movement' }
);
const renderSystem = defineSystem(
[Query(Transform, Sprite)],
(query) => { /* ... */ },
{ name: 'render', runAfter: ['movement'] }
);
  • runAfter: ['movement'] — the current system runs after movement
  • runBefore: ['render'] — the current system runs before render

The engine uses topological sorting to determine the final execution order. If the ordering constraints form a cycle, an error is thrown at startup.

System Parameters

Commands

Create, modify, and destroy entities at runtime:

import { Commands, Sprite, Transform, Velocity, defineResource } from 'esengine';
const Score = defineResource({ value: 0 });
defineSystem([Commands()], (cmds) => {
// Spawn a new entity with components (chainable)
const bullet = cmds.spawn()
.insert(Transform, { position: { x: 0, y: 0, z: 0 } })
.insert(Sprite, { size: { x: 8, y: 8 } })
.id();
// Modify an existing entity
cmds.entity(bullet)
.insert(Velocity, { linear: { x: 100, y: 0, z: 0 } })
.remove(Sprite);
// Despawn an entity
cmds.despawn(bullet);
// Insert a resource
cmds.insertResource(Score, { value: 0 });
});

Commands API

MethodReturnsDescription
cmds.spawn()EntityCommandsCreate a new entity, returns a builder
cmds.entity(entity)EntityCommandsGet a builder for an existing entity
cmds.despawn(entity)thisQueue entity for destruction
cmds.insertResource(res, value)thisInsert or overwrite a resource

EntityCommands API

spawn() and entity() return an EntityCommands builder. All methods are chainable:

MethodReturnsDescription
.insert(component, data?)thisAdd or update a component
.remove(component)thisRemove a component
.id()EntityGet the entity ID

Query

Iterate entities with specific components:

import { Query, Mut, Transform, Sprite } from 'esengine';
defineSystem([Query(Mut(Transform), Sprite)], (query) => {
for (const [entity, transform, sprite] of query) {
transform.position.x += 1;
}
});

See Queries for the full API.

Res (read-only resource)

import { Res, Time } from 'esengine';
defineSystem([Res(Time)], (time) => {
console.log(`Delta: ${time.delta}s`);
});

ResMut (mutable resource)

import { ResMut } from 'esengine';
defineSystem([ResMut(GameState)], (state) => {
state.get().score += 10;
});

See Resources for the full API.

EventWriter

Send events from a system:

import { defineEvent, EventWriter } from 'esengine';
const DamageEvent = defineEvent<{ target: number; amount: number }>('Damage');
defineSystem([EventWriter(DamageEvent)], (writer) => {
writer.send({ target: enemy, amount: 25 });
});

EventReader

Receive events sent by other systems:

import { EventReader } from 'esengine';
defineSystem([EventReader(DamageEvent)], (reader) => {
for (const event of reader) {
console.log(`${event.target} took ${event.amount} damage`);
}
});

See Events for the full API.

GetWorld

Direct access to the ECS World. Use it with world.get() / world.set() for O(1) component access by entity ID, or with utility functions like findEntityByName():

import { GetWorld, findEntityByName, ProgressBar } from 'esengine';
defineSystem([GetWorld()], (world) => {
const entity = findEntityByName(world, 'HealthBar');
if (entity) {
const bar = world.get(entity, ProgressBar);
bar.value = playerHealth / maxHealth;
world.set(entity, ProgressBar, bar);
}
});

World API

MethodReturnsDescription
world.get(entity, Component)component dataRead a component (O(1))
world.set(entity, Component, data)voidWrite a component (O(1))
world.has(entity, Component)booleanCheck if entity has component
world.tryGet(entity, Component)data | nullSafe read, returns null if missing
world.valid(entity)booleanCheck if entity is alive
world.setParent(child, parent)voidSet entity’s parent (child Transform becomes relative to parent)
world.removeParent(entity)voidRemove entity’s parent, making it a root entity

Entity Hierarchy

Use setParent / removeParent to build parent-child relationships. A child’s Transform is relative to its parent.

import { GetWorld, Commands, Transform, Sprite } from 'esengine';
defineSystem([Commands(), GetWorld()], (cmds, world) => {
const parent = cmds.spawn()
.insert(Transform, { position: { x: 100, y: 100, z: 0 } })
.insert(Sprite, {})
.id();
const child = cmds.spawn()
.insert(Transform, { position: { x: 50, y: 0, z: 0 } })
.insert(Sprite, {})
.id();
world.setParent(child, parent);
// Later, to detach:
// world.removeParent(child);
});

Combining Parameters

defineSystem(
[Commands(), Res(Time), Res(Input), GetWorld(), Query(Mut(Transform), Velocity)],
(cmds, time, input, world, query) => {
// All parameters available
}
);

Example: Player Movement

Define a Speed component and attach it to the player entity in the editor. Then write a system:

src/components/Speed.ts
import { defineComponent } from 'esengine';
export const Speed = defineComponent('Speed', { value: 200 });
src/systems/movement.ts
import { defineSystem, addSystem, Res, Time, Input, Query, Mut, Transform } from 'esengine';
import { Speed } from '../components/Speed';
addSystem(defineSystem(
[Res(Time), Res(Input), Query(Mut(Transform), Speed)],
(time, input, query) => {
for (const [entity, transform, speed] of query) {
if (input.isKeyDown('KeyD')) {
transform.position.x += speed.value * time.delta;
}
if (input.isKeyDown('KeyA')) {
transform.position.x -= speed.value * time.delta;
}
}
}
));

Async Systems

defineSystem accepts async functions. This is useful for startup systems that need to load assets, or systems that call async APIs:

import { defineSystem, addStartupSystem, Res, Prefabs } from 'esengine';
addStartupSystem(defineSystem(
[Res(Prefabs)],
async (prefabs) => {
const { root } = await prefabs.instantiate('prefabs/Enemy.esprefab');
console.log('Spawned enemy:', root);
}
));

Async systems work in all schedule phases. No special declaration is needed — just pass an async function and the engine handles the rest. Commands are automatically flushed after the async function completes.

Error Handling

Each system runs inside its own try/catch boundary. If a system throws an error, it is caught and logged to the console without crashing the entire game loop. Other systems continue to execute normally.

Next Steps