use std::collections::VecDeque;
#[cfg(feature = "stats")]
use std::time::Instant;
use bevy::{log, platform::collections::HashMap, prelude::*};
use crate::{pathfind::PathfindArgs, prelude::*, WithoutPathingFailures};
#[derive(Default, Debug, Copy, Clone)]
pub struct PathfindSettings {
pub default_mode: PathfindMode,
}
#[derive(Resource, Debug, Copy, Clone)]
pub struct NorthstarPluginSettings {
pub max_pathfinding_agents_per_frame: usize,
pub max_collision_avoidance_agents_per_frame: usize,
pub pathfind_settings: PathfindSettings,
}
impl Default for NorthstarPluginSettings {
fn default() -> Self {
Self {
max_pathfinding_agents_per_frame: 128,
max_collision_avoidance_agents_per_frame: 128,
pathfind_settings: PathfindSettings::default(),
}
}
}
#[derive(Default)]
pub struct NorthstarPlugin<N: Neighborhood> {
_neighborhood: std::marker::PhantomData<N>,
}
#[derive(Default, Debug)]
pub struct PathfindingStats {
pub average_time: f64,
pub average_length: f64,
pub pathfind_time: Vec<f64>,
pub pathfind_length: Vec<f64>,
}
#[derive(Default, Debug)]
pub struct CollisionStats {
pub average_time: f64,
pub average_length: f64,
pub avoidance_time: Vec<f64>,
pub avoidance_length: Vec<f64>,
}
#[derive(Resource, Default, Debug)]
pub struct Stats {
pub pathfinding: PathfindingStats,
pub collision: CollisionStats,
}
impl Stats {
pub fn add_pathfinding(&mut self, time: f64, length: f64) {
self.pathfinding.pathfind_time.push(time);
self.pathfinding.pathfind_length.push(length);
self.pathfinding.average_time = self.pathfinding.pathfind_time.iter().sum::<f64>()
/ self.pathfinding.pathfind_time.len() as f64;
self.pathfinding.average_length = self.pathfinding.pathfind_length.iter().sum::<f64>()
/ self.pathfinding.pathfind_length.len() as f64;
}
pub fn reset_pathfinding(&mut self) {
self.pathfinding.average_time = 0.0;
self.pathfinding.average_length = 0.0;
self.pathfinding.pathfind_time.clear();
self.pathfinding.pathfind_length.clear();
}
pub fn add_collision(&mut self, time: f64, length: f64) {
self.collision.avoidance_time.push(time);
self.collision.avoidance_length.push(length);
self.collision.average_time = self.collision.avoidance_time.iter().sum::<f64>()
/ self.collision.avoidance_time.len() as f64;
self.collision.average_length = self.collision.avoidance_length.iter().sum::<f64>()
/ self.collision.avoidance_length.len() as f64;
}
pub fn reset_collision(&mut self) {
self.collision.average_time = 0.0;
self.collision.average_length = 0.0;
self.collision.avoidance_time.clear();
self.collision.avoidance_length.clear();
}
}
impl<N: 'static + Neighborhood> Plugin for NorthstarPlugin<N> {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
tag_pathfinding_requests,
update_blocking_map,
pathfind::<N>,
next_position::<N>,
reroute_path::<N>,
)
.chain()
.in_set(PathingSet),
)
.insert_resource(NorthstarPluginSettings::default())
.insert_resource(BlockingMap::default())
.insert_resource(Stats::default())
.insert_resource(DirectionMap::default())
.register_type::<Path>()
.register_type::<Pathfind>()
.register_type::<PathfindMode>()
.register_type::<NextPos>()
.register_type::<AgentOfGrid>()
.register_type::<GridAgents>();
}
}
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct PathingSet;
#[derive(Resource, Default)]
pub struct BlockingMap(pub HashMap<UVec3, Entity>);
#[derive(Resource, Default)]
pub struct DirectionMap(pub HashMap<Entity, Vec3>);
#[derive(Component)]
#[component(storage = "SparseSet")]
pub(crate) struct NeedsPathfinding;
fn tag_pathfinding_requests(mut commands: Commands, query: Query<Entity, Changed<Pathfind>>) {
for entity in query.iter() {
commands.entity(entity).try_insert(NeedsPathfinding);
}
}
fn pathfind<N: Neighborhood + 'static>(
grid: Single<&Grid<N>>,
mut commands: Commands,
mut query: Query<
(Entity, &AgentPos, &Pathfind, Option<&mut AgentMask>),
With<NeedsPathfinding>,
>,
blocking: Res<BlockingMap>,
settings: Res<NorthstarPluginSettings>,
#[cfg(feature = "stats")] mut stats: ResMut<Stats>,
) {
let grid = grid.into_inner();
let mut count = 0;
for (entity, start, pathfind, mut agent_mask) in &mut query {
if count >= settings.max_pathfinding_agents_per_frame {
return;
}
if start.0 == pathfind.goal {
commands.entity(entity).remove::<Pathfind>();
commands.entity(entity).remove::<NeedsPathfinding>();
continue;
}
#[cfg(feature = "stats")]
let start_time = Instant::now();
let algorithm = pathfind
.mode
.unwrap_or(settings.pathfind_settings.default_mode);
let mask = if let Some(agent_mask) = agent_mask.as_mut() {
Some(&mut agent_mask.0)
} else {
None
};
let path = grid.pathfind(&mut PathfindArgs {
start: start.0,
goal: pathfind.goal,
blocking: Some(&blocking.0),
mask,
mode: algorithm,
limits: pathfind.limits,
});
#[cfg(feature = "stats")]
let elapsed_time = start_time.elapsed().as_secs_f64();
if let Some(path) = path {
#[cfg(feature = "stats")]
stats.add_pathfinding(elapsed_time, path.cost() as f64);
commands
.entity(entity)
.try_insert(path)
.try_remove::<PathfindingFailed>()
.try_remove::<NeedsPathfinding>();
} else {
#[cfg(feature = "stats")]
stats.add_pathfinding(elapsed_time, 0.0);
commands
.entity(entity)
.try_insert(PathfindingFailed)
.try_remove::<NeedsPathfinding>()
.try_remove::<NextPos>(); }
count += 1;
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn next_position<N: Neighborhood + 'static>(
mut query: Query<
(
Entity,
&mut Path,
&AgentPos,
&Pathfind,
Option<&mut AgentMask>,
),
(WithoutPathingFailures, Without<NextPos>),
>,
grid: Single<&Grid<N>>,
mut blocking_map: ResMut<BlockingMap>,
mut direction: ResMut<DirectionMap>,
mut commands: Commands,
settings: Res<NorthstarPluginSettings>,
mut queue: Local<VecDeque<Entity>>,
#[cfg(feature = "stats")] mut stats: ResMut<Stats>,
) {
let grid = grid.into_inner();
if queue.is_empty() {
for (entity, ..) in query.iter() {
queue.push_back(entity);
}
}
let mut processed = 0;
for _ in 0..queue.len() {
if processed >= settings.max_collision_avoidance_agents_per_frame {
break;
}
let entity = queue.pop_front().unwrap();
if let Ok((entity, mut path, position, pathfind, agent_mask)) = query.get_mut(entity) {
if position.0 == pathfind.goal {
commands.entity(entity).try_remove::<Path>();
commands.entity(entity).try_remove::<Pathfind>();
continue;
}
let next = if grid.collision() {
#[cfg(feature = "stats")]
let start = Instant::now();
let default_mask = NavMask::default();
let mask = agent_mask.as_ref().map(|m| &m.0).unwrap_or(&default_mask);
let success = avoidance(
grid,
entity,
&mut path,
position.0,
&blocking_map.0,
mask,
&direction.0,
grid.avoidance_distance() as usize,
);
if !success {
commands.entity(entity).try_insert(AvoidanceFailed);
continue;
}
#[cfg(feature = "stats")]
let elapsed = start.elapsed().as_secs_f64();
#[cfg(feature = "stats")]
stats.add_collision(elapsed, path.cost() as f64);
processed += 1;
path.pop()
} else {
path.pop()
};
if let Some(next) = next {
direction
.0
.insert(entity, next.as_vec3() - position.0.as_vec3());
if blocking_map.0.contains_key(&next) && grid.collision() {
queue.push_back(entity);
continue;
}
if grid.collision() {
blocking_map.0.remove(&position.0);
blocking_map.0.insert(next, entity);
}
commands.entity(entity).try_insert(NextPos(next));
queue.push_back(entity);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn avoidance<N: Neighborhood + 'static>(
grid: &Grid<N>,
entity: Entity,
path: &mut Path,
position: UVec3,
blocking_map: &HashMap<UVec3, Entity>,
mask: &NavMask,
direction: &HashMap<Entity, Vec3>,
avoidance_distance: usize,
) -> bool {
if path.is_empty() {
log::error!("Path is empty for entity: {:?}", entity);
return false;
}
let count = if path.path().len() > avoidance_distance {
avoidance_distance
} else {
path.path().len()
};
let next_position = path.path.front().unwrap();
if path.path.len() == 1 && blocking_map.contains_key(next_position) {
return false;
}
let difference = next_position.as_vec3() - position.as_vec3();
let unblocked_pos: Vec<UVec3> = path
.path
.iter()
.take(count)
.filter(|pos| {
if let Some(blocking_entity) = blocking_map.get(*pos) {
if *blocking_entity == entity {
return true;
}
if *pos == next_position {
return false;
}
if let Some(blocking_dir) = direction.get(blocking_entity) {
let dot = difference.dot(*blocking_dir);
if dot <= 0.0 {
return false;
}
} else {
return false;
}
}
true
})
.cloned()
.collect();
if unblocked_pos.len() < count {
let avoidance_goal = {
let _ = info_span!("avoidance_goal", name = "avoidance_goal").entered();
path.path
.iter()
.skip(count)
.find(|pos| !blocking_map.contains_key(&**pos))
};
if let Some(avoidance_goal) = avoidance_goal {
let radius = position.as_vec3().distance(avoidance_goal.as_vec3()) as u32
+ grid.avoidance_distance();
let new_path = grid.pathfind_astar_radius(
position,
*avoidance_goal,
radius,
blocking_map,
Some(mask),
);
if let Some(new_path) = new_path {
let old_path = path
.path
.iter()
.skip_while(|pos| *pos != avoidance_goal)
.cloned()
.collect::<Vec<UVec3>>();
let mut combined_path = new_path.path().to_vec();
combined_path.extend(old_path);
if combined_path.is_empty() {
log::error!("Combined path is empty for entity: {:?}", entity);
return false;
}
let graph_path = path.graph_path.clone();
*path = Path::from_slice(&combined_path, new_path.cost());
path.graph_path = graph_path;
} else {
return false;
}
} else {
return false;
}
}
true
}
#[allow(clippy::type_complexity)]
fn reroute_path<N: Neighborhood + 'static>(
mut query: Query<
(Entity, &AgentPos, &Pathfind, &Path, Option<&mut AgentMask>),
With<AvoidanceFailed>,
>,
grid: Single<&Grid<N>>,
blocking_map: Res<BlockingMap>,
mut commands: Commands,
settings: Res<NorthstarPluginSettings>,
#[cfg(feature = "stats")] mut stats: ResMut<Stats>,
) {
let grid = grid.into_inner();
for (count, (entity, position, pathfind, path, mut agent_mask)) in query.iter_mut().enumerate()
{
if count >= settings.max_pathfinding_agents_per_frame {
return;
}
#[cfg(feature = "stats")]
let start = Instant::now();
let mut pathfind = pathfind.clone();
pathfind.mode = Some(
pathfind
.mode
.unwrap_or(settings.pathfind_settings.default_mode),
);
let new_path = grid.reroute_path(
path,
position.0,
&pathfind,
&blocking_map.0,
agent_mask.as_mut().map(|m| &mut m.0),
);
if let Some(new_path) = new_path {
if new_path.path().last().unwrap() != &pathfind.goal {
log::error!("WE HAVE A PARTIAL ROUTE ISSUE: {:?}", entity);
}
#[cfg(feature = "stats")]
let elapsed = start.elapsed().as_secs_f64();
#[cfg(feature = "stats")]
stats.add_collision(elapsed, new_path.cost() as f64);
commands.entity(entity).try_insert(new_path);
commands.entity(entity).try_remove::<AvoidanceFailed>();
} else {
commands.entity(entity).try_insert(RerouteFailed);
commands.entity(entity).try_remove::<AvoidanceFailed>();
#[cfg(feature = "stats")]
let elapsed = start.elapsed().as_secs_f64();
#[cfg(feature = "stats")]
stats.add_collision(elapsed, 0.0);
}
}
}
fn update_blocking_map(
mut blocking_map: ResMut<BlockingMap>,
query: Query<(Entity, &AgentPos), With<Blocking>>,
) {
blocking_map.0.clear();
for (entity, position) in query.iter() {
blocking_map.0.insert(position.0, entity);
}
}