use crate::{navigator::find_best_candidate, FocusCause, 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, Copy)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Default, Debug, PartialEq, Clone)
)]
pub enum NavNeighbor {
#[default]
Auto,
Blocked,
Set(Entity),
}
impl NavNeighbor {
pub fn get(&self) -> Option<Entity> {
if let NavNeighbor::Set(n) = self {
Some(*n)
} else {
None
}
}
}
#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Default, Debug, PartialEq, Clone)
)]
pub struct NavNeighbors {
pub neighbors: [NavNeighbor; 8],
}
impl NavNeighbors {
pub const EMPTY: NavNeighbors = NavNeighbors {
neighbors: [NavNeighbor::Auto; 8],
};
pub const fn get(&self, octant: CompassOctant) -> NavNeighbor {
self.neighbors[octant.to_index()]
}
pub const fn set(&mut self, octant: CompassOctant, entity: Entity) {
self.neighbors[octant.to_index()] = NavNeighbor::Set(entity);
}
pub const fn block(&mut self, octant: CompassOctant) {
self.neighbors[octant.to_index()] = NavNeighbor::Blocked;
}
}
#[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 == NavNeighbor::Set(entity) {
*neighbor = NavNeighbor::Auto;
}
}
}
}
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() {
let NavNeighbor::Set(entity) = neighbor else {
continue;
};
if entities.contains(entity) {
*neighbor = NavNeighbor::Auto;
}
}
}
}
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 block_edge(&mut self, a: Entity, direction: CompassOctant) {
self.neighbors
.entry(a)
.or_insert(NavNeighbors::EMPTY)
.block(direction);
}
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 block_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
self.block_edge(a, direction);
self.block_edge(b, direction.opposite());
}
pub fn add_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
for &[a, b] in entities.array_windows() {
self.add_symmetrical_edge(a, b, 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()
&& let Some(last_entity) = rest.last()
{
self.add_symmetrical_edge(*last_entity, *first_entity, direction);
}
}
pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> NavNeighbor {
self.neighbors
.get(&focus)
.map(|neighbors| neighbors.get(octant))
.unwrap_or(NavNeighbor::Auto)
}
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.get() {
match self.map.get_neighbor(current_focus, direction) {
NavNeighbor::Auto => Err(DirectionalNavigationError::NoNeighborInDirection {
current_focus,
direction,
}),
NavNeighbor::Blocked => Err(DirectionalNavigationError::BlockedNavigation {
current_focus,
direction,
}),
NavNeighbor::Set(new_focus) => {
self.focus.set(new_focus, FocusCause::Navigated);
Ok(new_focus)
}
}
} 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,
},
#[error("Navigation explicitly blocked from {current_focus} in the {direction:?} direction.")]
BlockedNavigation {
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)
.filter(|neighbors| {
matches!(
neighbors.get(octant),
NavNeighbor::Blocked | NavNeighbor::Set(_)
)
})
.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), NavNeighbor::Auto);
neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER);
for i in 0..8 {
if i == CompassOctant::SouthEast.to_index() {
assert_eq!(
neighbors.get(CompassOctant::SouthEast),
NavNeighbor::Set(Entity::PLACEHOLDER)
);
} else {
assert_eq!(
neighbors.get(CompassOctant::from_index(i).unwrap()),
NavNeighbor::Auto
);
}
}
}
#[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),
NavNeighbor::Set(b)
);
assert_eq!(
map.get_neighbor(b, CompassOctant::SouthEast.opposite()),
NavNeighbor::Auto
);
}
#[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),
NavNeighbor::Set(b)
);
assert_eq!(
map.get_neighbor(b, CompassOctant::South),
NavNeighbor::Set(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),
NavNeighbor::Set(b)
);
assert_eq!(
map.get_neighbor(b, CompassOctant::South),
NavNeighbor::Set(a)
);
map.remove(b);
assert_eq!(map.get_neighbor(a, CompassOctant::North), NavNeighbor::Auto);
assert_eq!(map.get_neighbor(b, CompassOctant::South), NavNeighbor::Auto);
}
#[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), NavNeighbor::Auto);
assert_eq!(map.get_neighbor(b, CompassOctant::South), NavNeighbor::Auto);
assert_eq!(map.get_neighbor(b, CompassOctant::East), NavNeighbor::Auto);
assert_eq!(map.get_neighbor(c, CompassOctant::West), NavNeighbor::Auto);
}
#[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),
NavNeighbor::Set(b)
);
assert_eq!(
map.get_neighbor(b, CompassOctant::East),
NavNeighbor::Set(c)
);
assert_eq!(map.get_neighbor(c, CompassOctant::East), NavNeighbor::Auto);
assert_eq!(map.get_neighbor(a, CompassOctant::West), NavNeighbor::Auto);
assert_eq!(
map.get_neighbor(b, CompassOctant::West),
NavNeighbor::Set(a)
);
assert_eq!(
map.get_neighbor(c, CompassOctant::West),
NavNeighbor::Set(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),
NavNeighbor::Set(b)
);
assert_eq!(
map.get_neighbor(b, CompassOctant::East),
NavNeighbor::Set(c)
);
assert_eq!(
map.get_neighbor(c, CompassOctant::East),
NavNeighbor::Set(a)
);
assert_eq!(
map.get_neighbor(a, CompassOctant::West),
NavNeighbor::Set(c)
);
assert_eq!(
map.get_neighbor(b, CompassOctant::West),
NavNeighbor::Set(a)
);
assert_eq!(
map.get_neighbor(c, CompassOctant::West),
NavNeighbor::Set(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, FocusCause::Navigated);
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),
NavNeighbor::Set(node_b)
);
assert_eq!(
nav_map.get_neighbor(node_b, CompassOctant::West),
NavNeighbor::Set(node_a)
);
assert_eq!(
nav_map.get_neighbor(node_a, CompassOctant::South),
NavNeighbor::Set(node_c)
);
assert_eq!(
nav_map.get_neighbor(node_c, CompassOctant::North),
NavNeighbor::Set(node_a)
);
assert_eq!(
nav_map.get_neighbor(node_a, CompassOctant::SouthEast),
NavNeighbor::Set(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),
NavNeighbor::Set(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),
NavNeighbor::Set(wide_top),
"Should navigate to wide_top not bottom, even though bottom's center is closer."
);
}
#[test]
fn test_respects_set_blocks() {
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.block_edge(node_a, 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(0.0, 50.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),
NavNeighbor::Blocked
);
assert_eq!(
nav_map.get_neighbor(node_a, CompassOctant::South),
NavNeighbor::Set(node_c)
);
}
}