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/.
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 datatrigger()/trigger_mut()— access trigger contextobserver()— 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: EventWriter → MessageWriter, EventReader → MessageReader, Events → Messages. 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