✨ 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/.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user