observer_propagation/
observer_propagation.rs

1//! Demonstrates how to propagate events through the hierarchy with observers.
2
3use std::time::Duration;
4
5use bevy::{log::LogPlugin, prelude::*, time::common_conditions::on_timer};
6use rand::{seq::IteratorRandom, thread_rng, Rng};
7
8fn main() {
9    App::new()
10        .add_plugins((MinimalPlugins, LogPlugin::default()))
11        .add_systems(Startup, setup)
12        .add_systems(
13            Update,
14            attack_armor.run_if(on_timer(Duration::from_millis(200))),
15        )
16        // Add a global observer that will emit a line whenever an attack hits an entity.
17        .add_observer(attack_hits)
18        .run();
19}
20
21// In this example, we spawn a goblin wearing different pieces of armor. Each piece of armor
22// is represented as a child entity, with an `Armor` component.
23//
24// We're going to model how attack damage can be partially blocked by the goblin's armor using
25// event bubbling. Our events will target the armor, and if the armor isn't strong enough to block
26// the attack it will continue up and hit the goblin.
27fn setup(mut commands: Commands) {
28    commands
29        .spawn((Name::new("Goblin"), HitPoints(50)))
30        .observe(take_damage)
31        .with_children(|parent| {
32            parent
33                .spawn((Name::new("Helmet"), Armor(5)))
34                .observe(block_attack);
35            parent
36                .spawn((Name::new("Socks"), Armor(10)))
37                .observe(block_attack);
38            parent
39                .spawn((Name::new("Shirt"), Armor(15)))
40                .observe(block_attack);
41        });
42}
43
44// This event represents an attack we want to "bubble" up from the armor to the goblin.
45#[derive(Clone, Component)]
46struct Attack {
47    damage: u16,
48}
49
50// We enable propagation by implementing `Event` manually (rather than using a derive) and specifying
51// two important pieces of information:
52impl Event for Attack {
53    // 1. Which component we want to propagate along. In this case, we want to "bubble" (meaning propagate
54    //    from child to parent) so we use the `Parent` component for propagation. The component supplied
55    //    must implement the `Traversal` trait.
56    type Traversal = &'static Parent;
57    // 2. We can also choose whether or not this event will propagate by default when triggered. If this is
58    //    false, it will only propagate following a call to `Trigger::propagate(true)`.
59    const AUTO_PROPAGATE: bool = true;
60}
61
62/// An entity that can take damage.
63#[derive(Component, Deref, DerefMut)]
64struct HitPoints(u16);
65
66/// For damage to reach the wearer, it must exceed the armor.
67#[derive(Component, Deref)]
68struct Armor(u16);
69
70/// A normal bevy system that attacks a piece of the goblin's armor on a timer.
71fn attack_armor(entities: Query<Entity, With<Armor>>, mut commands: Commands) {
72    let mut rng = thread_rng();
73    if let Some(target) = entities.iter().choose(&mut rng) {
74        let damage = rng.gen_range(1..20);
75        commands.trigger_targets(Attack { damage }, target);
76        info!("⚔️  Attack for {} damage", damage);
77    }
78}
79
80fn attack_hits(trigger: Trigger<Attack>, name: Query<&Name>) {
81    if let Ok(name) = name.get(trigger.entity()) {
82        info!("Attack hit {}", name);
83    }
84}
85
86/// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage.
87fn block_attack(mut trigger: Trigger<Attack>, armor: Query<(&Armor, &Name)>) {
88    let (armor, name) = armor.get(trigger.entity()).unwrap();
89    let attack = trigger.event_mut();
90    let damage = attack.damage.saturating_sub(**armor);
91    if damage > 0 {
92        info!("🩸 {} damage passed through {}", damage, name);
93        // The attack isn't stopped by the armor. We reduce the damage of the attack, and allow
94        // it to continue on to the goblin.
95        attack.damage = damage;
96    } else {
97        info!("🛡️  {} damage blocked by {}", attack.damage, name);
98        // Armor stopped the attack, the event stops here.
99        trigger.propagate(false);
100        info!("(propagation halted early)\n");
101    }
102}
103
104/// A callback on the armor wearer, triggered when a piece of armor is not able to block an attack,
105/// or the wearer is attacked directly.
106fn take_damage(
107    trigger: Trigger<Attack>,
108    mut hp: Query<(&mut HitPoints, &Name)>,
109    mut commands: Commands,
110    mut app_exit: EventWriter<AppExit>,
111) {
112    let attack = trigger.event();
113    let (mut hp, name) = hp.get_mut(trigger.entity()).unwrap();
114    **hp = hp.saturating_sub(attack.damage);
115
116    if **hp > 0 {
117        info!("{} has {:.1} HP", name, hp.0);
118    } else {
119        warn!("💀 {} has died a gruesome death", name);
120        commands.entity(trigger.entity()).despawn_recursive();
121        app_exit.send(AppExit::Success);
122    }
123
124    info!("(propagation reached root)\n");
125}