1use 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 .add_observer(
21 (|explode_mines: On<ExplodeMines>,
22 mines: Query<&Mine>,
23 index: Res<SpatialIndex>,
24 mut commands: Commands| {
25 for entity in index.get_nearby(explode_mines.pos) {
27 let mine = mines.get(entity).unwrap();
29 if mine.pos.distance(explode_mines.pos) < mine.size + explode_mines.radius {
30 commands.trigger(Explode { entity });
33 }
34 }
35 })
36 .run_if(|enabled: Res<ExplosionsEnabled>| enabled.0),
37 )
38 .add_observer(on_add_mine)
40 .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#[derive(Event)]
85struct ExplodeMines {
86 pos: Vec2,
87 radius: f32,
88}
89
90#[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 .observe(explode_mine);
122
123 let mut observer = Observer::new(explode_mine);
131
132 for _ in 0..1000 {
134 let entity = commands.spawn(Mine::random(&mut rng)).id();
135 observer.watch_entity(entity);
136 }
137
138 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
151fn 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 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 commands.trigger(ExplodeMines {
173 pos: mine.pos,
174 radius: mine.size,
175 });
176}
177
178fn 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
189fn 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
216const CELL_SIZE: f32 = 64.0;
218
219impl SpatialIndex {
220 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}