1use bevy_app::prelude::*;
19use bevy_ecs::{
20 entity::{EntityHashMap, EntityHashSet},
21 prelude::*,
22 system::SystemParam,
23};
24use bevy_math::CompassOctant;
25use thiserror::Error;
26
27use crate::InputFocus;
28
29#[cfg(feature = "bevy_reflect")]
30use bevy_reflect::{prelude::*, Reflect};
31
32#[derive(Default)]
34pub struct DirectionalNavigationPlugin;
35
36impl Plugin for DirectionalNavigationPlugin {
37 fn build(&self, app: &mut App) {
38 app.init_resource::<DirectionalNavigationMap>();
39
40 #[cfg(feature = "bevy_reflect")]
41 app.register_type::<NavNeighbors>()
42 .register_type::<DirectionalNavigationMap>();
43 }
44}
45
46#[derive(Default, Debug, Clone, PartialEq)]
48#[cfg_attr(
49 feature = "bevy_reflect",
50 derive(Reflect),
51 reflect(Default, Debug, PartialEq, Clone)
52)]
53pub struct NavNeighbors {
54 pub neighbors: [Option<Entity>; 8],
61}
62
63impl NavNeighbors {
64 pub const EMPTY: NavNeighbors = NavNeighbors {
66 neighbors: [None; 8],
67 };
68
69 pub const fn get(&self, octant: CompassOctant) -> Option<Entity> {
71 self.neighbors[octant.to_index()]
72 }
73
74 pub const fn set(&mut self, octant: CompassOctant, entity: Entity) {
76 self.neighbors[octant.to_index()] = Some(entity);
77 }
78}
79
80#[derive(Resource, Debug, Default, Clone, PartialEq)]
94#[cfg_attr(
95 feature = "bevy_reflect",
96 derive(Reflect),
97 reflect(Resource, Debug, Default, PartialEq, Clone)
98)]
99pub struct DirectionalNavigationMap {
100 pub neighbors: EntityHashMap<NavNeighbors>,
105}
106
107impl DirectionalNavigationMap {
108 pub fn remove(&mut self, entity: Entity) {
117 self.neighbors.remove(&entity);
118
119 for node in self.neighbors.values_mut() {
120 for neighbor in node.neighbors.iter_mut() {
121 if *neighbor == Some(entity) {
122 *neighbor = None;
123 }
124 }
125 }
126 }
127
128 pub fn remove_multiple(&mut self, entities: EntityHashSet) {
136 for entity in &entities {
137 self.neighbors.remove(entity);
138 }
139
140 for node in self.neighbors.values_mut() {
141 for neighbor in node.neighbors.iter_mut() {
142 if let Some(entity) = *neighbor {
143 if entities.contains(&entity) {
144 *neighbor = None;
145 }
146 }
147 }
148 }
149 }
150
151 pub fn clear(&mut self) {
153 self.neighbors.clear();
154 }
155
156 pub fn add_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
162 self.neighbors
163 .entry(a)
164 .or_insert(NavNeighbors::EMPTY)
165 .set(direction, b);
166 }
167
168 pub fn add_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
173 self.add_edge(a, b, direction);
174 self.add_edge(b, a, direction.opposite());
175 }
176
177 pub fn add_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
181 for pair in entities.windows(2) {
182 self.add_symmetrical_edge(pair[0], pair[1], direction);
183 }
184 }
185
186 pub fn add_looping_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
190 self.add_edges(entities, direction);
191 if let Some((first_entity, rest)) = entities.split_first() {
192 if let Some(last_entity) = rest.last() {
193 self.add_symmetrical_edge(*last_entity, *first_entity, direction);
194 }
195 }
196 }
197
198 pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> Option<Entity> {
200 self.neighbors
201 .get(&focus)
202 .and_then(|neighbors| neighbors.get(octant))
203 }
204
205 pub fn get_neighbors(&self, entity: Entity) -> Option<&NavNeighbors> {
210 self.neighbors.get(&entity)
211 }
212}
213
214#[derive(SystemParam, Debug)]
216pub struct DirectionalNavigation<'w> {
217 pub focus: ResMut<'w, InputFocus>,
219 pub map: Res<'w, DirectionalNavigationMap>,
221}
222
223impl DirectionalNavigation<'_> {
224 pub fn navigate(
231 &mut self,
232 direction: CompassOctant,
233 ) -> Result<Entity, DirectionalNavigationError> {
234 if let Some(current_focus) = self.focus.0 {
235 if let Some(new_focus) = self.map.get_neighbor(current_focus, direction) {
236 self.focus.set(new_focus);
237 Ok(new_focus)
238 } else {
239 Err(DirectionalNavigationError::NoNeighborInDirection {
240 current_focus,
241 direction,
242 })
243 }
244 } else {
245 Err(DirectionalNavigationError::NoFocus)
246 }
247 }
248}
249
250#[derive(Debug, PartialEq, Clone, Error)]
252pub enum DirectionalNavigationError {
253 #[error("No focusable entity is currently set.")]
255 NoFocus,
256 #[error("No neighbor from {current_focus} in the {direction:?} direction.")]
258 NoNeighborInDirection {
259 current_focus: Entity,
261 direction: CompassOctant,
263 },
264}
265
266#[cfg(test)]
267mod tests {
268 use bevy_ecs::system::RunSystemOnce;
269
270 use super::*;
271
272 #[test]
273 fn setting_and_getting_nav_neighbors() {
274 let mut neighbors = NavNeighbors::EMPTY;
275 assert_eq!(neighbors.get(CompassOctant::SouthEast), None);
276
277 neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER);
278
279 for i in 0..8 {
280 if i == CompassOctant::SouthEast.to_index() {
281 assert_eq!(
282 neighbors.get(CompassOctant::SouthEast),
283 Some(Entity::PLACEHOLDER)
284 );
285 } else {
286 assert_eq!(neighbors.get(CompassOctant::from_index(i).unwrap()), None);
287 }
288 }
289 }
290
291 #[test]
292 fn simple_set_and_get_navmap() {
293 let mut world = World::new();
294 let a = world.spawn_empty().id();
295 let b = world.spawn_empty().id();
296
297 let mut map = DirectionalNavigationMap::default();
298 map.add_edge(a, b, CompassOctant::SouthEast);
299
300 assert_eq!(map.get_neighbor(a, CompassOctant::SouthEast), Some(b));
301 assert_eq!(
302 map.get_neighbor(b, CompassOctant::SouthEast.opposite()),
303 None
304 );
305 }
306
307 #[test]
308 fn symmetrical_edges() {
309 let mut world = World::new();
310 let a = world.spawn_empty().id();
311 let b = world.spawn_empty().id();
312
313 let mut map = DirectionalNavigationMap::default();
314 map.add_symmetrical_edge(a, b, CompassOctant::North);
315
316 assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
317 assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
318 }
319
320 #[test]
321 fn remove_nodes() {
322 let mut world = World::new();
323 let a = world.spawn_empty().id();
324 let b = world.spawn_empty().id();
325
326 let mut map = DirectionalNavigationMap::default();
327 map.add_edge(a, b, CompassOctant::North);
328 map.add_edge(b, a, CompassOctant::South);
329
330 assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
331 assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
332
333 map.remove(b);
334
335 assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
336 assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
337 }
338
339 #[test]
340 fn remove_multiple_nodes() {
341 let mut world = World::new();
342 let a = world.spawn_empty().id();
343 let b = world.spawn_empty().id();
344 let c = world.spawn_empty().id();
345
346 let mut map = DirectionalNavigationMap::default();
347 map.add_edge(a, b, CompassOctant::North);
348 map.add_edge(b, a, CompassOctant::South);
349 map.add_edge(b, c, CompassOctant::East);
350 map.add_edge(c, b, CompassOctant::West);
351
352 let mut to_remove = EntityHashSet::default();
353 to_remove.insert(b);
354 to_remove.insert(c);
355
356 map.remove_multiple(to_remove);
357
358 assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
359 assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
360 assert_eq!(map.get_neighbor(b, CompassOctant::East), None);
361 assert_eq!(map.get_neighbor(c, CompassOctant::West), None);
362 }
363
364 #[test]
365 fn edges() {
366 let mut world = World::new();
367 let a = world.spawn_empty().id();
368 let b = world.spawn_empty().id();
369 let c = world.spawn_empty().id();
370
371 let mut map = DirectionalNavigationMap::default();
372 map.add_edges(&[a, b, c], CompassOctant::East);
373
374 assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
375 assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
376 assert_eq!(map.get_neighbor(c, CompassOctant::East), None);
377
378 assert_eq!(map.get_neighbor(a, CompassOctant::West), None);
379 assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
380 assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
381 }
382
383 #[test]
384 fn looping_edges() {
385 let mut world = World::new();
386 let a = world.spawn_empty().id();
387 let b = world.spawn_empty().id();
388 let c = world.spawn_empty().id();
389
390 let mut map = DirectionalNavigationMap::default();
391 map.add_looping_edges(&[a, b, c], CompassOctant::East);
392
393 assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
394 assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
395 assert_eq!(map.get_neighbor(c, CompassOctant::East), Some(a));
396
397 assert_eq!(map.get_neighbor(a, CompassOctant::West), Some(c));
398 assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
399 assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
400 }
401
402 #[test]
403 fn nav_with_system_param() {
404 let mut world = World::new();
405 let a = world.spawn_empty().id();
406 let b = world.spawn_empty().id();
407 let c = world.spawn_empty().id();
408
409 let mut map = DirectionalNavigationMap::default();
410 map.add_looping_edges(&[a, b, c], CompassOctant::East);
411
412 world.insert_resource(map);
413
414 let mut focus = InputFocus::default();
415 focus.set(a);
416 world.insert_resource(focus);
417
418 assert_eq!(world.resource::<InputFocus>().get(), Some(a));
419
420 fn navigate_east(mut nav: DirectionalNavigation) {
421 nav.navigate(CompassOctant::East).unwrap();
422 }
423
424 world.run_system_once(navigate_east).unwrap();
425 assert_eq!(world.resource::<InputFocus>().get(), Some(b));
426
427 world.run_system_once(navigate_east).unwrap();
428 assert_eq!(world.resource::<InputFocus>().get(), Some(c));
429
430 world.run_system_once(navigate_east).unwrap();
431 assert_eq!(world.resource::<InputFocus>().get(), Some(a));
432 }
433}