325e278af1
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/.
139 lines
4.6 KiB
Markdown
139 lines
4.6 KiB
Markdown
# 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 |
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
commands.trigger(Speak { message: "Hello!".into() });
|
|
world.trigger_ref(&mut event); // inspect after observers mutate
|
|
```
|
|
|
|
### EntityEvent (Entity-Targeted Events)
|
|
|
|
```rust
|
|
#[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 |
|
|
|
|
```rust
|
|
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.
|
|
|
|
```rust
|
|
#[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 `..`:
|
|
```rust
|
|
let EntityComponentsTrigger { components, .. } = e.trigger(); // 0.19
|
|
```
|