use crate::{navigator::find_best_candidate, InputFocus};
use bevy_app::prelude::*;
use bevy_ecs::{
entity::{EntityHashMap, EntityHashSet},
prelude::*,
system::SystemParam,
};
use bevy_math::{CompassOctant, Vec2};
use thiserror::Error;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{prelude::*, Reflect};
#[derive(Default)]
pub struct DirectionalNavigationPlugin;
impl Plugin for DirectionalNavigationPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<DirectionalNavigationMap>()
.init_resource::<AutoNavigationConfig>();
}
}
#[derive(Resource, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Resource, Debug, PartialEq, Clone)
)]
pub struct AutoNavigationConfig {
pub min_alignment_factor: f32,
pub max_search_distance: Option<f32>,
pub prefer_aligned: bool,
}
impl Default for AutoNavigationConfig {
fn default() -> Self {
Self {
min_alignment_factor: 0.0, max_search_distance: None, prefer_aligned: true, }
}
}
#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Default, Debug, PartialEq, Clone)
)]
pub struct NavNeighbors {
pub neighbors: [Option<Entity>; 8],
}
impl NavNeighbors {
pub const EMPTY: NavNeighbors = NavNeighbors {
neighbors: [None; 8],
};
pub const fn get(&self, octant: CompassOctant) -> Option<Entity> {
self.neighbors[octant.to_index()]
}
pub const fn set(&mut self, octant: CompassOctant, entity: Entity) {
self.neighbors[octant.to_index()] = Some(entity);
}
}
#[derive(Resource, Debug, Default, Clone, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Resource, Debug, Default, PartialEq, Clone)
)]
pub struct DirectionalNavigationMap {
pub neighbors: EntityHashMap<NavNeighbors>,
}
impl DirectionalNavigationMap {
pub fn remove(&mut self, entity: Entity) {
self.neighbors.remove(&entity);
for node in self.neighbors.values_mut() {
for neighbor in node.neighbors.iter_mut() {
if *neighbor == Some(entity) {
*neighbor = None;
}
}
}
}
pub fn remove_multiple(&mut self, entities: EntityHashSet) {
for entity in &entities {
self.neighbors.remove(entity);
}
for node in self.neighbors.values_mut() {
for neighbor in node.neighbors.iter_mut() {
if let Some(entity) = *neighbor {
if entities.contains(&entity) {
*neighbor = None;
}
}
}
}
}
pub fn clear(&mut self) {
self.neighbors.clear();
}
pub fn add_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
self.neighbors
.entry(a)
.or_insert(NavNeighbors::EMPTY)
.set(direction, b);
}
pub fn add_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
self.add_edge(a, b, direction);
self.add_edge(b, a, direction.opposite());
}
pub fn add_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
for pair in entities.windows(2) {
self.add_symmetrical_edge(pair[0], pair[1], direction);
}
}
pub fn add_looping_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
self.add_edges(entities, direction);
if let Some((first_entity, rest)) = entities.split_first() {
if let Some(last_entity) = rest.last() {
self.add_symmetrical_edge(*last_entity, *first_entity, direction);
}
}
}
pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> Option<Entity> {
self.neighbors
.get(&focus)
.and_then(|neighbors| neighbors.get(octant))
}
pub fn get_neighbors(&self, entity: Entity) -> Option<&NavNeighbors> {
self.neighbors.get(&entity)
}
}
#[derive(SystemParam, Debug)]
pub struct DirectionalNavigation<'w> {
pub focus: ResMut<'w, InputFocus>,
pub map: Res<'w, DirectionalNavigationMap>,
}
impl<'w> DirectionalNavigation<'w> {
pub fn navigate(
&mut self,
direction: CompassOctant,
) -> Result<Entity, DirectionalNavigationError> {
if let Some(current_focus) = self.focus.0 {
if let Some(new_focus) = self.map.get_neighbor(current_focus, direction) {
self.focus.set(new_focus);
Ok(new_focus)
} else {
Err(DirectionalNavigationError::NoNeighborInDirection {
current_focus,
direction,
})
}
} else {
Err(DirectionalNavigationError::NoFocus)
}
}
}
#[derive(Debug, PartialEq, Clone, Error)]
pub enum DirectionalNavigationError {
#[error("No focusable entity is currently set.")]
NoFocus,
#[error("No neighbor from {current_focus} in the {direction:?} direction.")]
NoNeighborInDirection {
current_focus: Entity,
direction: CompassOctant,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Clone)
)]
pub struct FocusableArea {
pub entity: Entity,
pub position: Vec2,
pub size: Vec2,
}
pub trait Navigable {
fn get_bounds(&self) -> (Vec2, Vec2);
}
pub fn auto_generate_navigation_edges(
nav_map: &mut DirectionalNavigationMap,
nodes: &[FocusableArea],
config: &AutoNavigationConfig,
) {
for origin in nodes {
for octant in [
CompassOctant::North,
CompassOctant::NorthEast,
CompassOctant::East,
CompassOctant::SouthEast,
CompassOctant::South,
CompassOctant::SouthWest,
CompassOctant::West,
CompassOctant::NorthWest,
] {
if nav_map
.get_neighbors(origin.entity)
.and_then(|neighbors| neighbors.get(octant))
.is_some()
{
continue; }
let best_candidate = find_best_candidate(origin, octant, nodes, config);
if let Some(neighbor) = best_candidate {
nav_map.add_edge(origin.entity, neighbor, octant);
}
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use bevy_ecs::system::RunSystemOnce;
use super::*;
#[test]
fn setting_and_getting_nav_neighbors() {
let mut neighbors = NavNeighbors::EMPTY;
assert_eq!(neighbors.get(CompassOctant::SouthEast), None);
neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER);
for i in 0..8 {
if i == CompassOctant::SouthEast.to_index() {
assert_eq!(
neighbors.get(CompassOctant::SouthEast),
Some(Entity::PLACEHOLDER)
);
} else {
assert_eq!(neighbors.get(CompassOctant::from_index(i).unwrap()), None);
}
}
}
#[test]
fn simple_set_and_get_navmap() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_edge(a, b, CompassOctant::SouthEast);
assert_eq!(map.get_neighbor(a, CompassOctant::SouthEast), Some(b));
assert_eq!(
map.get_neighbor(b, CompassOctant::SouthEast.opposite()),
None
);
}
#[test]
fn symmetrical_edges() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_symmetrical_edge(a, b, CompassOctant::North);
assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
}
#[test]
fn remove_nodes() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_edge(a, b, CompassOctant::North);
map.add_edge(b, a, CompassOctant::South);
assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
map.remove(b);
assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
}
#[test]
fn remove_multiple_nodes() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let c = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_edge(a, b, CompassOctant::North);
map.add_edge(b, a, CompassOctant::South);
map.add_edge(b, c, CompassOctant::East);
map.add_edge(c, b, CompassOctant::West);
let mut to_remove = EntityHashSet::default();
to_remove.insert(b);
to_remove.insert(c);
map.remove_multiple(to_remove);
assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
assert_eq!(map.get_neighbor(b, CompassOctant::East), None);
assert_eq!(map.get_neighbor(c, CompassOctant::West), None);
}
#[test]
fn edges() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let c = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_edges(&[a, b, c], CompassOctant::East);
assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
assert_eq!(map.get_neighbor(c, CompassOctant::East), None);
assert_eq!(map.get_neighbor(a, CompassOctant::West), None);
assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
}
#[test]
fn looping_edges() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let c = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_looping_edges(&[a, b, c], CompassOctant::East);
assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
assert_eq!(map.get_neighbor(c, CompassOctant::East), Some(a));
assert_eq!(map.get_neighbor(a, CompassOctant::West), Some(c));
assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
}
#[test]
fn manual_nav_with_system_param() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let c = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_looping_edges(&[a, b, c], CompassOctant::East);
world.insert_resource(map);
let mut focus = InputFocus::default();
focus.set(a);
world.insert_resource(focus);
let config = AutoNavigationConfig::default();
world.insert_resource(config);
assert_eq!(world.resource::<InputFocus>().get(), Some(a));
fn navigate_east(mut nav: DirectionalNavigation) {
nav.navigate(CompassOctant::East).unwrap();
}
world.run_system_once(navigate_east).unwrap();
assert_eq!(world.resource::<InputFocus>().get(), Some(b));
world.run_system_once(navigate_east).unwrap();
assert_eq!(world.resource::<InputFocus>().get(), Some(c));
world.run_system_once(navigate_east).unwrap();
assert_eq!(world.resource::<InputFocus>().get(), Some(a));
}
#[test]
fn test_auto_generate_navigation_edges() {
let mut nav_map = DirectionalNavigationMap::default();
let config = AutoNavigationConfig::default();
let node_a = Entity::from_bits(1); let node_b = Entity::from_bits(2); let node_c = Entity::from_bits(3); let node_d = Entity::from_bits(4);
let nodes = vec![
FocusableArea {
entity: node_a,
position: Vec2::new(0.0, 0.0),
size: Vec2::new(50.0, 50.0),
}, FocusableArea {
entity: node_b,
position: Vec2::new(100.0, 0.0),
size: Vec2::new(50.0, 50.0),
}, FocusableArea {
entity: node_c,
position: Vec2::new(0.0, 100.0),
size: Vec2::new(50.0, 50.0),
}, FocusableArea {
entity: node_d,
position: Vec2::new(100.0, 100.0),
size: Vec2::new(50.0, 50.0),
}, ];
auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
assert_eq!(
nav_map.get_neighbor(node_a, CompassOctant::East),
Some(node_b)
);
assert_eq!(
nav_map.get_neighbor(node_b, CompassOctant::West),
Some(node_a)
);
assert_eq!(
nav_map.get_neighbor(node_a, CompassOctant::South),
Some(node_c)
);
assert_eq!(
nav_map.get_neighbor(node_c, CompassOctant::North),
Some(node_a)
);
assert_eq!(
nav_map.get_neighbor(node_a, CompassOctant::SouthEast),
Some(node_d)
);
}
#[test]
fn test_auto_generate_respects_manual_edges() {
let mut nav_map = DirectionalNavigationMap::default();
let config = AutoNavigationConfig::default();
let node_a = Entity::from_bits(1);
let node_b = Entity::from_bits(2);
let node_c = Entity::from_bits(3);
nav_map.add_edge(node_a, node_c, CompassOctant::East);
let nodes = vec![
FocusableArea {
entity: node_a,
position: Vec2::new(0.0, 0.0),
size: Vec2::new(50.0, 50.0),
},
FocusableArea {
entity: node_b,
position: Vec2::new(50.0, 0.0),
size: Vec2::new(50.0, 50.0),
}, FocusableArea {
entity: node_c,
position: Vec2::new(100.0, 0.0),
size: Vec2::new(50.0, 50.0),
},
];
auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
assert_eq!(
nav_map.get_neighbor(node_a, CompassOctant::East),
Some(node_c)
);
}
#[test]
fn test_edge_distance_vs_center_distance() {
let mut nav_map = DirectionalNavigationMap::default();
let config = AutoNavigationConfig::default();
let left = Entity::from_bits(1);
let wide_top = Entity::from_bits(2);
let bottom = Entity::from_bits(3);
let left_node = FocusableArea {
entity: left,
position: Vec2::new(100.0, 200.0),
size: Vec2::new(100.0, 100.0),
};
let wide_top_node = FocusableArea {
entity: wide_top,
position: Vec2::new(350.0, 150.0),
size: Vec2::new(300.0, 80.0),
};
let bottom_node = FocusableArea {
entity: bottom,
position: Vec2::new(270.0, 300.0),
size: Vec2::new(100.0, 80.0),
};
let nodes = vec![left_node, wide_top_node, bottom_node];
auto_generate_navigation_edges(&mut nav_map, &nodes, &config);
assert_eq!(
nav_map.get_neighbor(left, CompassOctant::East),
Some(wide_top),
"Should navigate to wide_top not bottom, even though bottom's center is closer."
);
}
}