Systems
A system is a function that runs each frame. You declare what it needs — a query over components, plus resources and events — and Estella calls it with exactly those, in the schedule.
import { defineComponent, defineSystem, addSystem, Query, Mut, Res, Time, Transform} from 'esengine';
const Speed = defineComponent('Speed', { value: 200 });
addSystem(defineSystem( [Res(Time), Query(Mut(Transform), Speed)], (time, query) => { for (const [entity, transform, speed] of query) { transform.position.x += speed.value * time.delta; } }));Parameters
Section titled “Parameters”A system’s first argument is its parameter list — the data it will receive, in order:
Query(...)— iterate every entity that has all listed components. Each iteration yields[entity, ...components].Mut(Component)— a component the system writes. Unwrapped components are read-only.Res(Resource)/ResMut(Resource)— a shared resource, read-only or writable (such asTimefor frame timing).Commands()— deferred structural changes (spawn / despawn / add / remove), applied at the next flush.EventReader(E)/EventWriter(E)— read or send a typed event stream.
Running order
Section titled “Running order”Systems live in a schedule of ordered phases. Register into the default
Update phase with addSystem, into Startup (runs once) with addStartupSystem,
or into any phase with addSystemToSchedule(Schedule.X, system):
| Phase | When it runs |
|---|---|
Startup |
Once, before the first frame. |
First → PreUpdate → Update → PostUpdate → Last |
Every frame, in this order. |
FixedPreUpdate → FixedUpdate → FixedPostUpdate |
On the fixed timestep (physics / replication cadence) — 0..n times per frame. |
Within a phase, systems run in registration order. To constrain that, register on
the App with ordering options — these reference system names, so name the
systems you want to order:
import { Schedule } from 'esengine';
const move = defineSystem([...], moveFn, { name: 'move' });const camera = defineSystem([...], cameraFn, { name: 'camera' });
app.addSystemToSchedule(Schedule.Update, move);app.addSystemToSchedule(Schedule.Update, camera, { runAfter: ['move'], // camera follows movement runIf: () => isPlaying, // skip this system when the condition is false});Scene systems only run while their scene is active, so multiple scenes can coexist in one world without interfering.
Filtering & change detection
Section titled “Filtering & change detection”A query can narrow beyond “has these components” and react to changes:
// Extra presence filters — components you require/exclude but don't read:Query(Transform, Mut(Velocity)).without(Frozen).with(Enemy)
// Boolean combinations via filter():import { Query, With, Without, Or, Not } from 'esengine';Query(Transform).filter(Or(With(Player), With(Ally)))
// Change detection — only entities whose component was added/changed since// this system last ran:import { Query, Added, Changed } from 'esengine';Query(Added(Health)) // newly-added this frameQuery(Changed(Transform)) // written since the last runRemoved(C) is its own query that yields entities that lost C:
import { defineSystem, Removed } from 'esengine';defineSystem([Removed(Health)], (removed) => { // Health is your own component for (const entity of removed) { /* cleanup */ }});A query result also has convenience methods beyond for…of:
| Method | Returns |
|---|---|
query.single() |
The one match (or null) — for a lone player/camera. |
query.count() / query.isEmpty() |
Match count / whether empty. |
query.toArray() |
All results as an array. |
query.forEach((e, ...c) => …) |
Callback per match. |
Events
Section titled “Events”Events are a typed, decoupled message stream — one system sends, another reads
next frame, with no direct reference between them. Define an event, register it on
the app, then take an EventWriter / EventReader as a system parameter:
import { defineEvent, defineSystem, EventWriter, EventReader, Commands } from 'esengine';
const Damaged = defineEvent<{ entity: number; amount: number }>('Damaged');app.addEvent(Damaged);
// Producer:addSystem(defineSystem([EventWriter(Damaged)], (damaged) => { damaged.send({ entity: 12, amount: 5 });}));
// Consumer (reads what was sent since it last ran):addSystem(defineSystem([EventReader(Damaged), Commands()], (damaged, cmds) => { for (const evt of damaged) { /* evt.entity, evt.amount */ }}));A reader is iterable and also offers .isEmpty() / .toArray(). See the
event-system example for the full producer/consumer pattern.