Event system
Ark provides an event system with observers that allow an application to react on events, such as adding and removing components and entities.
Observers can filter for the events they are interested in, in several ways. A callback function is executed for the affected entity whenever an observer’s filter matches.
In addition to built-in lifecycle events like OnCreateEntity
or OnAddComponents
,
Ark supports custom event types that enable domain-specific triggers.
These events can be emitted manually and observed with the same filtering and callback mechanisms,
making them ideal for modeling interactions such as user input, synchronization, or game logic.
Observers are lightweight, composable, and follow the same declarative patterns as Ark’s query system. They provide fine-grained control over when and how logic is executed. This design encourages a declarative, data-driven approach while maintaining performance and flexibility.
Example
// Create an observer
ecs.Observe1[Position](ecs.OnCreateEntity).
Do(func(e ecs.Entity, pos *Position) {
fmt.Printf("%#v\n", pos)
}).
Register(&world)
// Create an entity that triggers the observer's callback
builder := ecs.NewMap1[Position](&world)
builder.NewEntity(&Position{X: 10, Y: 11})
Event types
Observers are specific for different event types, and each observer can react only to one type. See below for how to react on multiple different types.
- OnCreateEntity — Emitted after a new entity is created.
- OnRemoveEntity — Emitted before an entity is removed.
- OnAddComponents — Emitted after components are added to an existing entity.
- OnRemoveComponents — Emitted before components are removed from an entity.
- OnSetComponents — Emitted after existing components are set from an entity.
- OnAddRelations — Emitted after relation targets are added to an entity.*
- OnRemoveRelations — Emitted before relation targets are removed from an entity.*
If multiple components are added/removed/set for an entity, one event is emitted for the entire operation.
* Relation events are emitted when entities with relations are created or removed, when relation components are added or removed, as well as when targets are set without changing components.
Combining multiple types
Observers can be combined to react to multiple event types in a single callback function. Below is a combination of observers to react on component addition as well as removal. The callback is set up to be able to distinguish between these event types (if needed).
// Common callback
fn := func(evt ecs.EventType, entity ecs.Entity, pos *Position) {
if evt == ecs.OnAddComponents {
// do something
}
if evt == ecs.OnRemoveComponents {
// do something
}
}
// Observer for adding components
ecs.Observe1[Position](ecs.OnAddComponents).
Do(func(e ecs.Entity, pos *Position) { fn(ecs.OnAddComponents, e, pos) }).
Register(&world)
// Observer for removing components
ecs.Observe1[Position](ecs.OnRemoveComponents).
Do(func(e ecs.Entity, pos *Position) { fn(ecs.OnRemoveComponents, e, pos) }).
Register(&world)
Filters
Observers filter for the components specified by their generic parameters.
Additional components can be specified using Observer.For
,
but these are not directly accessible in the callback.
Observers only trigger when all specified components (in parameters and in For
)
are affected in a single operation.
For example, if an observer watches Position
and Velocity
,
both must be added or removed together for the observer to activate
Further, events can be filtered by the composition of the affected entity via
Observer.With
, Observer.Without
and Observer.Exclusive
, just like queries.
Examples (leaving out observer registration):
Both observers are triggered when an entity with Position
is created.
The first one has direct access to the component in the callback while the second does not:
ecs.Observe1[Position](ecs.OnCreateEntity).
Do(func(e ecs.Entity, p *Position) { /* ... */ })
ecs.Observe(ecs.OnCreateEntity).
With(ecs.C[Position]()).
Do(func(e ecs.Entity) { /* ... */ })
Both observers are triggered when an entity with Position
as well as Velocity
is created:
ecs.Observe2[Position, Velocity](ecs.OnCreateEntity).
Do(func(e ecs.Entity, p *Position, v *Velocity) { /* ... */ })
ecs.Observe1[Position](ecs.OnCreateEntity).
With(ecs.C[Velocity]()).
Do(func(e ecs.Entity, p *Position) { /* ... */ })
An observer that is triggered when any entity is created, irrespective of its components:
ecs.Observe(ecs.OnCreateEntity).
Do(func(e ecs.Entity) { /* ... */ })
An observer that is triggered when a Position
component is added to an existing entity:
ecs.Observe1[Position](ecs.OnAddComponents).
Do(func(e ecs.Entity, p *Position) { /* ... */ })
An observer that is triggered when a Position
component is added to an entity
that has Velocity
, but not Altitude
(or rather, had before the operation):
ecs.Observe1[Position](ecs.OnAddComponents).
With(ecs.C[Velocity]()).
Without(ecs.C[Altitude]()).
Do(func(e ecs.Entity, p *Position) { /* ... */ })
Event timing
The time an event is emitted relative to the operation it is related to depends on the event’s type. The observer callbacks are executed immediately by any emitted event.
Events for entity creation and for adding or setting components are emitted after the operation. Hence, the new or changed components can be inspected in the observer’s callback. If emitted from individual operations, the world is in an unlocked state when the callback is executed. Contrary, when emitted from a batch operation, the world is locked.
Events for entity or component removal are emitted before the operation. This way, the entity or component to be removed can be inspected in the observer’s callback. In this case, the world is locked when the callback is executed.
For batch operations, all events are emitted before or after the entire batch, respectively. For batch creation or addition, events are emitted after the potential batch callback is executed for all entities, allowing to inspect the result.
Note that observer order is undefined. Observers are not necessarily triggered in the same order as they were registered.
Custom events
Custom events in Ark allow developers to define and emit their own event types, enabling application-specific logic such as UI interactions, game state changes, or other domain-specific triggers. These events support the same filtering and observer mechanisms as built-in events.
Define custom event types using EventRegistry.NewEventType
:
// Create an event registry
var registry = ecs.EventRegistry{}
// Create event types
var OnCollisionDetected = registry.NewEventType()
var OnInputReceived = registry.NewEventType()
var OnLevelLoaded = registry.NewEventType()
var OnTimerElapsed = registry.NewEventType()
Ideally, custom event types are stored as global variables of the applications.
Alteratively, if all custom events are defined in one place, constants can be used like this:
const (
OnCollisionDetected ecs.EventType = iota
OnInputReceived
OnLevelLoaded
OnTimerElapsed
)
Use custom events like this:
// Create an event registry
var registry = ecs.EventRegistry{}
// Define the event type
var OnTeleport = registry.NewEventType()
// Add an observer for the event type
ecs.Observe1[Position](OnTeleport).
Do(func(e ecs.Entity, p *Position) { /*...*/ }).
Register(&world)
// Define the event
event := world.Event(OnTeleport).
For(ecs.C[Position]())
// Emit the event for an entity
event.Emit(entity)
Observers might not be interested in components, or in more than one component. This is also supported by custom events:
// Create an event registry
var registry = ecs.EventRegistry{}
// Define the event type
var OnClick = registry.NewEventType()
// Emit a click event
world.Event(OnClick).Emit(uiElement)
Here, the event is created and emitted in a single expression.
However, it is recommended to store events after construction and to reuse them for Emit
.
Reusing event instances is especially important for events with components,
as it avoids repeated lookups and improves runtime efficiency.
The overhead for component ID lookup is ≈20ns per component.
For custom events, observer filters work exactly the same as for predefined events.
The components in the generic parameters of the observer, as well as those defined by For
,
are matched against the components of the event.
With
, Without
and Exclusive
are matched against the entity for which the event is emitted.
Note that custom events can also be emitted for the zero entity:
// Create an event registry
var registry = ecs.EventRegistry{}
// Define the event type
var OnGameOver = registry.NewEventType()
// Emit a game over event
world.Event(OnGameOver).Emit(ecs.Entity{})