Files
reliquary/plugins/bevy/skills/bevy-0.19/references/events-observers.md
T
g4borg c661027d19 🤖 Flesh out bevy-0.19 and bevy-upgrade skills from research
Replace scaffold placeholders with full skill content:
- bevy-0.19: ECS cheat sheet, critical traps table, architecture guide, and 7 reference files
- bevy-upgrade: version migration workflow, transition references for 0.15→0.19, and 3 per-bump reference files
2026-07-07 17:52:10 +02:00

3.8 KiB

Events, Messages, Observers & Hooks

0.18 split the old single event system into Messages (pull, buffered) and Events (push, observed). They are different traits with different derives — using the wrong one won't compile, and porting old code mechanically to the wrong side is an architecture bug.

Messages — pull-based, double-buffered

The renamed successor of EventWriter/EventReader. Use for decoupled many-to-many data flow where several systems batch-process the same stream.

#[derive(Message)]
struct Collision { a: Entity, b: Entity }

fn produce(mut writer: MessageWriter<Collision>) {
    writer.write(Collision { a, b });          // also: write_batch(iter), write_default()
}

fn consume(mut reader: MessageReader<Collision>) {
    for c in reader.read() { /* each reader has its own cursor */ }
}
  • Double-buffered: a message survives one update() cycle and is dropped on the next. A reader that runs before the writer in the same frame sees it next frame.
  • Multiple readers run concurrently; multiple writers of the same type cannot.
  • Reader extras: len(), is_empty(), clear().
  • Run condition: on_message::<M>().

Events — push-based, immediate, observed

#[derive(Event)]
struct Speak { text: String }

// Global observer
app.add_observer(|on: On<Speak>| println!("{}", on.text));

// Entity-scoped observer — fires only for this entity
commands.entity(id).observe(|on: On<Hit>| { /* ... */ });

// Trigger
commands.trigger(Speak { text: "hi".into() });
world.trigger_ref(&mut event);   // inspect the event after observers mutate it

Observer param On<'w, 't, E, B> (B: Bundle filters by component):

  • event() / event_mut() — event data (also Deref<Target = E> for direct field access)
  • trigger() / trigger_mut() — trigger context
  • observer() — the observer entity

Observers run immediately during the trigger, not at a schedule point.

EntityEvent — entity-targeted

#[derive(EntityEvent)]
struct Hit {
    #[event_target]
    entity: Entity,
    damage: f32,
}

#[entity_event(propagate)] makes the event bubble up the ChildOf chain — useful for UI-like hit forwarding.

0.19: EntityComponentsTrigger gained archetype fields — destructure with ..: let EntityComponentsTrigger { components, .. } = e.trigger();

Lifecycle events

Built-in events observers can watch per component:

Event Fires Order
Add component added where absent first
Insert any insert (add or replace) after Add
Replace component removed, regardless of replacement before Remove
Remove component removed after Replace
Despawn entity despawned (per component) last
world.add_observer(|on: On<Add, Health>| { /* entity just gained Health */ });

Component hooks — synchronous, inline

For callbacks that must be atomic with the ECS operation itself (not deferred, not scheduled):

#[derive(Component)]
#[component(on_add = init_health_bar)]
struct Health(f32);

fn init_health_bar(world: DeferredWorld, ctx: HookContext) { /* ... */ }

Available: on_add, on_insert, on_replace, on_remove, on_despawn.

Choosing the mechanism

Mechanism Model Reach for it when
Messages pull, buffered, scheduled several systems independently process a stream; frame-batched work
Event + observer push, immediate something must react now (gameplay reactions, structural responses)
EntityEvent push, targeted, can propagate the reaction belongs to a specific entity
Component hook synchronous, inline the response must not be separable from the ECS operation
Changed<T> / Added<T> per-tick polling cheap frame-wise checks inside a normal system