Skip to content

Commit

Permalink
Add code samples to hooks-and-observers release notes (bevyengine#1437)
Browse files Browse the repository at this point in the history
Co-authored-by: iiYese <[email protected]>
  • Loading branch information
alice-i-cecile and iiYese authored Jun 20, 2024
1 parent 66ed6e0 commit 79a5b7a
Showing 1 changed file with 106 additions and 8 deletions.
114 changes: 106 additions & 8 deletions release-content/0.14/release-notes/10756_hooks_and_observers.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ These are intended for enforcing lower level ECS invariants required to use comp
Hooks always run before observers and cannot be removed and so are more suitable for maintaining critical safety or correctness invariants.
Additionally, hooks are somewhat faster than observers, as their reduced flexibility means that fewer lookups are involved.

By contrast, observers are a flexible tool intended for gameplay logic.
They can listen to the same lifecycle events as hooks, but can also respond to custom, user-defined triggers.
Observers can be attached to a single entity, listening only to triggers targeted at that entity (callbacks anyone?), but they can also be used to listen for triggers without an associated entity.
Their advantages over buffered events are clearest when combined with commands that emit triggers (to avoid ever entering a bad state),
or when you're taking advantage of observers' ability to emit triggers which are then immediately processed, chaining recursively.

Let's examine a simple example where we care about maintaining invariants: trying to target a specific `Enemy`.

```rust
Expand All @@ -62,13 +56,117 @@ We want to automatically clear the target when it's despawned (or made untargeta

If we use a pull-based approach (`RemovedComponents` is the most natural here), there can be gaps (within a single frame) between the entity being despawned and the `Target` component being updated.
This can lead to all sorts of bizarre bugs: fun for speedrunners, but not for game developers.
Instead, we can set up a hook on the `Targetable` component: whenever it is despawned, go through the list of entities stored in the `targeted_by` field and set their `Target` to `None`.

Let's see what this looks like with a hook on `Targetable`:

```rust
// Rather than a derive, let's configure the hooks with a custom implementation of Component
impl Component for Targetable {
const STORAGE_TYPE: StorageType = StorageType::Table;

fn register_component_hooks(hooks: &mut ComponentHooks) {
// Whenever this component is removed, or an entity with this component is despawned...
hooks.on_remove(|mut world, targeted_entity, _component_id|{
// Grab the data that's about to be removed
let targetable = world.get::<Targetable>(targeted_entity).unwrap();
for targeting_entity in targetable.targeted_by {
// Track down the entity that's targeting us
let mut targeting = world.get::<Targeting>(targeting_entity).unwrap();
// And clear its target, cleaning up any dangling references
targeting.0 = None;
}
})
}
}
```

Because adding and removing components can only be done in the context of exclusive world access, hooks are always run *immediately*, leaving no opportunity for desynchronization.

By contrast, observers are a flexible tool intended for higher level application logic.
They can listen to the same lifecycle events as hooks, but can also respond to custom, user-defined triggers.
Their advantages over buffered events are clearest when you're targeting a specific entity,
when combined with commands that emit triggers (to avoid ever entering a bad state),
or when you're taking advantage of observers' ability to emit triggers which are then immediately processed, chaining recursively.

Let's examine the API through a simple gameplay-flavored example:

```rust
// Any event type can be used as a trigger
#[derive(Event)]
struct DealDamage {
damage: u8
}

#[derive(Event)]
struct LoseLife {
life_lost: u8,
}

#[derive(Event)]
struct PlayerDeath;

// Observers are stored as components on entities,
// and can be set up to watch specific entities.
fn spawn_player(mut commands: Commands) {
let player_entity = commands
// Setting up some ordinary components
.spawn((Player, Life(10), Defense(2))).id();

// Now, we're adding some callback-style behavior using observers,
// watching the entity itself.
// By attaching the observer to the entity it's watching, we ensure that it gets cleaned up.
commands.insert(Observer::new(respond_to_damage_taken).with_entity(player_entity));
commands.insert(Observer::new(respond_to_life_lost).with_entity(player_entity));
}

// We can send triggers using commands (or methods on `World`)
fn attack_player(monster_query: Query<&Damage, With<Monster>>, player_query: Query<Entity, With<Player>>, mut commands: Commands) {
let player_entity = player_query.single();

for damage in monster_query {
// We could target multiple entities here just as easily!
commands.trigger_targets(DealDamage {damage}, player_entity);
}
}

// Observers use system syntax, with a special `Trigger` parameter as the first param,
// and can request any other `SystemParam` from the world
fn respond_to_damage_taken(trigger: Trigger<DealDamage>, query: Query<&Defense>, mut commands: Commands) {
// We can access information about the entity responding to the event by reading data from the trigger,
// and combining it with additional queries
let defense = query.get(trigger.entity).unwrap_or_default();
let damage = trigger.event().damage;
let life_lost = damage.0.saturating_sub(defense.0);
// Observers can be chained into each other, by sending more triggers using commands
commands.trigger_targets(trigger.entity)
}

fn respond_to_losing_life(trigger: Trigger<LoseLife>, mut life_query: Query<&mut Life>, player_query: Query<Entity, With<Player>>, mut commands: Commands) {
let mut life = life_query.single_mut(trigger.entity);
let life_lost = trigger.event().life_lost;
life.0 = life.0.saturating_sub(life_lost);

if player_query.contains(trigger.entity){
// Triggers can be sent globally, targeting no entity in particular
commands.trigger(PlayerDeath);
}
}

app
.add_systems(Startup, spawn_player)
.add_systems(Update, attack_player)
// Similarly, observers can also be registered globally, listening to any matching event,
// regardless of its entity target
.observe(|_trigger: Trigger<PlayerDeath>, app_exit: EventWriter<AppExit>| {
println!("You died. Game over!")
app_exit.send_default();
});
```

In the future, we intend to use hooks and observers to [replace `RemovedComponents`], [make our hierarchy management more robust], create a first-party replacement for [`bevy_eventlistener`] as part of our UI work and [build out relations].
These are powerful, abstract tools: we can't wait to see the mad science the community cooks up!

When you're ready to get started, check out the [`component hooks`] and [`observers`] examples for the API details.
When you're ready to get started, check out the [`component hooks`] and [`observers`] examples for more API details.

[`Event`]: https://dev-docs.bevyengine.org/bevy/ecs/event/trait.Event.html
[`Added`]: https://dev-docs.bevyengine.org/bevy/ecs/prelude/struct.Added.html
Expand Down

0 comments on commit 79a5b7a

Please sign in to comment.