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:
- Parameters — an array of
Query,Res,ResMut,Commands,GetWorld,EventWriter, orEventReaderdescriptors - 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.StartupaddSystem(movementSystem); // Schedule.UpdateaddSystemToSchedule(Schedule.FixedUpdate, physicsSystem);Schedule Types
| Schedule | When it runs |
|---|---|
Startup | Once at the beginning |
First | Every frame, before PreUpdate |
PreUpdate | Every frame, before Update |
Update | Every frame (main game logic) |
PostUpdate | Every frame, after Update |
Last | Every frame, after PostUpdate |
FixedPreUpdate | At fixed intervals, before FixedUpdate |
FixedUpdate | At fixed intervals (physics) |
FixedPostUpdate | At 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 aftermovementrunBefore: ['render']— the current system runs beforerender
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
| Method | Returns | Description |
|---|---|---|
cmds.spawn() | EntityCommands | Create a new entity, returns a builder |
cmds.entity(entity) | EntityCommands | Get a builder for an existing entity |
cmds.despawn(entity) | this | Queue entity for destruction |
cmds.insertResource(res, value) | this | Insert or overwrite a resource |
EntityCommands API
spawn() and entity() return an EntityCommands builder. All methods are chainable:
| Method | Returns | Description |
|---|---|---|
.insert(component, data?) | this | Add or update a component |
.remove(component) | this | Remove a component |
.id() | Entity | Get 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
| Method | Returns | Description |
|---|---|---|
world.get(entity, Component) | component data | Read a component (O(1)) |
world.set(entity, Component, data) | void | Write a component (O(1)) |
world.has(entity, Component) | boolean | Check if entity has component |
world.tryGet(entity, Component) | data | null | Safe read, returns null if missing |
world.valid(entity) | boolean | Check if entity is alive |
world.setParent(child, parent) | void | Set entity’s parent (child Transform becomes relative to parent) |
world.removeParent(entity) | void | Remove 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:
import { defineComponent } from 'esengine';export const Speed = defineComponent('Speed', { value: 200 });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
- Queries — query filters and iteration methods
- Resources — global singleton data
- Change Detection — reacting to component changes
- Events — decoupled inter-system communication