Fix HierarchicalPathFinder pathing from inaccessible source locations.

When a search is initiated from an inaccessible source location, a path is still allowed if there is an adjacent, accessible location the unit can move into. The local pathfinder and HierarchicalPathFinder already account for this logic, but HPF has some bugs.

Firstly, when the HierarchicalPathFinder heuristic is being evaluated, it assumes all cells being explored are accessible - this is important for performance as it avoids constantly rechecking the accessibility of cells. Although this fact holds true for cells explored by the path search, it does not hold true for cells being added as the initial starting points of the search.

Secondly, when checking for adjacent locations to an inaccessible source cell, we checked only if these were still on the map. This is insufficient - we need to know if movement between the source cell and the adjacent cell is possible.

The fixes resolve this by:
- Teaching the heuristic an extra parameter to know if the location is known to be accessible. This allow an accessibility check to be performed for starting locations which stops HPF mistakenly assuming the abstract node for that location is the one we need to consider, and to correctly check the adjacent locations and their abstract nodes instead. The parameter means will can still skip the accessibility check in the typical case where the path search is being expanded, preserving performance.
- When adjacent cells are considered we now check if movement to them is possible. This stops HPF from allowing jumps over height discontinuities (i.e. no magically jumping up or down cliffs) and thinking a path is possible when it is not.
This commit is contained in:
RoosterDragon
2023-04-30 12:24:49 +01:00
committed by Matthias Mailänder
parent 0788e5ff3e
commit 2fe7e1bff9
2 changed files with 30 additions and 22 deletions

View File

@@ -763,7 +763,7 @@ namespace OpenRA.Mods.Common.Pathfinder
foreach (var dir in CVec.Directions)
{
var adjacentSource = source + dir;
if (!world.Map.Contains(adjacentSource))
if (!MovementAllowedBetweenCells(source, adjacentSource))
continue;
var adjacentSourceAbstractCell = AbstractCellForLocalCell(adjacentSource);
@@ -959,7 +959,7 @@ namespace OpenRA.Mods.Common.Pathfinder
foreach (var dir in CVec.Directions)
{
var adjacentSource = source + dir;
if (!world.Map.Contains(adjacentSource))
if (!MovementAllowedBetweenCells(source, adjacentSource))
continue;
var abstractAdjacentSource = AbstractCellForLocalCell(adjacentSource);
@@ -1118,12 +1118,12 @@ namespace OpenRA.Mods.Common.Pathfinder
/// (the heuristic) for a local path search. The abstract search must run in the opposite direction to the
/// local search. So when searching from source to target, the abstract search must be from target to source.
/// </summary>
Func<CPos, int> Heuristic(PathSearch abstractSearch, int estimatedSearchSize,
Func<CPos, bool, int> Heuristic(PathSearch abstractSearch, int estimatedSearchSize,
HashSet<CPos> sources, List<CPos> unpathableNodes)
{
var nodeForCostLookup = new Dictionary<CPos, CPos>(estimatedSearchSize);
var graph = (SparsePathGraph)abstractSearch.Graph;
return cell =>
return (cell, knownAccessible) =>
{
// When dealing with an unreachable source cell, the path search will check adjacent locations.
// These cells may be reachable, but may represent jumping into an area cut off from the target.
@@ -1131,14 +1131,19 @@ namespace OpenRA.Mods.Common.Pathfinder
if (unpathableNodes != null && unpathableNodes.Contains(cell))
return PathGraph.PathCostForInvalidPath;
// All other cells searched by the heuristic are guaranteed to be reachable.
// During a search, all other cells searched by the heuristic are guaranteed to be reachable.
// So we don't need to handle an abstract cell lookup failing, or the search failing to expand.
// Cells added as initial starting points for the search are filtered out if they aren't reachable.
// The search only explores accessible cells from then on.
// Cells added as initial starting points for the search might not be reachable,
// so for those we need to perform accessibility checks.
// If the exceptions here do fire, they indicate a bug. The abstract graph is considering a cell to be
// unreachable, but the local pathfinder thinks it is reachable. We must fix the abstract graph to also
// consider the cell to be reachable.
var maybeAbstractCell = AbstractCellForLocalCellNoAccessibleCheck(cell);
CPos? maybeAbstractCell;
if (knownAccessible)
maybeAbstractCell = AbstractCellForLocalCellNoAccessibleCheck(cell);
else
maybeAbstractCell = AbstractCellForLocalCell(cell);
if (maybeAbstractCell == null)
{
// If the source cell is unreachable, use one of the adjacent reachable cells instead.
@@ -1146,14 +1151,14 @@ namespace OpenRA.Mods.Common.Pathfinder
{
foreach (var dir in CVec.Directions)
{
var adjacentSource = cell + dir;
if (!world.Map.Contains(adjacentSource) ||
(unpathableNodes != null && unpathableNodes.Contains(adjacentSource)))
var adjacentCell = cell + dir;
if (!MovementAllowedBetweenCells(cell, adjacentCell) ||
(unpathableNodes != null && unpathableNodes.Contains(adjacentCell)))
continue;
// Ideally we'd choose the cheapest cell rather than just any one of them,
// but we're lazy and this is an edge case.
maybeAbstractCell = AbstractCellForLocalCell(adjacentSource);
maybeAbstractCell = AbstractCellForLocalCell(adjacentCell);
if (maybeAbstractCell != null)
break;
}
@@ -1258,7 +1263,7 @@ namespace OpenRA.Mods.Common.Pathfinder
PathSearch GetLocalPathSearch(
Actor self, IEnumerable<CPos> srcs, CPos dst, Func<CPos, int> customCost,
Actor ignoreActor, BlockedByActor check, bool laneBias, Grid? grid, int heuristicWeightPercentage,
Func<CPos, int> heuristic = null,
Func<CPos, bool, int> heuristic = null,
bool inReverse = false,
PathSearch.IRecorder recorder = null)
{