- Fix `ref` keyword used as variable name in code examples - Fix "retries next tick" → systems continue on normal schedule - Fix `State::set()` → `NextState::set()` in upgrade skill - Fix `SceneRoot` → `WorldAssetRoot` for 0.19 consistency - Fix Replace lifecycle event description (overwritten, not removed) - Fix grep patterns (quoting, trailing spaces) - Remove third-party physics/networking content (not Bevy built-ins)
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 (alsoDeref<Target = E>for direct field access)trigger()/trigger_mut()— trigger contextobserver()— 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 value overwritten by a new insert over an existing value | 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 |