Skip to content

Systems

Systems are named callbacks attached to a query. They are the usual way to run game or application logic over matching entities.

A system callback receives an ecs_iter_t *. Each call gives a batch of matching entities for the system query.

static void move_system(ecs_iter_t *it) {
Position *positions = ecs_field(it, 0);
Velocity *velocities = ecs_field(it, 1);
for (uint32_t i = 0; i < it->count; i++) {
positions[i].x += velocities[i].x;
positions[i].y += velocities[i].y;
}
}
ecs_system_id_t Move = ecs_system({
.name = "Move",
.phase = EcsOnUpdate,
.query = {
.terms = { ecs_inout(Position), ecs_in(Velocity) },
},
.callback = move_system,
});

callback is required for normal C systems. name is optional, but recommended because it makes debugging and traces easier to read.

The query follows the same rules as queries created with ecs_query(): ecs_in, ecs_out, and ecs_inout terms are returned through ecs_field(), ecs_filter terms must exist but are not returned, and ecs_not terms must not exist. Entities with the built-in Disabled component are skipped by default because system queries inherit the implicit ecs_not(Disabled) query rule.

To run a system only on disabled entities, mention Disabled explicitly:

ecs_system({
.name = "PausedDebug",
.phase = EcsOnUpdate,
.query = { .terms = { ecs_in(Position), ecs_filter(Disabled) } },
.callback = paused_debug_system,
});

Resources are unique values stored on the world. In C systems, read them through it->world before the entity loop:

static void move_system(ecs_iter_t *it) {
const Time *time = ecs_get_resource_read(it->world, Time);
Position *positions = ecs_field(it, 0);
const Velocity *velocities = ecs_field(it, 1);
for (uint32_t i = 0; i < it->count; i++) {
positions[i].x += velocities[i].x * time->dt;
}
}

In C++, callbacks can request ecs::res<T> or ecs::res<const T>. Resource parameters do not become query terms and do not consume an ecs_field() index. SIECS resolves them before iterating the query tables.

ecs::system("Move").each([](
ecs::res<const Time> time,
Position &position,
const Velocity &velocity
) {
position.x += velocity.x * time->dt;
});

Use ecs_progress() to run all enabled systems:

ecs_progress();

ecs_progress() runs phases in this order:

PhaseTypical use
EcsPreStartSetup work before start systems. Runs only on the first ecs_progress().
EcsStartOne-time startup logic. Runs only on the first ecs_progress().
EcsPostStartWork that must run after start systems. Runs only on the first ecs_progress().
EcsOnLoadOne-frame load or bootstrap work.
EcsPostLoadWork that must run after load systems.
EcsPreUpdatePrepare state before simulation.
EcsOnUpdateMain simulation logic.
EcsPostUpdateCleanup or derived state after simulation.
EcsPreRenderPrepare render data.
EcsOnRenderRender-facing systems.
EcsPostRenderCleanup after render-facing systems.

You can also run one phase or one system manually:

ecs_run_phase(EcsOnUpdate);
ecs_run_system(Move);

The one-time start rule applies only to ecs_progress(). Calling ecs_run_phase(EcsStart) manually runs that phase normally.

ecs_run_system() ignores phase order and runs only the selected enabled system. This is useful for tests or explicit one-off work.

OnPreUpdate, OnUpdate, OnPostUpdate, and OnRender are compatibility aliases for the matching Ecs* phase names.

Systems in different phases are ordered by the phase list above. Inside one phase, use after when a system depends on another system having run first:

ecs_system_id_t Integrate = ecs_system({
.name = "Integrate",
.phase = EcsOnUpdate,
.query = { .terms = { ecs_inout(Position), ecs_in(Velocity) } },
.callback = integrate_system,
});
ecs_system({
.name = "SyncTransform",
.phase = EcsOnUpdate,
.query = { .terms = { ecs_in(Position), ecs_inout(Transform) } },
.callback = sync_transform_system,
.after = { Integrate },
});

after accepts up to four system ids. System id 0 is reserved, so an omitted or zero-filled after array means no dependency.

Dependencies must point to systems in the same phase. Dependency cycles and cross-phase dependencies are debug assertion failures.

Systems are enabled by default. Create a disabled system with .disabled = true or toggle it later:

ecs_system_id_t Damage = ecs_system({
.name = "Damage",
.phase = EcsOnUpdate,
.query = { .terms = { ecs_inout(Health) } },
.callback = damage_system,
.disabled = true,
});
ecs_system_enable(Damage);
ecs_system_disable(Damage);

Disabled systems are skipped by ecs_progress(), ecs_run_phase(), and ecs_run_system().

This is separate from the Disabled component on entities. A disabled system does not run at all; an entity with Disabled is simply excluded from normal system queries.

System callbacks receive direct pointers to component arrays for the current batch. Updating those component values in place is expected:

Health *health = ecs_field(it, 0);
health[i].value -= 1;

Adding or removing components can move entities between tables and invalidate the current batch pointers. Keep structural changes outside iteration unless the API explicitly documents that the operation is safe for iteration.

typedef struct {
    const char *name;
    ecs_query_desc_t query;
    void (*callback)(ecs_iter_t *);
    ecs_phase_t phase;
    ecs_system_id_t after[4];
    bool disabled;
} ecs_system_desc_t;
FieldMeaning
nameOptional debug name.
queryComponents matched by the system.
callbackFunction called for each non-empty matching batch.
phasePhase used by ecs_progress() and ecs_run_phase().
afterSame-phase systems that must run before this one.
disabledWhen true, the system is created but not run.