Improve long distance path-finding if target chunk is unloaded

This commit is contained in:
Dmitry Kashitsyn 2024-04-08 21:07:25 +05:00
parent 95cddc6b63
commit ae652eef85
No known key found for this signature in database
GPG Key ID: 6C747B56FC5F1ABA
4 changed files with 113 additions and 37 deletions

View File

@ -87,6 +87,8 @@ pub struct TraversalConfig {
pub can_climb: bool,
/// Whether the agent can fly.
pub can_fly: bool,
/// Whether chunk containing target position is currently loaded
pub is_target_loaded: bool,
}
const DIAGONALS: [Vec2<i32>; 8] = [
@ -125,7 +127,7 @@ impl Route {
let next1 = self.next(1).unwrap_or(next0);
// Stop using obstructed paths
if !walkable(vol, next1) {
if !walkable(vol, next0) || !walkable(vol, next1) {
return None;
}
@ -387,7 +389,7 @@ impl Chaser {
// theory this shouldn't happen, but in practice the world is full
// of unpredictable obstacles that are more than willing to mess up
// our day. TODO: Come up with a better heuristic for this
if end_to_tgt > pos_to_tgt * 0.3 + 5.0 && complete {
if end_to_tgt > pos_to_tgt * 0.3 + 5.0 && complete && traversal_cfg.is_target_loaded {
None
} else if thread_rng().gen::<f32>() < 0.001 {
self.route = None;
@ -420,6 +422,7 @@ impl Chaser {
.unwrap_or(true)
|| self.astar.is_some()
|| self.route.is_none()
|| !traversal_cfg.is_target_loaded
{
self.last_search_tgt = Some(tgt);
@ -540,6 +543,12 @@ where
get_walkable_z(endf.map(|e| e.floor() as i32)),
) {
(Some(start), Some(end)) => (start, end),
// Special case for partially loaded path finding
(Some(start), None) if !traversal_cfg.is_target_loaded => {
(start, endf.map(|e| e.floor() as i32))
},
_ => return (None, false),
};
@ -608,7 +617,7 @@ where
.filter(|_| {
vol.get(pos - Vec3::unit_z())
.map(|b| !b.is_liquid())
.unwrap_or(true)
.unwrap_or(traversal_cfg.is_target_loaded)
|| traversal_cfg.can_climb
|| traversal_cfg.can_fly
})
@ -622,17 +631,17 @@ where
|| vol
.get(pos + Vec3::unit_z() * 2)
.map(|b| !b.is_solid())
.unwrap_or(true))
.unwrap_or(traversal_cfg.is_target_loaded))
&& (dir.z < 2
|| vol
.get(pos + Vec3::unit_z() * 3)
.map(|b| !b.is_solid())
.unwrap_or(true))
.unwrap_or(traversal_cfg.is_target_loaded))
&& (dir.z >= 0
|| vol
.get(pos + *dir + Vec3::unit_z() * 2)
.map(|b| !b.is_solid())
.unwrap_or(true)))
.unwrap_or(traversal_cfg.is_target_loaded)))
})
.map(|(pos, dir)| {
let destination = pos + dir;
@ -651,28 +660,34 @@ where
let satisfied = |pos: &Vec3<i32>| pos == &end;
let mut new_astar = match astar.take() {
None => Astar::new(25_000, start, DefaultHashBuilder::default()),
None => Astar::new(
if traversal_cfg.is_target_loaded {
// Normal mode
25_000
} else {
// Most of the times we would need to plot within current chunk,
// so half of intra-site limit should be enough in most cases
500
},
start,
DefaultHashBuilder::default(),
),
Some(astar) => astar,
};
let path_result = new_astar.poll(100, heuristic, neighbors, satisfied);
*astar = Some(new_astar);
match path_result {
PathResult::Path(path, _cost) => {
*astar = None;
(Some(path), true)
PathResult::Path(path, _cost) => (Some(path), true),
PathResult::None(path) => (Some(path), false),
PathResult::Exhausted(path) => (Some(path), false),
PathResult::Pending => {
// Keep astar for the next iteration
*astar = Some(new_astar);
(None, false)
},
PathResult::None(path) => {
*astar = None;
(Some(path), false)
},
PathResult::Exhausted(path) => {
*astar = None;
(Some(path), false)
},
PathResult::Pending => (None, false),
}
}

View File

@ -66,7 +66,7 @@ struct DefaultState {
fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathResult<Vec2<i32>> {
let heuristic = |tile: &Vec2<i32>, _: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
let mut astar = Astar::new(1000, start, BuildHasherDefault::<FxHasher64>::default());
let mut astar = Astar::new(1_000, start, BuildHasherDefault::<FxHasher64>::default());
let transition = |a: Vec2<i32>, b: Vec2<i32>| {
let distance = a.as_::<f32>().distance(b.as_());
@ -212,7 +212,7 @@ fn path_site(
}
}
fn path_towns(
fn path_between_towns(
start: SiteId,
end: SiteId,
sites: &Sites,
@ -344,9 +344,7 @@ fn goto<S: State>(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Ac
// Get the next waypoint on the route toward the goal
let waypoint = waypoint.get_or_insert_with(|| {
let rpos = wpos - ctx.npc.wpos;
let len = rpos.magnitude();
let wpos = ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST);
// let rpos = wpos - ctx.npc.wpos;
wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()))
});
@ -397,7 +395,12 @@ fn goto_flying<S: State>(
.stop_if(move |ctx: &mut NpcCtx| {
ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)
})
.debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z))
.debug(move || {
format!(
"goto flying ({}, {}, {}), goal dist {}",
wpos.x, wpos.y, wpos.z, goal_dist
)
})
.map(|_, _| {})
}
@ -406,7 +409,12 @@ fn goto_flying<S: State>(
fn goto_2d<S: State>(wpos2d: Vec2<f32>, speed_factor: f32, goal_dist: f32) -> impl Action<S> {
now(move |ctx, _| {
let wpos = wpos2d.with_z(ctx.world.sim().get_surface_alt_approx(wpos2d.as_()));
goto(wpos, speed_factor, goal_dist)
goto(wpos, speed_factor, goal_dist).debug(move || {
format!(
"goto 2d ({}, {}), z {}, goal dist {}",
wpos2d.x, wpos2d.y, wpos.z, goal_dist
)
})
})
}
@ -431,6 +439,12 @@ fn goto_2d_flying<S: State>(
waypoint_dist,
height_offset,
)
.debug(move || {
format!(
"goto 2d flying ({}, {}), goal dist {}",
wpos2d.x, wpos2d.y, goal_dist
)
})
})
}
@ -439,6 +453,7 @@ where
F: FnMut(&mut NpcCtx) -> Option<Vec2<f32>> + Clone + Send + Sync + 'static,
{
until(move |ctx, next_point: &mut F| {
// Pick next waypoint, return if path ended
let wpos = next_point(ctx)?;
let wpos_site = |wpos: Vec2<f32>| {
@ -448,13 +463,24 @@ where
.and_then(|chunk| chunk.sites.first().copied())
};
// If we're traversing within a site, to intra-site pathfinding
let wpos_sites_contain = |wpos: Vec2<f32>, site: Id<world::site::Site>| {
ctx.world
.sim()
.get(wpos.as_().wpos_to_cpos())
.map(|chunk| chunk.sites.contains(&site))
.unwrap_or(false)
};
let npc_wpos = ctx.npc.wpos;
// If we're traversing within a site, do intra-site pathfinding
if let Some(site) = wpos_site(wpos) {
let mut site_exit = wpos;
while let Some(next) = next_point(ctx).filter(|next| wpos_site(*next) == Some(site)) {
while let Some(next) = next_point(ctx).filter(|next| wpos_sites_contain(*next, site)) {
site_exit = next;
}
// Navigate through the site to the site exit
if let Some(path) = path_site(wpos, site_exit, site, ctx.index) {
Some(Either::Left(
seq(path.into_iter().map(move |wpos| goto_2d(wpos, 1.0, 8.0))).then(goto_2d(
@ -464,13 +490,34 @@ where
)),
))
} else {
Some(Either::Right(goto_2d(site_exit, speed_factor, 8.0)))
// No intra-site path found, just attempt to move towards the exit node
Some(Either::Right(
goto_2d(site_exit, speed_factor, 8.0)
.debug(move || {
format!(
"direct from {}, {}, ({}) to site exit at {}, {}",
npc_wpos.x, npc_wpos.y, npc_wpos.z, site_exit.x, site_exit.y
)
})
.boxed(),
))
}
} else {
Some(Either::Right(goto_2d(wpos, speed_factor, 8.0)))
// We're in the middle of a road, just go to the next waypoint
Some(Either::Right(
goto_2d(wpos, speed_factor, 8.0)
.debug(move || {
format!(
"from {}, {}, ({}) to the next waypoint at {}, {}",
npc_wpos.x, npc_wpos.y, npc_wpos.z, wpos.x, wpos.y
)
})
.boxed(),
))
}
})
.with_state(next_point)
.debug(|| "traverse points")
}
/// Try to travel to a site. Where practical, paths will be taken.
@ -483,7 +530,7 @@ fn travel_to_point<S: State>(wpos: Vec2<f32>, speed_factor: f32) -> impl Action<
let mut points = (1..n as usize + 1).map(move |i| start + diff * (i as f32 / n));
traverse_points(move |_| points.next(), speed_factor)
})
.debug(|| "travel to point")
.debug(move || format!("travel to point {}, {}", wpos.x, wpos.y))
}
/// Try to travel to a site. Where practical, paths will be taken.
@ -496,16 +543,16 @@ fn travel_to_site<S: State>(tgt_site: SiteId, speed_factor: f32) -> impl Action<
// If we're currently in a site, try to find a path to the target site via
// tracks
if let Some(current_site) = ctx.npc.current_site
&& let Some(tracks) = path_towns(current_site, tgt_site, sites, ctx.world)
&& let Some(tracks) = path_between_towns(current_site, tgt_site, sites, ctx.world)
{
let mut nodes = tracks.path
let mut path_nodes = tracks.path
.into_iter()
.flat_map(move |(track_id, reversed)| (0..)
.map(move |node_idx| (node_idx, track_id, reversed)));
traverse_points(move |ctx| {
let (node_idx, track_id, reversed) = nodes.next()?;
let (node_idx, track_id, reversed) = path_nodes.next()?;
let nodes = &ctx.world.civs().tracks.get(track_id).path().nodes;
// Handle the case where we walk paths backward
@ -568,7 +615,7 @@ fn travel_to_site<S: State>(tgt_site: SiteId, speed_factor: f32) -> impl Action<
// .boxed()
} else if let Some(site) = sites.get(tgt_site) {
// If all else fails, just walk toward the target site in a straight line
travel_to_point(site.wpos.map(|e| e as f32 + 0.5), speed_factor).boxed()
travel_to_point(site.wpos.map(|e| e as f32 + 0.5), speed_factor).debug(|| "travel to point fallback").boxed()
} else {
// If we can't find a way to get to the site at all, there's nothing more to be done
finish().boxed()

View File

@ -277,6 +277,18 @@ impl<'a> AgentData<'a> {
.map(|pos| pos.as_())
.unwrap_or(travel_to);
let in_loaded_chunk = |pos: Vec3<f32>| {
read_data
.terrain
.contains_key(read_data.terrain.pos_key(pos.map(|e| e.floor() as i32)))
};
// If current position lies inside a loaded chunk, we need to plan routes using
// voxel info. If target happens to be in an unloaded chunk,
// we need to make our way to the current chunk border, and
// then reroute if needed.
let is_target_loaded = in_loaded_chunk(chase_tgt);
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
@ -284,6 +296,7 @@ impl<'a> AgentData<'a> {
chase_tgt,
TraversalConfig {
min_tgt_dist: self.traversal_config.min_tgt_dist * 1.25,
is_target_loaded,
..self.traversal_config
},
) {

View File

@ -200,6 +200,7 @@ impl<'a> System<'a> for Sys {
min_tgt_dist: scale * moving_body.map_or(1.0, |body| body.max_radius()),
can_climb: moving_body.map_or(false, Body::can_climb),
can_fly: moving_body.map_or(false, |b| b.fly_thrust().is_some()),
is_target_loaded: true,
};
let health_fraction = health.map_or(1.0, Health::fraction);