From ae652eef8558a7091ce925192720688dbf8d321f Mon Sep 17 00:00:00 2001 From: Dmitry Kashitsyn Date: Mon, 8 Apr 2024 21:07:25 +0500 Subject: [PATCH] Improve long distance path-finding if target chunk is unloaded --- common/src/path.rs | 57 ++++++++++++++--------- rtsim/src/rule/npc_ai.rs | 79 +++++++++++++++++++++++++------- server/agent/src/action_nodes.rs | 13 ++++++ server/src/sys/agent.rs | 1 + 4 files changed, 113 insertions(+), 37 deletions(-) diff --git a/common/src/path.rs b/common/src/path.rs index e3fedc0722..1e8d3881d5 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -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; 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::() < 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| 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), } } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 1c65dd1b19..a1591fcee4 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -66,7 +66,7 @@ struct DefaultState { fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathResult> { let heuristic = |tile: &Vec2, _: &Vec2| tile.as_::().distance(end.as_()); - let mut astar = Astar::new(1000, start, BuildHasherDefault::::default()); + let mut astar = Astar::new(1_000, start, BuildHasherDefault::::default()); let transition = |a: Vec2, b: Vec2| { let distance = a.as_::().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(wpos: Vec3, 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( .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( fn goto_2d(wpos2d: Vec2, speed_factor: f32, goal_dist: f32) -> impl Action { 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( 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> + 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| { @@ -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, site: Id| { + 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(wpos: Vec2, 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(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(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() diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index a250adfe4c..5de5ea175a 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -277,6 +277,18 @@ impl<'a> AgentData<'a> { .map(|pos| pos.as_()) .unwrap_or(travel_to); + let in_loaded_chunk = |pos: Vec3| { + 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 }, ) { diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 42ee585091..9fa0c1d8de 100755 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -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);