docs/bevy/examples/ecs/observer_propagation.rs 4.3 K · 122 lines · raw · history

1 //! Demonstrates how to propagate events through the hierarchy with observers.
2
3 use std::time::Duration;
4
5 use bevy::{log::LogPlugin, prelude::*, time::common_conditions::on_timer};
6 use rand::{rng, seq::IteratorRandom, RngExt};
7
8 fn 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.
27 fn 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 //
46 // We enable propagation by adding the event attribute and specifying two important pieces of information.
47 //
48 // - **propagate:**
49 // Enables the default propagation behavior ("bubbling" up from child to parent using the ChildOf component).
50 //
51 // - **auto_propagate:**
52 // We can also choose whether or not this event will propagate by default when triggered. If this is
53 // false, it will only propagate following a call to `On::propagate(true)`.
54 #[derive(Clone, Component, EntityEvent)]
55 #[entity_event(propagate, auto_propagate)]
56 struct Attack {
57 entity: Entity,
58 damage: u16,
59 }
60
61 /// An entity that can take damage.
62 #[derive(Component, Deref, DerefMut)]
63 struct HitPoints(u16);
64
65 /// For damage to reach the wearer, it must exceed the armor.
66 #[derive(Component, Deref)]
67 struct Armor(u16);
68
69 /// A normal bevy system that attacks a piece of the goblin's armor on a timer.
70 fn attack_armor(entities: Query<Entity, With<Armor>>, mut commands: Commands) {
71 let mut rng = rng();
72 if let Some(entity) = entities.iter().choose(&mut rng) {
73 let damage = rng.random_range(1..20);
74 commands.trigger(Attack { damage, entity });
75 info!("⚔️ Attack for {} damage", damage);
76 }
77 }
78
79 fn attack_hits(attack: On<Attack>, name: Query<&Name>) {
80 if let Ok(name) = name.get(attack.entity) {
81 info!("Attack hit {}", name);
82 }
83 }
84
85 /// A callback placed on [`Armor`], checking if it absorbed all the [`Attack`] damage.
86 fn block_attack(mut attack: On<Attack>, armor: Query<(&Armor, &Name)>) {
87 let (armor, name) = armor.get(attack.entity).unwrap();
88 let damage = attack.damage.saturating_sub(**armor);
89 if damage > 0 {
90 info!("🩸 {} damage passed through {}", damage, name);
91 // The attack isn't stopped by the armor. We reduce the damage of the attack, and allow
92 // it to continue on to the goblin.
93 attack.damage = damage;
94 } else {
95 info!("🛡️ {} damage blocked by {}", attack.damage, name);
96 // Armor stopped the attack, the event stops here.
97 attack.propagate(false);
98 info!("(propagation halted early)\n");
99 }
100 }
101
102 /// A callback on the armor wearer, triggered when a piece of armor is not able to block an attack,
103 /// or the wearer is attacked directly.
104 fn take_damage(
105 attack: On<Attack>,
106 mut hp: Query<(&mut HitPoints, &Name)>,
107 mut commands: Commands,
108 mut app_exit: MessageWriter<AppExit>,
109 ) {
110 let (mut hp, name) = hp.get_mut(attack.entity).unwrap();
111 **hp = hp.saturating_sub(attack.damage);
112
113 if **hp > 0 {
114 info!("{} has {:.1} HP", name, hp.0);
115 } else {
116 warn!("💀 {} has died a gruesome death", name);
117 commands.entity(attack.entity).despawn();
118 app_exit.write(AppExit::Success);
119 }
120
121 info!("(propagation reached root)\n");
122 }