observers/
observers.rs

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