Files
reliquary/docs/workpad/research/bevy-skill-research/04-events-observers.md
T
g4borg 325e278af1 Scaffold bevy plugin with bevy-0.19 and bevy-upgrade skills
Add plugin skeleton with versioned skill directories, TODO SKILL.md
scaffolds with goals for skill-creator, and copy over bevy-skill-research
reports from spacecraft into docs/workpad/research/.
2026-07-07 17:52:00 +02:00

4.6 KiB

Bevy Skill Research: Events, Messages & Observers

Bevy version: 0.18, with 0.19 notes
Date: 2026-07-07

The Event/Message Split (0.18 Breaking Change)

In 0.18, the old Event/EventWriter/EventReader system was split into two distinct systems:

Concept Trait Writer Reader Model
Messages #[derive(Message)] MessageWriter<E> MessageReader<E> Pull-based, double-buffered, batch
Events #[derive(Event)] commands.trigger() Observers (On<E>) Push-based, immediate
// Messages — for batch processing, decoupled many-to-many
#[derive(Message)]
struct Collision { entity_a: Entity, entity_b: Entity }

// Events — for push-based observer reactions
#[derive(Event)]
struct Speak { message: String }

Messages (Pull-Based, Buffered)

The renamed successor to the old EventWriter/EventReader pattern.

  • MessageWriter::write(msg) / write_batch(iter) / write_default()
  • MessageReader::read() returns iterator, advances cursor; len(), is_empty(), clear()
  • Double-buffered: messages survive one update() cycle, cleared on the next
  • Multiple readers can run concurrently; writers for the same type cannot
fn send(mut writer: MessageWriter<Collision>) {
    writer.write(Collision { entity_a: a, entity_b: b });
}

fn receive(mut reader: MessageReader<Collision>) {
    for collision in reader.read() {
        // handle
    }
}

Events + Observers (Push-Based, Immediate)

Observer system parameter: On<'w, 't, E, B> where B: Bundle filters by component.

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

Registration

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

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

Triggering

commands.trigger(Speak { message: "Hello!".into() });
world.trigger_ref(&mut event);  // inspect after observers mutate

EntityEvent (Entity-Targeted Events)

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

Supports #[entity_event(propagate)] for automatic hierarchy traversal (event bubbles up through ChildOf chain).

Lifecycle Events (Component Triggers)

Built-in events fired on component lifecycle changes:

Event Fires when Order
Add Component added to entity that lacked it First
Insert Any insert (add or replace) After Add
Replace Component removed regardless of replacement Before Remove
Remove Component removed from entity After Replace
Despawn Entity despawned (per component) Last
world.add_observer(|_: On<Add, Health>| { /* entity just got Health */ });
world.add_observer(|_: On<Remove, Health>| { /* Health was removed */ });

Low-Level Component Hooks

Synchronous callbacks during the ECS operation itself (not scheduled systems). These run inline — not deferred.

#[derive(Component)]
#[component(on_add = my_hook)]
#[component(on_insert = my_hook)]
struct MyComponent;

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

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

When to Use What

Mechanism Model Best for
MessageWriter/MessageReader Pull, scheduled, buffered Batch processing, decoupled many-to-many communication
Event + Observers Push, immediate React NOW to something (structural changes, gameplay events)
Component hooks Synchronous, inline Deterministic callback AS PART OF the ECS operation
Changed<T> / Added<T> filters Pull, per-tick polling Check mutations each frame in a normal system

Rule of thumb: Use Messages when multiple systems need to process the same data independently. Use Events + Observers when you need immediate reaction to something happening. Use hooks when the response must be atomic with the ECS operation.

Migration Notes

0.17 → 0.18: EventWriterMessageWriter, EventReaderMessageReader, EventsMessages. Event trait is now observer-only. on_event run condition → on_message.

0.18 → 0.19: EntityComponentsTrigger gains archetype fields — destructuring requires ..:

let EntityComponentsTrigger { components, .. } = e.trigger(); // 0.19