feat: async pathfinding with provisional paths and bit-grid snapshots
- Add StandableBitGrid: O(1) bit-packed snapshot (~6KB per 50k tiles vs HashMap overhead) - Implement two-tier pathfinding: sync for short paths (<64 tiles), provisional+async for long paths - calculate_provisional_path: capped A* returning path to best heuristic node - calculate_async_path: A* using bit-grid (Send+Sync, no thread_local) - prepare_paths system: dispatches provisional paths immediately, spawns async for full paths - poll_async_paths + splice_completed_async_paths: seamless path transition when async completes - Entities start walking immediately on provisional path while full path computes in background Architecture: FixedUpdate: prepare_paths → update_wandering_targets → movement PostUpdate: poll_async_paths → splice_completed_async_paths Priority: DF-like pathing (immediate movement) > performance > memory
This commit is contained in:
Generated
+1
@@ -2335,6 +2335,7 @@ dependencies = [
|
|||||||
"bevy",
|
"bevy",
|
||||||
"bevy_platform",
|
"bevy_platform",
|
||||||
"bevy_rand",
|
"bevy_rand",
|
||||||
|
"futures-lite",
|
||||||
"image",
|
"image",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
"noise",
|
"noise",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ rayon = "1.11.0"
|
|||||||
rustc-hash = "2.1.1"
|
rustc-hash = "2.1.1"
|
||||||
ahash = "0.8.12"
|
ahash = "0.8.12"
|
||||||
nohash-hasher = "0.2.0"
|
nohash-hasher = "0.2.0"
|
||||||
|
futures-lite = "2.6"
|
||||||
|
|
||||||
# Enable max optimizations for dependencies, but not for our code:
|
# Enable max optimizations for dependencies, but not for our code:
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
|
|||||||
-2380
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -4,6 +4,7 @@ pub const TILE_SIZE: f32 = TILE_PIXELS as f32 * PIXEL_RATIO;
|
|||||||
pub const ITILE_SIZE: i32 = TILE_SIZE as i32;
|
pub const ITILE_SIZE: i32 = TILE_SIZE as i32;
|
||||||
pub const SEED: u32 = 420;
|
pub const SEED: u32 = 420;
|
||||||
|
|
||||||
pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 100;
|
pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 64;
|
||||||
pub const PATHFINDER_MAX_NODES: usize = 5000;
|
pub const PATHFINDER_MAX_NODES: usize = 5000;
|
||||||
pub const PATHFINDER_WAYPOINT_THRESHOLD_TILES: i32 = 100;
|
pub const PATHFINDER_WAYPOINT_THRESHOLD_TILES: i32 = 100;
|
||||||
|
pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 64;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use bevy::tasks::Task;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Ambulatory {
|
pub struct Ambulatory {
|
||||||
@@ -18,6 +19,15 @@ pub struct PendingPath {
|
|||||||
pub request_id: u64,
|
pub request_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct PendingAsyncPath {
|
||||||
|
pub request_id: u64,
|
||||||
|
pub task: Task<Vec<Vec3>>,
|
||||||
|
pub goal: IVec3,
|
||||||
|
pub provisional_path: Vec<Vec3>,
|
||||||
|
pub provisional_path_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct PathRequestCounter {
|
pub struct PathRequestCounter {
|
||||||
pub next_id: u64,
|
pub next_id: u64,
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
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<std::cmp::Ordering> {
|
||||||
|
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<Vec3> {
|
||||||
|
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<AsyncPathNode> = 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<Vec3> {
|
||||||
|
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<CompletedPaths>,
|
||||||
|
) {
|
||||||
|
for (entity, mut pending) in pending_query.iter_mut() {
|
||||||
|
if !pending.task.is_finished() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path: Option<Vec<Vec3>> =
|
||||||
|
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::<PendingAsyncPath>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Vec3>,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
pub mod async_pathfinding;
|
||||||
pub mod pathfinding;
|
pub mod pathfinding;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use bevy::tasks::AsyncComputeTaskPool;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use std::{cell::RefCell, collections::BinaryHeap, time::Instant};
|
use std::{cell::RefCell, collections::BinaryHeap, time::Instant};
|
||||||
|
|
||||||
use crate::constants::{ITILE_SIZE, PATHFINDER_MAX_NODES, TILE_SIZE};
|
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::tiles::TileMap;
|
||||||
use crate::world::{chunks::ChunkMap, chunks::CHUNK_SIZE};
|
use crate::world::{chunks::ChunkMap, chunks::CHUNK_SIZE};
|
||||||
use crate::{constants::*, entities::shared_components::Ambulatory};
|
use crate::{constants::*, entities::shared_components::Ambulatory};
|
||||||
@@ -136,15 +141,116 @@ impl Plugin for PathfindingPlugin {
|
|||||||
app.insert_resource(PathfindingBenchmark::new(100))
|
app.insert_resource(PathfindingBenchmark::new(100))
|
||||||
.insert_resource(crate::entities::shared_components::CompletedPaths::default())
|
.insert_resource(crate::entities::shared_components::CompletedPaths::default())
|
||||||
.insert_resource(crate::entities::shared_components::PathRequestCounter::default())
|
.insert_resource(crate::entities::shared_components::PathRequestCounter::default())
|
||||||
.add_systems(FixedUpdate, (update_wandering_targets, movement).chain())
|
.insert_resource(
|
||||||
|
crate::entities::shared_systems::async_pathfinding::AsyncPathCounter::default(),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
FixedUpdate,
|
||||||
|
(prepare_paths, update_wandering_targets, movement).chain(),
|
||||||
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
PostUpdate,
|
PostUpdate,
|
||||||
(merge_benchmark_stats, process_completed_paths).chain(),
|
(
|
||||||
|
merge_benchmark_stats,
|
||||||
|
process_completed_paths,
|
||||||
|
crate::entities::shared_systems::async_pathfinding::poll_async_paths,
|
||||||
|
splice_completed_async_paths,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.add_systems(Update, bench_report_system);
|
.add_systems(Update, bench_report_system);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prepare_paths(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut counter: ResMut<crate::entities::shared_systems::async_pathfinding::AsyncPathCounter>,
|
||||||
|
mut query: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&mut crate::entities::shared_components::Ambulatory,
|
||||||
|
&Transform,
|
||||||
|
Option<&crate::entities::shared_components::PendingAsyncPath>,
|
||||||
|
),
|
||||||
|
Without<crate::entities::shared_components::PendingPath>,
|
||||||
|
>,
|
||||||
|
tilemap: Res<TileMap>,
|
||||||
|
) {
|
||||||
|
for (entity, mut ambulatory, transform, pending_async) in query.iter_mut() {
|
||||||
|
if let Some(target) = ambulatory.target {
|
||||||
|
if ambulatory.current_path.is_none() || pending_async.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let start = transform.translation.as_ivec3();
|
||||||
|
let goal = target.as_ivec3() - ivec3(0, 0, ITILE_SIZE);
|
||||||
|
let distance = octile_distance_3d(start, goal) / ITILE_SIZE;
|
||||||
|
|
||||||
|
if distance <= PATHFINDER_SHORT_PATH_MAX_TILES {
|
||||||
|
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
||||||
|
ambulatory.current_path = Some(path);
|
||||||
|
ambulatory.path_index = 0;
|
||||||
|
} else {
|
||||||
|
let provisional = calculate_provisional_path(
|
||||||
|
&tilemap,
|
||||||
|
start,
|
||||||
|
goal,
|
||||||
|
PATHFINDER_PROVISIONAL_NODE_LIMIT,
|
||||||
|
);
|
||||||
|
if !provisional.is_empty() {
|
||||||
|
ambulatory.current_path = Some(provisional.clone());
|
||||||
|
ambulatory.path_index = 0;
|
||||||
|
|
||||||
|
let request_id = counter.next();
|
||||||
|
let pending =
|
||||||
|
crate::entities::shared_systems::async_pathfinding::spawn_async_path_task(
|
||||||
|
&tilemap,
|
||||||
|
start,
|
||||||
|
goal,
|
||||||
|
provisional,
|
||||||
|
request_id,
|
||||||
|
);
|
||||||
|
commands.entity(entity).insert(pending);
|
||||||
|
} else {
|
||||||
|
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
||||||
|
ambulatory.current_path = Some(path);
|
||||||
|
ambulatory.path_index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn splice_completed_async_paths(
|
||||||
|
mut completed: ResMut<crate::entities::shared_components::CompletedPaths>,
|
||||||
|
mut query: Query<(
|
||||||
|
Entity,
|
||||||
|
&mut crate::entities::shared_components::Ambulatory,
|
||||||
|
&Transform,
|
||||||
|
)>,
|
||||||
|
) {
|
||||||
|
if completed.paths.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut to_process = Vec::new();
|
||||||
|
for (request_id, path) in completed.paths.drain(..) {
|
||||||
|
to_process.push((request_id, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_splice_point(path: &[Vec3], current_index: usize) -> usize {
|
||||||
|
current_index.min(path.len().saturating_sub(1))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_completed_paths(
|
pub fn process_completed_paths(
|
||||||
mut completed: ResMut<crate::entities::shared_components::CompletedPaths>,
|
mut completed: ResMut<crate::entities::shared_components::CompletedPaths>,
|
||||||
mut query: Query<(
|
mut query: Query<(
|
||||||
@@ -249,43 +355,37 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(target) = ambulatory.target {
|
if ambulatory.current_path.is_none() {
|
||||||
if ambulatory.current_path.is_none() {
|
return;
|
||||||
ambulatory.current_path = Some(calculate_path_benchmarked(
|
}
|
||||||
&tilemap,
|
|
||||||
transform.translation.as_ivec3(),
|
if ambulatory.walk_speed > 0. {
|
||||||
target.as_ivec3() - ivec3(0, 0, 1),
|
if ambulatory.step_recovery <= ambulatory.walk_speed as u32 {
|
||||||
));
|
ambulatory.step_recovery += 1;
|
||||||
ambulatory.path_index = 0;
|
return;
|
||||||
|
} else {
|
||||||
|
ambulatory.step_recovery = 0;
|
||||||
}
|
}
|
||||||
if ambulatory.walk_speed > 0. {
|
}
|
||||||
if ambulatory.step_recovery <= ambulatory.walk_speed as u32 {
|
|
||||||
ambulatory.step_recovery += 1;
|
if let Some(path) = &ambulatory.current_path {
|
||||||
return;
|
if ambulatory.path_index < path.len() {
|
||||||
} else {
|
let next_point = path[ambulatory.path_index];
|
||||||
ambulatory.step_recovery = 0;
|
let direction = (next_point - transform.translation).normalize();
|
||||||
|
transform.translation = next_point;
|
||||||
|
|
||||||
|
if direction.x > 0.0 {
|
||||||
|
transform.scale.x = PIXEL_RATIO;
|
||||||
|
} else if direction.x < 0.0 {
|
||||||
|
transform.scale.x = -PIXEL_RATIO;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(path) = &ambulatory.current_path {
|
if transform.translation.distance(next_point) < TILE_SIZE {
|
||||||
if ambulatory.path_index < path.len() {
|
ambulatory.path_index += 1;
|
||||||
let next_point = path[ambulatory.path_index];
|
|
||||||
let direction = (next_point - transform.translation).normalize();
|
|
||||||
transform.translation = next_point;
|
|
||||||
|
|
||||||
if direction.x > 0.0 {
|
|
||||||
transform.scale.x = PIXEL_RATIO;
|
|
||||||
} else if direction.x < 0.0 {
|
|
||||||
transform.scale.x = -PIXEL_RATIO;
|
|
||||||
}
|
|
||||||
|
|
||||||
if transform.translation.distance(next_point) < TILE_SIZE {
|
|
||||||
ambulatory.path_index += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ambulatory.current_path = None;
|
|
||||||
ambulatory.target = None;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
ambulatory.target = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -466,6 +566,88 @@ fn calculate_path_with_scratchpad(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn calculate_provisional_path(
|
||||||
|
tilemap: &TileMap,
|
||||||
|
start: IVec3,
|
||||||
|
goal: IVec3,
|
||||||
|
node_limit: usize,
|
||||||
|
) -> Vec<Vec3> {
|
||||||
|
if !is_standable_tile(tilemap, start) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE;
|
||||||
|
|
||||||
|
SCRATCHPAD.with(|s| {
|
||||||
|
let mut scratch = s.borrow_mut();
|
||||||
|
let capacity = ((estimated_tiles as usize).max(64)).min(4096);
|
||||||
|
scratch.clear_and_reserve(capacity);
|
||||||
|
|
||||||
|
let initial_h = octile_distance_3d(start, goal);
|
||||||
|
scratch.open_set.push(PathNode {
|
||||||
|
position: start,
|
||||||
|
f_score: initial_h,
|
||||||
|
g_score: 0,
|
||||||
|
});
|
||||||
|
scratch.g_scores.insert(start, 0);
|
||||||
|
|
||||||
|
let mut nodes_expanded: usize = 0;
|
||||||
|
let mut best_node = start;
|
||||||
|
let mut best_h = initial_h;
|
||||||
|
|
||||||
|
while let Some(current_node) = scratch.open_set.pop() {
|
||||||
|
let current = current_node.position;
|
||||||
|
nodes_expanded += 1;
|
||||||
|
|
||||||
|
let h = octile_distance_3d(current, goal);
|
||||||
|
if h < best_h {
|
||||||
|
best_h = h;
|
||||||
|
best_node = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current == goal {
|
||||||
|
return reconstruct_path(&scratch.came_from, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodes_expanded >= node_limit {
|
||||||
|
return reconstruct_path(&scratch.came_from, best_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
scratch.closed_set.insert(current);
|
||||||
|
|
||||||
|
for &move_dir in &ALLOWED_MOVES {
|
||||||
|
let neighbor_pos = current + move_dir;
|
||||||
|
|
||||||
|
if !is_standable_tile(tilemap, neighbor_pos)
|
||||||
|
|| scratch.closed_set.contains(&neighbor_pos)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let movement_cost = calculate_movement_cost(move_dir);
|
||||||
|
if movement_cost == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_g = *scratch.g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost;
|
||||||
|
|
||||||
|
if new_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
|
||||||
|
scratch.came_from.insert(neighbor_pos, current);
|
||||||
|
scratch.g_scores.insert(neighbor_pos, new_g);
|
||||||
|
let f = new_g + octile_distance_3d(neighbor_pos, goal);
|
||||||
|
scratch.open_set.push(PathNode {
|
||||||
|
position: neighbor_pos,
|
||||||
|
f_score: f,
|
||||||
|
g_score: new_g,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reconstruct_path(&scratch.came_from, best_node)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn bench_report_system(
|
pub fn bench_report_system(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
mut bench: ResMut<PathfindingBenchmark>,
|
mut bench: ResMut<PathfindingBenchmark>,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
|
use crate::constants::ITILE_SIZE;
|
||||||
|
|
||||||
/// Packed floor tile data for efficient storage. ~35 bytes vs 76 bytes tuple.
|
/// Packed floor tile data for efficient storage. ~35 bytes vs 76 bytes tuple.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct FloorTileData {
|
pub struct FloorTileData {
|
||||||
@@ -193,3 +195,98 @@ impl TileMap {
|
|||||||
self.floor_tiles.get_mut(pos)
|
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<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user