Skip to content

Relations

Relations connect a source entity to one target entity. They use a separate relation id space and are changed with ecs_relate() and ecs_unrelate(), not with component ecs_set().

SIECS registers the built-in ChildOf relation during world initialization. It is acyclic, uses ByDepth storage, and deleting a parent deletes its sources.

ecs_relate(child, ChildOf, parent);
ecs_entity_t parent = ecs_target(child, ChildOf);
ecs_unrelate(child, ChildOf);
ECS_RELATION_DECLARE(GroupOf);
ECS_RELATION_DEFINE(GroupOf, {
.storage = EcsRelationByTarget,
.on_delete_target = EcsRemoveRelation,
});
ecs_init();
ECS_RELATION_REGISTER(GroupOf);
ecs_relate(member, GroupOf, group);

For runtime-created relations, use ecs_relation_init() with an ecs_relation_desc_t. A declared relation must be registered once per world with ECS_RELATION_REGISTER().

Choose the storage mode according to how the relation is queried:

Storage Use Query support
EcsRelationDense The target is stored in a dense per-source column. ecs_rel; ecs_targets() returns contiguous target records.
EcsRelationByDepth Acyclic hierarchies where depth and depth order matter. ecs_rel, ecs_depth, ecs_targets(), and ecs_order_by_depth().
EcsRelationByTarget Many sources share a target and exact target queries or target ordering are common. ecs_rel, ecs_to(), ecs_up(), and ecs_order_by_target().

EcsRelationByDepth requires .acyclic = true. ecs_to() requires ByTarget; ecs_depth() and ecs_order_by_depth() require ByDepth.

The relation target is always an entity handle. It must be live when passed to ecs_relate(). Retargeting replaces the existing target for that relation; calling ecs_relate() with the current target is a no-op.

if (ecs_has_relation(source, GroupOf)) {
ecs_entity_t target = ecs_target(source, GroupOf);
/* target includes its generation. */
}
if (ecs_has_relation_to(source, GroupOf, group)) {
/* source points exactly to group. */
}

Use the id-based variants when the relation id is dynamic: ecs_has_relation_id(), ecs_has_relation_to_id(), ecs_target_id(), ecs_relate_id(), and ecs_unrelate_id().

Relation terms are filters and do not create component fields:

ecs_query_id_t members = ecs_query({
.relations = { ecs_to(GroupOf, group) },
});

The available terms are ecs_rel() for presence, ecs_rel_opt() for an optional relation, ecs_not_rel() for absence, ecs_to() for an exact ByTarget target, and ecs_depth() for a ByDepth depth. Use ecs_targets() for contiguous Dense or ByDepth target records, and ecs_target_shared() for the target shared by a ByTarget batch.

For C++, use .with_relation<Relation>(), .to<Relation>(target), .depth<Relation>(depth), .order_by_depth<Relation>(), and .order_by_target<Relation>() on ecs::query.

The on_delete_target policy controls what happens when a target is killed:

Policy Effect
EcsRemoveRelation Remove the relation from every source. Sources remain alive.
EcsDeleteSources Kill every source that points to the target.

Removing or retargeting a relation also updates the runtime bookkeeping used by the relation storage. Relation transitions caused by target deletion emit the same relation observer events as explicit ecs_unrelate() calls.

Relation changes can be deferred while a system or an explicit defer scope is active:

ecs_defer_begin();
ecs_relate(source, GroupOf, first_group);
ecs_relate(source, GroupOf, final_group);
ecs_defer_end();

Multiple deferred changes to the same relation are coalesced to the final target before the command buffer is flushed.