Skip to main content

observers/
observers.rs

1//! Demonstrates how to observe events: both component lifecycle events and custom events.
2
3use bevy::ecs::entity::EntityHashSet;
4use bevy::{platform::collections::HashMap, prelude::*};
5use chacha20::ChaCha8Rng;
6use rand::{RngExt, SeedableRng};
7
8fn 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)]
47struct ExplosionsEnabled(bool);
48
49impl Default for ExplosionsEnabled {
50    fn default() -> Self {
51        Self(true)
52    }
53}
54
55fn 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)]
66struct Mine {
67    pos: Vec2,
68    size: f32,
69}
70
71impl 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)]
85struct 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)]
94struct Explode {
95    entity: Entity,
96}
97
98fn 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
142fn 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
152fn 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
163fn 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`
179fn 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
190fn 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)]
212struct SpatialIndex {
213    map: HashMap<(i32, i32), EntityHashSet>,
214}
215
216/// Cell size has to be bigger than any `TriggerMine::radius`
217const CELL_SIZE: f32 = 64.0;
218
219impl 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}