1use 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 .add_observer(
19 |explode_mines: On<ExplodeMines>,
20 mines: Query<&Mine>,
21 index: Res<SpatialIndex>,
22 mut commands: Commands| {
23 for entity in index.get_nearby(explode_mines.pos) {
25 let mine = mines.get(entity).unwrap();
27 if mine.pos.distance(explode_mines.pos) < mine.size + explode_mines.radius {
28 commands.trigger(Explode { entity });
31 }
32 }
33 },
34 )
35 .add_observer(on_add_mine)
37 .add_observer(on_remove_mine)
40 .run();
41}
42
43#[derive(Component)]
44struct Mine {
45 pos: Vec2,
46 size: f32,
47}
48
49impl Mine {
50 fn random(rand: &mut ChaCha8Rng) -> Self {
51 Mine {
52 pos: Vec2::new(
53 (rand.random::<f32>() - 0.5) * 1200.0,
54 (rand.random::<f32>() - 0.5) * 600.0,
55 ),
56 size: 4.0 + rand.random::<f32>() * 16.0,
57 }
58 }
59}
60
61#[derive(Event)]
63struct ExplodeMines {
64 pos: Vec2,
65 radius: f32,
66}
67
68#[derive(EntityEvent)]
72struct Explode {
73 entity: Entity,
74}
75
76fn setup(mut commands: Commands) {
77 commands.spawn(Camera2d);
78 commands.spawn((
79 Text::new(
80 "Click on a \"Mine\" to trigger it.\n\
81 When it explodes it will trigger all overlapping mines.",
82 ),
83 Node {
84 position_type: PositionType::Absolute,
85 top: px(12),
86 left: px(12),
87 ..default()
88 },
89 ));
90
91 let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);
92
93 commands
94 .spawn(Mine::random(&mut rng))
95 .observe(explode_mine);
99
100 let mut observer = Observer::new(explode_mine);
108
109 for _ in 0..1000 {
111 let entity = commands.spawn(Mine::random(&mut rng)).id();
112 observer.watch_entity(entity);
113 }
114
115 commands.spawn(observer);
117}
118
119fn on_add_mine(add: On<Add, Mine>, query: Query<&Mine>, mut index: ResMut<SpatialIndex>) {
120 let mine = query.get(add.entity).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(add.entity);
126}
127
128fn on_remove_mine(remove: On<Remove, Mine>, query: Query<&Mine>, mut index: ResMut<SpatialIndex>) {
130 let mine = query.get(remove.entity).unwrap();
131 let tile = (
132 (mine.pos.x / CELL_SIZE).floor() as i32,
133 (mine.pos.y / CELL_SIZE).floor() as i32,
134 );
135 index.map.entry(tile).and_modify(|set| {
136 set.remove(&remove.entity);
137 });
138}
139
140fn explode_mine(explode: On<Explode>, query: Query<&Mine>, mut commands: Commands) {
141 let Ok(mut entity) = commands.get_entity(explode.entity) else {
143 return;
144 };
145 info!("Boom! {} exploded.", explode.entity);
146 entity.despawn();
147 let mine = query.get(explode.entity).unwrap();
148 commands.trigger(ExplodeMines {
150 pos: mine.pos,
151 radius: mine.size,
152 });
153}
154
155fn draw_shapes(mut gizmos: Gizmos, mines: Query<&Mine>) {
157 for mine in &mines {
158 gizmos.circle_2d(
159 mine.pos,
160 mine.size,
161 Color::hsl((mine.size - 4.0) / 16.0 * 360.0, 1.0, 0.8),
162 );
163 }
164}
165
166fn handle_click(
168 mouse_button_input: Res<ButtonInput<MouseButton>>,
169 camera: Single<(&Camera, &GlobalTransform)>,
170 windows: Query<&Window>,
171 mut commands: Commands,
172) {
173 let Ok(windows) = windows.single() else {
174 return;
175 };
176
177 let (camera, camera_transform) = *camera;
178 if let Some(pos) = windows
179 .cursor_position()
180 .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
181 .map(|ray| ray.origin.truncate())
182 && mouse_button_input.just_pressed(MouseButton::Left)
183 {
184 commands.trigger(ExplodeMines { pos, radius: 1.0 });
185 }
186}
187
188#[derive(Resource, Default)]
189struct SpatialIndex {
190 map: HashMap<(i32, i32), HashSet<Entity>>,
191}
192
193const CELL_SIZE: f32 = 64.0;
195
196impl SpatialIndex {
197 fn get_nearby(&self, pos: Vec2) -> Vec<Entity> {
199 let tile = (
200 (pos.x / CELL_SIZE).floor() as i32,
201 (pos.y / CELL_SIZE).floor() as i32,
202 );
203 let mut nearby = Vec::new();
204 for x in -1..2 {
205 for y in -1..2 {
206 if let Some(mines) = self.map.get(&(tile.0 + x, tile.1 + y)) {
207 nearby.extend(mines.iter());
208 }
209 }
210 }
211 nearby
212 }
213}