Skip to content

Observers and Events

Observers run callbacks when an event is emitted for an entity matching the observer query. They are indexed by event and matching table, so a relation filter can be used without scanning unrelated entities.

Event Trigger trigger_data
EcsOnAdd A component is added. Pointer to the added component storage.
EcsOnRemove A component is removed. Pointer to the component storage before removal.
EcsOnSet A component is set or replaced. Pointer to the new value passed to ecs_set() or ecs_set_cid().
EcsOnRelationSet A relation is added or retargeted. Pointer to ecs_relation_event_t, after the transition.
EcsOnRelationRemove A relation is removed. Pointer to ecs_relation_event_t, before the transition.

EcsOnRelationSet is emitted for an addition and for a retarget. Calling ecs_relate() with the current target is a no-op and emits no relation event. Deferred relation changes emit when the command buffer is flushed; multiple deferred changes to one relation are coalesced to the final target.

static void on_position_set(ecs_observer_event_t *event) {
const Position *value = event->trigger_data;
/* value is the new value; storage has not been overwritten yet. */
log_position(event->entity, value);
}
ecs_observer({
.on = EcsOnSet,
.query = { .components = { ecs_in(Position) } },
.callback = on_position_set,
});

Observer queries follow normal component and relation matching rules. Entities with Disabled or Abstract are excluded by default; mention the component explicitly when an observer must include them.

ecs_observer({
.on = EcsOnSet,
.query = {
.components = { ecs_in(Position), ecs_filter(Disabled) },
},
.callback = on_disabled_position_set,
});

Observers are enabled by default. ecs_observer_disable() and ecs_observer_enable() toggle an observer without unregistering it.

The relation payload contains the relation id and both sides of the transition: relation, old_target, and new_target. The event type is ecs_relation_event_t in both language APIs.

The values are:

Operation old_target new_target
Add 0 New target.
Retarget Previous target. New target.
Remove Previous target. 0.

Use a relation query to observe one relation type:

static void on_group_changed(ecs_observer_event_t *event) {
const ecs_relation_event_t *change = event->trigger_data;
if (event->event == EcsOnRelationSet) {
log_relation(event->entity, change->old_target, change->new_target);
} else {
log_relation(event->entity, change->old_target, 0);
}
}
ecs_observer({
.on = EcsOnRelationSet,
.query.relations = { ecs_rel(GroupOf) },
.callback = on_group_changed,
});
ecs_observer({
.on = EcsOnRelationRemove,
.query.relations = { ecs_rel(GroupOf) },
.callback = on_group_changed,
});

Set observers match the entity’s new relation table. Remove observers match the old relation table, like EcsOnRemove matches the component before it is removed. This means an exact ecs_to() filter can identify the old target for a remove event and the new target for a set event.

The C++ wrapper provides ecs::OnRelationSet and ecs::OnRelationRemove. Relation data is read through ecs::observer_event::trigger_data<T>(), as shown in the C++ tab above.

ecs::observer_event and all trigger_data pointers are borrowed and valid only during the callback.

The C callback receives an ecs_observer_event_t with entity, event, user_data, and trigger_data. The C++ callback receives the corresponding ecs::observer_event object. Both payloads are borrowed for the duration of the callback:

Pass a small callback context through user_data:

ecs_observer({
.on = EcsOnSet,
.query.components = { ecs_in(Position) },
.callback = on_position_set,
.user_data = (uintptr_t)counter_ptr,
});

In C++, use user_data() on ecs::observer_event. Component callbacks may also receive an ecs::observer_event argument when they need the entity, event id, or callback context.

Create a custom event id with ecs_event() and trigger it explicitly:

ecs_event_t Damaged = ecs_event();
ecs_observer({
.on = Damaged,
.query = { .components = { ecs_in(Health) } },
.callback = on_damaged,
});
Damage damage = { .amount = 10 };
ecs_observer_trigger(entity, Damaged, &damage);

Custom events use the entity’s current table when they are triggered. Their trigger_data is the pointer supplied to ecs_observer_trigger() or ecs::trigger().