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(Position);

In C++, a type declared with ECS_COMPONENT_DECLARE uses the same C id and descriptor automatically; no second component registration is created. Plain C++ structs can still be registered automatically on first use. Use the reflected(...) declaration when a native C++ type’s fields must be visible to the REST explorer or JSON serialization:

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

When a shared C declaration also needs C++-only methods, use ECS_COMPONENT_DECLARE_CPP. The C view keeps the same layout:

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

The same header remains valid C: the method block is omitted and the C layout, id, descriptor, and reflection metadata remain unchanged. C++ methods must not add instance data.

Components are copied into an inheriting entity by default. Configure a component as shared when all instances should read the same base value:

ECS_COMPONENT_DEFINE(Material,
.inheritance = EcsInheritShared
);

Native C++ components use the equivalent registration option:

ecs::component<Material>({
.inheritance = EcsInheritShared,
});

The policy is fixed when the component is registered. Owned inheritance copies the effective base value during ecs_is_a(); it does not synchronize with the base afterward. Shared inherited fields are read-only in writable queries.

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. The C API expresses the same operation with the id-based calls:

ecs_set(entity, Position, { 1.0f, 2.0f });
ecs_set(entity, Velocity, { 3.0f, 4.0f });
ecs_add(entity, Selected);
ecs_add(entity, Visible);
if (ecs_has(entity, Selected) && ecs_has(entity, Visible)) {
ecs_remove(entity, Selected);
ecs_remove(entity, 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({
.components = { 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,
});

The C++ wrapper creates reflection metadata from reflected(...) declarations. For a runtime descriptor assembled dynamically, use the C descriptor API from C++.

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

Plain C++ structs are stored and queried normally, but have no reflected fields unless they use reflected(...).

Component descriptors can provide lifecycle hooks:

#include <stdio.h>
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;
printf("old=%f new=%f", stored->x, incoming->x);
}

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.