mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Add a max cost parameter to the astar algorithm so that it will terminate as exausted if this limit is reached. This is used to optimize site pathfinding by exiting early from finding a novel path if we know it won't be used.
This commit is contained in:
parent
ed94c1c1b6
commit
d18100c87a
@ -45,9 +45,19 @@ impl<S: Eq> PartialOrd for PathEntry<S> {
|
||||
}
|
||||
|
||||
pub enum PathResult<T> {
|
||||
/// No reachable nodes were satisfactory.
|
||||
///
|
||||
/// Contains path to node with the lowest heuristic value (out of the
|
||||
/// explored nodes).
|
||||
None(Path<T>),
|
||||
/// Either max_iters or max_cost was reached.
|
||||
///
|
||||
/// Contains path to node with the lowest heuristic value (out of the
|
||||
/// explored nodes).
|
||||
Exhausted(Path<T>),
|
||||
// second field is cost
|
||||
/// Path succefully found.
|
||||
///
|
||||
/// Second field is cost.
|
||||
Path(Path<T>, f32),
|
||||
Pending,
|
||||
}
|
||||
@ -84,6 +94,7 @@ struct NodeEntry<S> {
|
||||
pub struct Astar<S, Hasher> {
|
||||
iter: usize,
|
||||
max_iters: usize,
|
||||
max_cost: f32,
|
||||
potential_nodes: BinaryHeap<PathEntry<S>>, // cost, node pairs
|
||||
visited_nodes: HashMap<S, NodeEntry<S>, Hasher>,
|
||||
/// Node with the lowest heuristic value so far.
|
||||
@ -109,6 +120,7 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
|
||||
pub fn new(max_iters: usize, start: S, hasher: H) -> Self {
|
||||
Self {
|
||||
max_iters,
|
||||
max_cost: f32::MAX,
|
||||
iter: 0,
|
||||
potential_nodes: core::iter::once(PathEntry {
|
||||
cost_estimate: 0.0,
|
||||
@ -127,6 +139,11 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_max_cost(mut self, max_cost: f32) -> Self {
|
||||
self.max_cost = max_cost;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn poll<I>(
|
||||
&mut self,
|
||||
iters: usize,
|
||||
@ -143,15 +160,28 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
|
||||
{
|
||||
let iter_limit = self.max_iters.min(self.iter + iters);
|
||||
while self.iter < iter_limit {
|
||||
if let Some(PathEntry { node, .. }) = self.potential_nodes.pop() {
|
||||
if let Some(PathEntry {
|
||||
node,
|
||||
cost_estimate,
|
||||
}) = self.potential_nodes.pop()
|
||||
{
|
||||
let (node_cheapest, came_from) = self
|
||||
.visited_nodes
|
||||
.get(&node)
|
||||
.map(|n| (n.cheapest_score, n.came_from.clone()))
|
||||
.expect("");
|
||||
.expect("All nodes in the queue should be included in visisted_nodes");
|
||||
|
||||
if satisfied(&node) {
|
||||
return PathResult::Path(self.reconstruct_path_to(node), node_cheapest);
|
||||
// Note, we assume that cost_estimate isn't an overestimation
|
||||
// (i.e. that `heuristic` doesn't overestimate).
|
||||
} else if cost_estimate > self.max_cost {
|
||||
return PathResult::Exhausted(
|
||||
self.closest_node
|
||||
.clone()
|
||||
.map(|(lc, _)| self.reconstruct_path_to(lc))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
} else {
|
||||
for (neighbor, transition) in neighbors(&node) {
|
||||
if neighbor == came_from {
|
||||
@ -174,7 +204,7 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
|
||||
})
|
||||
.is_some();
|
||||
let h = heuristic(&neighbor, &node);
|
||||
// note that cheapest_scores does not include the heuristic
|
||||
// note that cheapest_score does not include the heuristic
|
||||
// priority queue does include heuristic
|
||||
let cost_estimate = cost + h;
|
||||
|
||||
|
@ -1130,10 +1130,12 @@ impl Civs {
|
||||
|
||||
// Find neighbors
|
||||
// Note, the maximum distance that I have so far observed not hitting the
|
||||
// iteration limit in `find_path` is 297. So I think this is a reasonible
|
||||
// iteration limit in `find_path` is 364. So I think this is a reasonible
|
||||
// limit (although the relationship between distance and pathfinding iterations
|
||||
// can be a bit variable).
|
||||
const MAX_NEIGHBOR_DISTANCE: f32 = 350.0;
|
||||
// can be a bit variable). Note, I have seen paths reach the iteration limit
|
||||
// with distances as small as 137, so this certainly doesn't catch all
|
||||
// cases that would fail.
|
||||
const MAX_NEIGHBOR_DISTANCE: f32 = 400.0;
|
||||
let mut nearby = self
|
||||
.sites
|
||||
.iter()
|
||||
@ -1173,18 +1175,8 @@ impl Civs {
|
||||
let start = loc;
|
||||
let end = self.sites.get(nearby).center;
|
||||
// Find a novel path.
|
||||
//
|
||||
// We rely on the cost at least being equal to the distance, to avoid
|
||||
// unnecessary novel pathfinding.
|
||||
let maybe_path = ((start.distance_squared(end) as f32).sqrt() < max_novel_cost)
|
||||
.then(|| {
|
||||
prof_span!("find path");
|
||||
let get_bridge = |start| self.bridges.get(&start).map(|(end, _)| *end);
|
||||
find_path(ctx, get_bridge, start, end)
|
||||
})
|
||||
.flatten()
|
||||
.filter(|&(_, cost)| cost < max_novel_cost);
|
||||
if let Some((path, cost)) = maybe_path {
|
||||
let get_bridge = |start| self.bridges.get(&start).map(|(end, _)| *end);
|
||||
if let Some((path, cost)) = find_path(ctx, get_bridge, start, end, max_novel_cost) {
|
||||
// Write the track to the world as a path
|
||||
for locs in path.nodes().windows(3) {
|
||||
let mut randomize_offset = false;
|
||||
@ -1328,7 +1320,9 @@ fn find_path(
|
||||
get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>,
|
||||
a: Vec2<i32>,
|
||||
b: Vec2<i32>,
|
||||
max_path_cost: f32,
|
||||
) -> Option<(Path<Vec2<i32>>, f32)> {
|
||||
prof_span!("find_path");
|
||||
const MAX_PATH_ITERS: usize = 100_000;
|
||||
let sim = &ctx.sim;
|
||||
// NOTE: If heuristic overestimates the actual cost, then A* is not guaranteed
|
||||
@ -1355,7 +1349,8 @@ fn find_path(
|
||||
MAX_PATH_ITERS,
|
||||
a,
|
||||
BuildHasherDefault::<FxHasher64>::default(),
|
||||
);
|
||||
)
|
||||
.with_max_cost(max_path_cost);
|
||||
astar
|
||||
.poll(MAX_PATH_ITERS, heuristic, neighbors, satisfied)
|
||||
.into_path()
|
||||
|
Loading…
Reference in New Issue
Block a user