refactor: simplify to time-sliced synchronous path queue

The async pathfinding with StandableBitGrid caused massive lag due to synchronous
bounding box calculation in the main thread (millions of hashmap lookups for long paths).
Additionally, the splicing logic had a catastrophic bug where it applied a single
finished path to ALL entities unconditionally.

Solution:
- Revert to thread-local synchronous A*
- Implement PathRequestQueue to process max 8 paths per frame
- Keep provisional paths for immediate movement
- Since entity walks provisional path, full path calculates from current position,
  eliminating the need for complex splicing logic.
This commit is contained in:
2026-03-18 17:23:48 +00:00
parent 31c93f7926
commit 00cbd92491
5 changed files with 45 additions and 454 deletions
+45 -52
View File
@@ -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<crate::entities::shared_systems::async_pathfinding::AsyncPathCounter>,
mut queue: ResMut<PathRequestQueue>,
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 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<crate::entities::shared_components::CompletedPaths>,
mut query: Query<(
Entity,
&mut crate::entities::shared_components::Ambulatory,
&Transform,
)>,
pub fn process_path_queue(
mut commands: Commands,
mut queue: ResMut<PathRequestQueue>,
tilemap: Res<TileMap>,
mut query: Query<
(Entity, &mut Ambulatory, &Transform),
With<crate::entities::shared_components::PendingPath>,
>,
) {
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::<crate::entities::shared_components::PendingPath>();
}
} 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<crate::entities::shared_components::CompletedPaths>,
mut query: Query<(