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.
Typed components
Section titled “Typed components”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);// Position is declared with ECS_COMPONENT_DECLARE in a shared C header.#include "components.h"
ecs::init();auto entity = ecs::entity::create();entity.set(Position{ .x = 1.0f, .y = 2.0f });auto position_id = ecs::component<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;});struct Position { reflected(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;});ECS_COMPONENT_DECLARE_CPP(Position, ECS_CPP_FIELDS( float x; float y; ), ECS_CPP_METHODS( void reset() { x = 0.0f; y = 0.0f; } float length_squared() const { return x * x + y * 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.
Inheritance policy
Section titled “Inheritance policy”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.
Set and Read
Section titled “Set and Read”ecs_set() adds the component if needed, then writes the value:
ecs_set(entity, Position, { .x = 1.0f, .y = 2.0f });entity.set(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;Position &position = entity.get<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;if (Position *position = entity.try_get<Position>()) { position->x += 1.0f;}Add, remove, and check components
Section titled “Add, remove, and check components”Use the typed operation for components that do not need an immediate value:
ecs_add(entity, Position);entity.add<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);}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);}if (entity.has<Position>()) { entity.remove<Position>();}Removing a component that is not present is a no-op.
Disabled
Section titled “Disabled”Disabled is a built-in component. Adding it to an entity excludes that entity
from queries by default:
ecs_add(entity, Disabled);entity.disable();Queries, systems, and observers skip disabled entities by default. To match them explicitly:
ecs_query({ .components = { ecs_in(Position), ecs_filter(Disabled) },});ecs::query().require<Position>().require<Disabled>().each( [](Position &position) { position.x += 1.0f; });Component requirements
Section titled “Component requirements”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. */ecs::component_requires<Renderable, Transform>();entity.add<Renderable>();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().
ID-based components
Section titled “ID-based components”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),});auto position_id = ecs::component<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);Position value{ .x = 1.0f, .y = 2.0f };entity.set(value);auto &position = entity.get<Position>();Reflection and JSON
Section titled “Reflection and JSON”Typed component macros register reflection metadata through sireflect and
sijson:
ECS_COMPONENT(Position, { float x; float y;});struct Position { reflected(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,});auto position_id = ecs::component<Position>();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);}#include <cstdio>
static void on_set_position(ecs_entity_t, const Position &incoming, Position &stored) { std::printf("old=%f new=%f", stored.x, incoming.x);}
ecs::component_hooks<Position> hooks{ .on_set = on_set_position };auto position_id = ecs::component(hooks);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.