Skip to content

Components

Components are data types registered in a world. They can be used through typed macros or directly through ecs_component_t ids.

Resources use a separate id space and are stored once per world instead of once per entity. Use a component for per-entity data, and a resource for unique world state such as time, input, renderer state, or shared config.

Components are declared once and registered with each world:

ECS_COMPONENT_DECLARE(Position, {
float x;
float y;
});
ECS_COMPONENT_DEFINE(Position);
ECS_COMPONENT_REGISTER(world, Position);

ecs_set() adds the component if needed, then writes the value:

ecs_set(entity, Position, { .x = 1.0f, .y = 2.0f });

ecs_get() and entity.get<T>() assume the component exists:

Position *position = ecs_get(entity, Position);
position->x += 1.0f;

When the component may be absent, use the nullable helpers:

Position *position = ecs_try_get(entity, Position);
if (position != NULL) position->x += 1.0f;

Use the typed operation for components that do not need an immediate value:

ecs_add(entity, Position);

The C++ methods accept several component types or values at once:

entity.set(Position{ 1.0f, 2.0f }, Velocity{ 3.0f, 4.0f });
entity.add<Selected, Visible>();
if (entity.has<Selected, Visible>()) {
entity.remove<Selected, Visible>();
}

Multi-component set, add, and remove calls are deferred as one batch. This lets SIECS compute the final archetype and migrate the entity once instead of once per component.

Check and remove components with:

if (ecs_has(entity, Position)) {
ecs_remove(entity, Position);
}

Removing a component that is not present is a no-op.

Disabled is a built-in component. Adding it to an entity excludes that entity from queries by default:

ecs_add(entity, Disabled);

Queries, systems, and observers skip disabled entities by default. To match them explicitly:

ecs_query({
.terms = { ecs_in(Position), ecs_filter(Disabled) },
});

Use ecs_with() when adding one component should also add another component:

ecs_with(ecs_id(Renderable), ecs_id(Transform));

ecs_add(entity, Renderable);

/* Transform was added first. */

This only affects future adds. It does not rewrite existing entities.

Requirement cycles are invalid. In debug builds, cycles are asserted when calling ecs_with().

For generic code, register a component from a descriptor:

typedef struct {
    float x;
    float y;
} Position;

ecs_component_t position_id = ecs_component({
    .name = "Position",
    .size = sizeof(Position),
});

Then use the _cid functions:

ecs_set_cid(entity, position_id, &(Position){ .x = 1.0f, .y = 2.0f });

Position *position = ecs_get_cid(entity, position_id);

Typed component macros register reflection metadata through sireflect and sijson:

ECS_COMPONENT(Position, {
    float x;
    float y;
});

That metadata is used by the REST explorer to list component schemas and serialize reflected component values. Components registered only with a raw descriptor are reflected only when struct_desc points to a valid sireflect_struct_desc_t produced by the reflection layer:

ecs_component_t position_id = ecs_component({
    .name = "Position",
    .size = sizeof(Position),
    .struct_desc = position_reflection_desc,
});

If a component has no reflection metadata, SIECS can still store and query it, but the REST explorer cannot show or edit its fields.

Component descriptors can provide lifecycle hooks:

static void on_set_position(
    ecs_entity_t entity,
    ecs_component_t component,
    const void *new_value,
    void *current_value
) {
    const Position *incoming = new_value;
    Position *stored = current_value;
    (void)world;
    (void)entity;
    (void)component;
    (void)incoming;
    (void)stored;
}

on_set receives the new value passed to ecs_set() or ecs_set_cid(). The stored value is still the previous value until the hook returns.

on_add receives zero-initialized storage after the component is added. on_remove receives the value that is about to be removed.