diff --git a/src/entities/shared_components/ambulatory.rs b/src/entities/shared_components/ambulatory.rs index c02a637..5b41470 100644 --- a/src/entities/shared_components/ambulatory.rs +++ b/src/entities/shared_components/ambulatory.rs @@ -1,5 +1,4 @@ use bevy::prelude::*; -use bevy::tasks::Task; #[derive(Component)] pub struct Ambulatory { @@ -19,15 +18,6 @@ pub struct PendingPath { pub request_id: u64, } -#[derive(Component)] -pub struct PendingAsyncPath { - pub request_id: u64, - pub task: Task>, - pub goal: IVec3, - pub provisional_path: Vec, - pub provisional_path_index: usize, -} - #[derive(Resource, Default)] pub struct PathRequestCounter { pub next_id: u64, diff --git a/src/entities/shared_systems/async_pathfinding.rs b/src/entities/shared_systems/async_pathfinding.rs deleted file mode 100644 index 67b0521..0000000 --- a/src/entities/shared_systems/async_pathfinding.rs +++ /dev/null @@ -1,296 +0,0 @@ -use bevy::prelude::*; -use bevy::tasks::AsyncComputeTaskPool; -use rustc_hash::{FxHashMap, FxHashSet}; -use std::collections::BinaryHeap; - -use crate::constants::{ITILE_SIZE, PATHFINDER_MAX_NODES}; -use crate::entities::shared_components::{CompletedPaths, PendingAsyncPath}; -use crate::world::tiles::tilemap::StandableBitGrid; -use crate::world::tiles::TileMap; - -pub struct AsyncPathfindingPlugin; - -impl Plugin for AsyncPathfindingPlugin { - fn build(&self, app: &mut App) { - app.insert_resource(AsyncPathCounter::default()) - .add_systems(PostUpdate, poll_async_paths); - } -} - -#[derive(Resource, Default)] -pub struct AsyncPathCounter { - pub next_id: u64, -} - -impl AsyncPathCounter { - pub fn next(&mut self) -> u64 { - let id = self.next_id; - self.next_id += 1; - id - } -} - -#[derive(Clone, Copy, Eq, PartialEq, Debug)] -struct AsyncPathNode { - position: IVec3, - f_score: i32, - g_score: i32, -} - -impl Ord for AsyncPathNode { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - other - .f_score - .cmp(&self.f_score) - .then_with(|| other.g_score.cmp(&self.g_score)) - } -} - -impl PartialOrd for AsyncPathNode { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -const ASYNC_ALLOWED_MOVES: [(i32, i32, i32); 24] = [ - (-1, 0, 0), - (1, 0, 0), - (0, -1, 0), - (0, 1, 0), - (-1, -1, 0), - (-1, 1, 0), - (1, -1, 0), - (1, 1, 0), - (-1, 0, 1), - (-1, 0, -1), - (1, 0, 1), - (1, 0, -1), - (0, -1, 1), - (0, -1, -1), - (0, 1, 1), - (0, 1, -1), - (-1, -1, 1), - (-1, -1, -1), - (-1, 1, 1), - (-1, 1, -1), - (1, -1, 1), - (1, -1, -1), - (1, 1, 1), - (1, 1, -1), -]; - -pub fn calculate_async_path(bit_grid: StandableBitGrid, start: IVec3, goal: IVec3) -> Vec { - let (sbx, sby, sbz) = match bit_grid.to_bit_coords(start) { - Some(c) => c, - None => return vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)], - }; - let (gbx, gby, gbz) = match bit_grid.to_bit_coords(goal) { - Some(c) => c, - None => return vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)], - }; - - let mut g_scores: FxHashMap<(u32, u32, u32), i32> = FxHashMap::default(); - let mut came_from: FxHashMap<(u32, u32, u32), (u32, u32, u32)> = FxHashMap::default(); - let mut closed_set: FxHashSet<(u32, u32, u32)> = FxHashSet::default(); - let mut open_set: BinaryHeap = BinaryHeap::new(); - - let h = octile_distance_3d_bit(sbx, sby, sbz, gbx, gby, gbz); - open_set.push(AsyncPathNode { - position: start, - f_score: h, - g_score: 0, - }); - g_scores.insert((sbx, sby, sbz), 0); - - let mut nodes_expanded: usize = 0; - - while let Some(current_node) = open_set.pop() { - let current = current_node.position; - nodes_expanded += 1; - - if nodes_expanded > PATHFINDER_MAX_NODES { - return reconstruct_path_async(&came_from, current, &bit_grid); - } - - if current == goal { - return reconstruct_path_async(&came_from, current, &bit_grid); - } - - let (cx, cy, cz) = match bit_grid.to_bit_coords(current) { - Some(c) => c, - None => continue, - }; - - closed_set.insert((cx, cy, cz)); - - for &(dx, dy, dz) in &ASYNC_ALLOWED_MOVES { - let nx = cx as i32 + dx; - let ny = cy as i32 + dy; - let nz = cz as i32 + dz; - - if nx < 0 || ny < 0 || nz < 0 { - continue; - } - let neighbor_bx = nx as u32; - let neighbor_by = ny as u32; - let neighbor_bz = nz as u32; - - if !bit_grid.is_standable_at(neighbor_bx, neighbor_by, neighbor_bz) - || closed_set.contains(&(neighbor_bx, neighbor_by, neighbor_bz)) - { - continue; - } - - let movement_cost = calculate_movement_cost_bit(dx, dy, dz); - if movement_cost == 0 { - continue; - } - - let neighbor_pos = IVec3::new( - current.x + dx * ITILE_SIZE, - current.y + dy * ITILE_SIZE, - current.z + dz * ITILE_SIZE, - ); - - let current_g = *g_scores.get(&(cx, cy, cz)).unwrap_or(&i32::MAX); - let new_g = current_g + movement_cost; - - let existing_g = *g_scores - .get(&(neighbor_bx, neighbor_by, neighbor_bz)) - .unwrap_or(&i32::MAX); - - if new_g < existing_g { - came_from.insert((neighbor_bx, neighbor_by, neighbor_bz), (cx, cy, cz)); - g_scores.insert((neighbor_bx, neighbor_by, neighbor_bz), new_g); - let h = - octile_distance_3d_bit(neighbor_bx, neighbor_by, neighbor_bz, gbx, gby, gbz); - open_set.push(AsyncPathNode { - position: neighbor_pos, - f_score: new_g + h, - g_score: new_g, - }); - } - } - } - - Vec::new() -} - -fn octile_distance_3d_bit(ax: u32, ay: u32, az: u32, bx: u32, by: u32, bz: u32) -> i32 { - let dx = (ax as i32 - bx as i32).abs(); - let dy = (ay as i32 - by as i32).abs(); - let dz = (az as i32 - bz as i32).abs(); - 10 * dx.max(dy).max(dz) + 4 * sort_middle(dx, dy, dz) + sort_min(dx, dy, dz) -} - -fn sort_middle(a: i32, b: i32, c: i32) -> i32 { - let mut arr = [a, b, c]; - arr.sort_unstable(); - arr[1] -} - -fn sort_min(a: i32, b: i32, c: i32) -> i32 { - let mut arr = [a, b, c]; - arr.sort_unstable(); - arr[0] -} - -fn calculate_movement_cost_bit(dx: i32, dy: i32, dz: i32) -> i32 { - match (dx.abs(), dy.abs(), dz.abs()) { - (1, 0, 0) | (0, 1, 0) => 10, - (1, 1, 0) => 14, - (1, 0, 1) | (0, 1, 1) => 42, - (1, 1, 1) => 56, - _ => 0, - } -} - -fn reconstruct_path_async( - came_from: &FxHashMap<(u32, u32, u32), (u32, u32, u32)>, - mut current: IVec3, - bit_grid: &StandableBitGrid, -) -> Vec { - let mut path = vec![Vec3::new( - current.x as f32, - current.y as f32, - current.z as f32, - )]; - - while let Some((cx, cy, cz)) = bit_grid.to_bit_coords(current) { - if let Some(&(px, py, pz)) = came_from.get(&(cx, cy, cz)) { - let prev = IVec3::new( - bit_grid.origin.x + (px as i32) * ITILE_SIZE, - bit_grid.origin.y + (py as i32) * ITILE_SIZE, - bit_grid.origin.z + (pz as i32) * ITILE_SIZE, - ); - path.push(Vec3::new(prev.x as f32, prev.y as f32, prev.z as f32)); - current = prev; - } else { - break; - } - } - - path.reverse(); - path -} - -pub fn poll_async_paths( - mut commands: Commands, - mut pending_query: Query<(Entity, &mut PendingAsyncPath)>, - mut completed: ResMut, -) { - for (entity, mut pending) in pending_query.iter_mut() { - if !pending.task.is_finished() { - continue; - } - let path: Option> = - futures_lite::future::block_on(futures_lite::future::poll_once(&mut pending.task)); - if let Some(p) = path { - if !p.is_empty() { - completed.paths.push((pending.request_id, p)); - } - commands.entity(entity).remove::(); - } - } -} - -pub fn compute_bounding_box(start: IVec3, goal: IVec3, margin_tiles: i32) -> (IVec3, UVec3) { - let margin = margin_tiles * ITILE_SIZE; - let min_x = start.x.min(goal.x) - margin; - let max_x = start.x.max(goal.x) + margin; - let min_y = start.y.min(goal.y) - margin; - let max_y = start.y.max(goal.y) + margin; - let min_z = start.z.min(goal.z) - margin; - let max_z = start.z.max(goal.z) + margin; - - let origin = IVec3::new(min_x, min_y, min_z); - let size = UVec3::new( - ((max_x - min_x) / ITILE_SIZE + 1) as u32, - ((max_y - min_y) / ITILE_SIZE + 1) as u32, - ((max_z - min_z) / ITILE_SIZE + 1) as u32, - ); - - (origin, size) -} - -pub fn spawn_async_path_task( - tilemap: &TileMap, - start: IVec3, - goal: IVec3, - provisional_path: Vec, - request_id: u64, -) -> PendingAsyncPath { - let (origin, size) = compute_bounding_box(start, goal, 20); - let bit_grid = StandableBitGrid::new(origin, size, tilemap); - - let pool = AsyncComputeTaskPool::get(); - let task = pool.spawn(async move { calculate_async_path(bit_grid, start, goal) }); - - PendingAsyncPath { - request_id, - task, - goal, - provisional_path, - provisional_path_index: 0, - } -} diff --git a/src/entities/shared_systems/mod.rs b/src/entities/shared_systems/mod.rs index e2529e9..1bc27c5 100644 --- a/src/entities/shared_systems/mod.rs +++ b/src/entities/shared_systems/mod.rs @@ -1,2 +1 @@ -pub mod async_pathfinding; pub mod pathfinding; diff --git a/src/entities/shared_systems/pathfinding.rs b/src/entities/shared_systems/pathfinding.rs index 6e7e19a..36437a0 100644 --- a/src/entities/shared_systems/pathfinding.rs +++ b/src/entities/shared_systems/pathfinding.rs @@ -1,15 +1,12 @@ use bevy::prelude::*; -use bevy::tasks::AsyncComputeTaskPool; use rayon::prelude::*; use rustc_hash::FxHashMap; use rustc_hash::FxHashSet; -use std::{cell::RefCell, collections::BinaryHeap, time::Instant}; +use std::{cell::RefCell, collections::BinaryHeap, collections::VecDeque, time::Instant}; use crate::constants::{ ITILE_SIZE, PATHFINDER_MAX_NODES, PATHFINDER_PROVISIONAL_NODE_LIMIT, TILE_SIZE, }; -use crate::entities::shared_components::PendingAsyncPath; -use crate::world::tiles::tilemap::StandableBitGrid; use crate::world::tiles::TileMap; use crate::world::{chunks::ChunkMap, chunks::CHUNK_SIZE}; use crate::{constants::*, entities::shared_components::Ambulatory}; @@ -134,6 +131,13 @@ impl PathfindingBenchmark { } } +#[derive(Resource, Default)] +pub struct PathRequestQueue { + pub pending: VecDeque<(Entity, IVec3, IVec3)>, +} + +const MAX_PATHS_PER_FRAME: usize = 8; + pub struct PathfindingPlugin; impl Plugin for PathfindingPlugin { @@ -141,9 +145,7 @@ impl Plugin for PathfindingPlugin { app.insert_resource(PathfindingBenchmark::new(100)) .insert_resource(crate::entities::shared_components::CompletedPaths::default()) .insert_resource(crate::entities::shared_components::PathRequestCounter::default()) - .insert_resource( - crate::entities::shared_systems::async_pathfinding::AsyncPathCounter::default(), - ) + .insert_resource(PathRequestQueue::default()) .add_systems( FixedUpdate, (prepare_paths, update_wandering_targets, movement).chain(), @@ -153,8 +155,7 @@ impl Plugin for PathfindingPlugin { ( merge_benchmark_stats, process_completed_paths, - crate::entities::shared_systems::async_pathfinding::poll_async_paths, - splice_completed_async_paths, + process_path_queue, ), ) .add_systems(Update, bench_report_system); @@ -163,26 +164,19 @@ impl Plugin for PathfindingPlugin { pub fn prepare_paths( mut commands: Commands, - mut counter: ResMut, + mut queue: ResMut, mut query: Query< ( Entity, &mut crate::entities::shared_components::Ambulatory, &Transform, - Option<&crate::entities::shared_components::PendingAsyncPath>, ), Without, >, tilemap: Res, ) { - for (entity, mut ambulatory, transform, pending_async) in query.iter_mut() { - if ambulatory.current_path.is_some() { - continue; - } - if pending_async.is_some() { - continue; - } - if ambulatory.target.is_none() { + for (entity, mut ambulatory, transform) in query.iter_mut() { + if ambulatory.current_path.is_some() || ambulatory.target.is_none() { continue; } let Some(target) = ambulatory.target else { @@ -204,19 +198,18 @@ pub fn prepare_paths( PATHFINDER_PROVISIONAL_NODE_LIMIT, ); if !provisional.is_empty() { - ambulatory.current_path = Some(provisional.clone()); + ambulatory.current_path = Some(provisional); ambulatory.path_index = 0; - let request_id = counter.next(); - let pending = - crate::entities::shared_systems::async_pathfinding::spawn_async_path_task( - &tilemap, + queue.pending.push_back((entity, start, goal)); + commands + .entity(entity) + .insert(crate::entities::shared_components::PendingPath { start, goal, - provisional, - request_id, - ); - commands.entity(entity).insert(pending); + waypoint_path: Vec::new(), + request_id: 0, + }); } else { let path = calculate_path_benchmarked(&tilemap, start, goal); ambulatory.current_path = Some(path); @@ -226,38 +219,38 @@ pub fn prepare_paths( } } -pub fn splice_completed_async_paths( - mut completed: ResMut, - mut query: Query<( - Entity, - &mut crate::entities::shared_components::Ambulatory, - &Transform, - )>, +pub fn process_path_queue( + mut commands: Commands, + mut queue: ResMut, + tilemap: Res, + mut query: Query< + (Entity, &mut Ambulatory, &Transform), + With, + >, ) { - if completed.paths.is_empty() { - return; - } + let mut processed = 0; + while processed < MAX_PATHS_PER_FRAME { + if let Some((entity, _old_start, goal)) = queue.pending.pop_front() { + processed += 1; - let mut to_process = Vec::new(); - for (request_id, path) in completed.paths.drain(..) { - to_process.push((request_id, path)); - } + if let Ok((_, mut ambulatory, transform)) = query.get_mut(entity) { + let actual_start = transform.translation.as_ivec3(); + let full_path = calculate_path_benchmarked(&tilemap, actual_start, goal); - for (request_id, path) in to_process { - for (entity, mut ambulatory, _transform) in query.iter_mut() { - if ambulatory.current_path.as_ref().is_none_or(|p| p != &path) { - let splice_index = find_splice_point(&path, ambulatory.path_index); - ambulatory.current_path = Some(path.clone()); - ambulatory.path_index = splice_index; + if !full_path.is_empty() { + ambulatory.current_path = Some(full_path); + ambulatory.path_index = 0; + } + commands + .entity(entity) + .remove::(); } + } else { + break; } } } -fn find_splice_point(path: &[Vec3], current_index: usize) -> usize { - current_index.min(path.len().saturating_sub(1)) -} - pub fn process_completed_paths( mut completed: ResMut, mut query: Query<( diff --git a/src/world/tiles/tilemap.rs b/src/world/tiles/tilemap.rs index 38806b1..028c005 100644 --- a/src/world/tiles/tilemap.rs +++ b/src/world/tiles/tilemap.rs @@ -195,98 +195,3 @@ impl TileMap { self.floor_tiles.get_mut(pos) } } - -/// Bit-packed bounding-box snapshot for async pathfinding. -/// 1 bit per tile = ~6KB for 50,000 tiles vs HashMap overhead. -/// Must be Send+Sync — no RefCell, no Arc. -#[derive(Clone, Debug)] -pub struct StandableBitGrid { - pub origin: IVec3, - pub size: UVec3, - pub bits: Vec, -} - -impl StandableBitGrid { - /// Create a bit-grid snapshot of all standable tiles within bounding box. - /// origin: min corner (inclusive), snapped to ITILE_SIZE - /// size: dimensions in tiles (not pixels) - pub fn new(origin: IVec3, size: UVec3, tilemap: &TileMap) -> Self { - let total_bits = (size.x * size.y * size.z) as usize; - let words = (total_bits + 63) / 64; - let mut bits = vec![0u64; words]; - - for bz in 0..size.z { - for by in 0..size.y { - for bx in 0..size.x { - let pos = IVec3::new( - origin.x + (bx as i32) * ITILE_SIZE, - origin.y + (by as i32) * ITILE_SIZE, - origin.z + (bz as i32) * ITILE_SIZE, - ); - if Self::tile_is_standable(tilemap, pos) { - let idx = ((bz * size.y * size.x) + (by * size.x) + bx) as usize; - bits[idx / 64] |= 1u64 << (idx % 64); - } - } - } - } - - Self { origin, size, bits } - } - - /// Standable check using TileMap (mirrors pathfinding.rs::is_standable_tile) - #[inline] - fn tile_is_standable(tilemap: &TileMap, pos: IVec3) -> bool { - let can_stand_in_tile = tilemap - .floor_tiles - .get(&pos) - .map(|t| t.can_stand_in()) - .unwrap_or(false); - let can_stand_in_fixture = tilemap - .fixture_tiles - .get(&pos) - .map(|t| t.can_stand_in()) - .unwrap_or(false); - let pos_below = pos - IVec3::new(0, 0, ITILE_SIZE); - let can_stand_on_tile_below = tilemap - .floor_tiles - .get(&pos_below) - .map(|t| t.can_stand_on()) - .unwrap_or(false); - let can_stand_on_fixture_below = tilemap - .fixture_tiles - .get(&pos_below) - .map(|t| t.can_stand_on()) - .unwrap_or(false); - - (can_stand_in_tile || can_stand_in_fixture) - && (can_stand_on_tile_below || can_stand_on_fixture_below) - } - - /// O(1) standable check using bit-grid coordinates. - #[inline] - pub fn is_standable_at(&self, bx: u32, by: u32, bz: u32) -> bool { - if bx >= self.size.x || by >= self.size.y || bz >= self.size.z { - return false; - } - let idx = ((bz * self.size.y * self.size.x) + (by * self.size.x) + bx) as usize; - self.bits[idx / 64] & (1u64 << (idx % 64)) != 0 - } - - /// Convert IVec3 world position to bit-grid coordinates. - /// Returns None if position is outside the grid bounds. - #[inline] - pub fn to_bit_coords(&self, pos: IVec3) -> Option<(u32, u32, u32)> { - let local = pos - self.origin; - if local.x < 0 || local.y < 0 || local.z < 0 { - return None; - } - let bx = (local.x / ITILE_SIZE) as u32; - let by = (local.y / ITILE_SIZE) as u32; - let bz = (local.z / ITILE_SIZE) as u32; - if bx >= self.size.x || by >= self.size.y || bz >= self.size.z { - return None; - } - Some((bx, by, bz)) - } -}