docs/bevy/examples/ecs/observers.rs 7.9 K · 236 lines · raw · history

1 //! Demonstrates how to observe events: both component lifecycle events and custom events.
2
3 use bevy::ecs::entity::EntityHashSet;
4 use bevy::{platform::collections::HashMap, prelude::*};
5 use chacha20::ChaCha8Rng;
6 use rand::{RngExt, SeedableRng};
7
8 fn main() {
9 App::new()
10 .add_plugins(DefaultPlugins)
11 .init_resource::<SpatialIndex>()
12 .init_resource::<ExplosionsEnabled>()
13 .add_systems(Startup, setup)
14 .add_systems(Update, (draw_shapes, handle_click, toggle_explosions))
15 // Observers are systems that run when an event is "triggered". This observer runs whenever
16 // `ExplodeMines` is triggered.
17 //
18 // Observers can have run conditions, just like systems! This observer only runs when
19 // explosions are enabled. Press Space to toggle.
20 .add_observer(
21 (|explode_mines: On<ExplodeMines>,
22 mines: Query<&Mine>,
23 index: Res<SpatialIndex>,
24 mut commands: Commands| {
25 // Access resources
26 for entity in index.get_nearby(explode_mines.pos) {
27 // Run queries
28 let mine = mines.get(entity).unwrap();
29 if mine.pos.distance(explode_mines.pos) < mine.size + explode_mines.radius {
30 // And queue commands, including triggering additional events
31 // Here we trigger the `Explode` event for entity `e`
32 commands.trigger(Explode { entity });
33 }
34 }
35 })
36 .run_if(|enabled: Res<ExplosionsEnabled>| enabled.0),
37 )
38 // This observer runs whenever the `Mine` component is added to an entity, and places it in a simple spatial index.
39 .add_observer(on_add_mine)
40 // This observer runs whenever the `Mine` component is removed from an entity (including despawning it)
41 // and removes it from the spatial index.
42 .add_observer(on_remove_mine)
43 .run();
44 }
45
46 #[derive(Resource)]
47 struct ExplosionsEnabled(bool);
48
49 impl Default for ExplosionsEnabled {
50 fn default() -> Self {
51 Self(true)
52 }
53 }
54
55 fn toggle_explosions(keyboard: Res<ButtonInput<KeyCode>>, mut enabled: ResMut<ExplosionsEnabled>) {
56 if keyboard.just_pressed(KeyCode::Space) {
57 enabled.0 = !enabled.0;
58 info!(
59 "Explosions {}",
60 if enabled.0 { "ENABLED" } else { "DISABLED" }
61 );
62 }
63 }
64
65 #[derive(Component)]
66 struct Mine {
67 pos: Vec2,
68 size: f32,
69 }
70
71 impl Mine {
72 fn random(rand: &mut ChaCha8Rng) -> Self {
73 Mine {
74 pos: Vec2::new(
75 (rand.random::<f32>() - 0.5) * 1200.0,
76 (rand.random::<f32>() - 0.5) * 600.0,
77 ),
78 size: 4.0 + rand.random::<f32>() * 16.0,
79 }
80 }
81 }
82
83 /// This is a normal [`Event`]. Any observer that watches for it will run when it is triggered.
84 #[derive(Event)]
85 struct ExplodeMines {
86 pos: Vec2,
87 radius: f32,
88 }
89
90 /// An [`EntityEvent`] is a specialized type of [`Event`] that can target a specific entity. In addition to
91 /// running normal "top level" observers when it is triggered (which target _any_ entity that Explodes), it will
92 /// also run any observers that target the _specific_ entity for that event.
93 #[derive(EntityEvent)]
94 struct Explode {
95 entity: Entity,
96 }
97
98 fn setup(mut commands: Commands) {
99 commands.spawn(Camera2d);
100 commands.spawn((
101 Text::new(
102 "Click on a \"Mine\" to trigger it.\n\
103 When it explodes it will trigger all overlapping mines.\n\
104 Press Space to toggle explosions (demonstrates observer run conditions).",
105 ),
106 Node {
107 position_type: PositionType::Absolute,
108 top: px(12),
109 left: px(12),
110 ..default()
111 },
112 ));
113
114 let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);
115
116 commands
117 .spawn(Mine::random(&mut rng))
118 // Observers can watch for events targeting a specific entity.
119 // This will create a new observer that runs whenever the Explode event
120 // is triggered for this spawned entity.
121 .observe(explode_mine);
122
123 // We want to spawn a bunch of mines. We could just call the code above for each of them.
124 // That would create a new observer instance for every Mine entity. Having duplicate observers
125 // generally isn't worth worrying about as the overhead is low. But if you want to be maximally efficient,
126 // you can reuse observers across entities.
127 //
128 // First, observers are actually just entities with the Observer component! The `observe()` functions
129 // you've seen so far in this example are just shorthand for manually spawning an observer.
130 let mut observer = Observer::new(explode_mine);
131
132 // As we spawn entities, we can make this observer watch each of them:
133 for _ in 0..1000 {
134 let entity = commands.spawn(Mine::random(&mut rng)).id();
135 observer.watch_entity(entity);
136 }
137
138 // By spawning the Observer component, it becomes active!
139 commands.spawn(observer);
140 }
141
142 fn on_add_mine(add: On<Add<Mine>>, query: Query<&Mine>, mut index: ResMut<SpatialIndex>) {
143 let mine = query.get(add.entity).unwrap();
144 let tile = (
145 (mine.pos.x / CELL_SIZE).floor() as i32,
146 (mine.pos.y / CELL_SIZE).floor() as i32,
147 );
148 index.map.entry(tile).or_default().insert(add.entity);
149 }
150
151 // Remove despawned mines from our index
152 fn on_remove_mine(remove: On<Remove<Mine>>, query: Query<&Mine>, mut index: ResMut<SpatialIndex>) {
153 let mine = query.get(remove.entity).unwrap();
154 let tile = (
155 (mine.pos.x / CELL_SIZE).floor() as i32,
156 (mine.pos.y / CELL_SIZE).floor() as i32,
157 );
158 index.map.entry(tile).and_modify(|set| {
159 set.remove(&remove.entity);
160 });
161 }
162
163 fn explode_mine(explode: On<Explode>, query: Query<&Mine>, mut commands: Commands) {
164 // Explode is an EntityEvent. `explode.entity` is the entity that Explode was triggered for.
165 let Ok(mut entity) = commands.get_entity(explode.entity) else {
166 return;
167 };
168 info!("Boom! {} exploded.", explode.entity);
169 entity.despawn();
170 let mine = query.get(explode.entity).unwrap();
171 // Trigger another explosion cascade.
172 commands.trigger(ExplodeMines {
173 pos: mine.pos,
174 radius: mine.size,
175 });
176 }
177
178 // Draw a circle for each mine using `Gizmos`
179 fn draw_shapes(mut gizmos: Gizmos, mines: Query<&Mine>) {
180 for mine in &mines {
181 gizmos.circle_2d(
182 mine.pos,
183 mine.size,
184 Color::hsl((mine.size - 4.0) / 16.0 * 360.0, 1.0, 0.8),
185 );
186 }
187 }
188
189 // Trigger `ExplodeMines` at the position of a given click
190 fn handle_click(
191 mouse_button_input: Res<ButtonInput<MouseButton>>,
192 camera: Single<(&Camera, &GlobalTransform)>,
193 windows: Query<&Window>,
194 mut commands: Commands,
195 ) {
196 let Ok(windows) = windows.single() else {
197 return;
198 };
199
200 let (camera, camera_transform) = *camera;
201 if let Some(pos) = windows
202 .cursor_position()
203 .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
204 .map(|ray| ray.origin.truncate())
205 && mouse_button_input.just_pressed(MouseButton::Left)
206 {
207 commands.trigger(ExplodeMines { pos, radius: 1.0 });
208 }
209 }
210
211 #[derive(Resource, Default)]
212 struct SpatialIndex {
213 map: HashMap<(i32, i32), EntityHashSet>,
214 }
215
216 /// Cell size has to be bigger than any `TriggerMine::radius`
217 const CELL_SIZE: f32 = 64.0;
218
219 impl SpatialIndex {
220 // Lookup all entities within adjacent cells of our spatial index
221 fn get_nearby(&self, pos: Vec2) -> Vec<Entity> {
222 let tile = (
223 (pos.x / CELL_SIZE).floor() as i32,
224 (pos.y / CELL_SIZE).floor() as i32,
225 );
226 let mut nearby = Vec::new();
227 for x in -1..2 {
228 for y in -1..2 {
229 if let Some(mines) = self.map.get(&(tile.0 + x, tile.1 + y)) {
230 nearby.extend(mines.iter());
231 }
232 }
233 }
234 nearby
235 }
236 }