Skip to content

Quickstart

This page is a tour of the SIECS model. It assumes that siecs.h and siecs.c are already part of your application; see Building and integrating for the two supported integration paths.

SIECS uses C17 for the runtime and C++20 for the typed wrapper. Both APIs drive the same active world.

The world owns every entity, component, query, system, resource and observer. Initialize it before any typed operation and destroy it after the application stops.

ecs_init();
/* Create entities, register systems, run frames. */
ecs_fini();

Handles belong to this world. Do not retain an entity or component pointer after ecs_fini().

An entity is an identity. Components are the data attached to that identity. Declare data once, register it in C, then add or set it on entities.

ECS_COMPONENT(Position, { float x; float y; });
ECS_COMPONENT(Velocity, { float x; float y; });
ECS_TAG(Enemy);
ECS_COMPONENT_REGISTER(Position);
ECS_COMPONENT_REGISTER(Velocity);
ECS_COMPONENT_REGISTER(Enemy);
ecs_entity_t enemy = ecs_new();
ecs_set(enemy, Position, { 10.0f, 20.0f });
ecs_set(enemy, Velocity, { 1.0f, 0.0f });
ecs_add(enemy, Enemy);

set adds the component when it is absent. get requires it to exist; use try_get when absence is a valid state.

A system is a named query callback. SIECS invokes it for every non-empty batch of matching entities when the selected phase runs.

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

Run the frame with ecs_progress() or ecs::progress(). The query owned by a system is persistent, so it is the normal choice for repeated work.

Use a query when you need to iterate data outside a scheduled system. Terms express required, optional, filtered, excluded, inherited or relation data.

ecs_query_id_t moving = ecs_query({
.components = { ecs_inout(Position), ecs_in_optional(Velocity) },
});
ecs_iter_t it = ecs_query_iter(moving);
while (ecs_iter_next(&it)) {
Position *position = ecs_field(&it, 0);
const Velocity *velocity = ecs_field(&it, 1);
for (uint32_t i = 0; i < it.count; i++) {
if (velocity) position[i].x += velocity[i].x;
}
}
ecs_query_fini(moving);

Optional fields are present for the whole batch or absent for the whole batch. Do not keep field pointers across a structural change such as add or remove.

A resource is one typed value for the whole world. It is useful for time, configuration, input, renderer state, or other shared state.

ECS_RESOURCE(Time, { float dt; });
ECS_RESOURCE_REGISTER(Time);
ecs_set_resource(Time, { .dt = 1.0f / 60.0f });
const Time *time = ecs_get_resource_read(Time);

Resources are not entity components and do not become query fields. A C++ system can request one directly with ecs::res<const Time>.

Relations connect entities. The built-in ChildOf relation models a hierarchy; custom relations model domain-specific edges such as GroupOf or Targets.

ecs_entity_t parent = ecs_new();
ecs_entity_t child = ecs_new();
ecs_relate(child, ChildOf, parent);
ecs_entity_t current_parent = ecs_target(child, ChildOf);

Inheritance builds reusable abstract bases on top of IsA. Read-only queries can see an inherited value; writable queries require a local override.

Observers react to a specific event instead of running every frame. Built-in events include component add, remove and set, plus relation transitions.

static void OnPositionSet(ecs_observer_event_t *event) {
const Position *value = event->trigger_data;
printf("x = %f\n", value->x);
}
ecs_observer({
.on = EcsOnSet,
.query.components = { ecs_in(Position) },
.callback = OnPositionSet,
});

Observer payloads are borrowed for the callback. Use them immediately; do not store pointers to event data.