Skip to content

Commit

Permalink
Teach HierarchicalPathFinder about Immovable actors.
Browse files Browse the repository at this point in the history
By tracking updates on the ActorMap the HierarchicalPathFinder can be aware of actors moving around the map. We track a subset of immovable actors referred to as super-immovable actors. These actors can be treated as impassable obstacles just like terrain. When a path needs to be found the abstract path will guide the search around these super-immovable actors just like it can guide the search around impassable terrain. For path searches that were previously imperformant because some immovable actors created a bottleneck that needed to be routed around, these will now be performant instead. Path searches with bottlenecks created by items such as trees, walls and buildings should see a performance improvement. Bottlenecks created by other units will not benefit.

We now maintain two sets of HPFs. One is aware of immovable actors and will be used for path searches that request BlockedByActor.Immovable, BlockedByActor.Stationary and BlockedByActor.All to guide that around the immovable obstacles. The other is aware of terrain only and will be used for searches that request BlockedByActor.None, or if an ignoreActor is provided. A new UI dropdown when using the `/hpf` command will allow switching between the visuals of the two sets.
  • Loading branch information
RoosterDragon committed Aug 13, 2022
1 parent f711cdf commit 8f9abbc
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 66 deletions.
1 change: 1 addition & 0 deletions OpenRA.Game/Traits/TraitsInterfaces.cs
Expand Up @@ -213,6 +213,7 @@ public interface IActorMap
bool AnyActorsAt(CPos a);
bool AnyActorsAt(CPos a, SubCell sub, bool checkTransient = true);
bool AnyActorsAt(CPos a, SubCell sub, Func<Actor, bool> withCondition);
IEnumerable<Actor> AllActors();
void AddInfluence(Actor self, IOccupySpace ios);
void RemoveInfluence(Actor self, IOccupySpace ios);
int AddCellTrigger(CPos[] cells, Action<Actor> onEntry, Action<Actor> onExit);
Expand Down
200 changes: 157 additions & 43 deletions OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs
Expand Up @@ -14,6 +14,7 @@
using System.Collections.ObjectModel;
using System.Linq;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;

namespace OpenRA.Mods.Common.Pathfinder
{
Expand Down Expand Up @@ -74,17 +75,23 @@ namespace OpenRA.Mods.Common.Pathfinder
/// <para>This implementation is aware of movement costs over terrain given by
/// <see cref="Locomotor.MovementCostToEnterCell"/>. It is aware of changes to the costs in terrain and able to
/// update the abstract graph when this occurs. It is able to search the abstract graph as if
/// <see cref="BlockedByActor.None"/> had been specified. It is not aware of actors on the map. So blocking actors
/// will not be accounted for in the heuristic.</para>
/// <see cref="BlockedByActor.None"/> had been specified. If <see cref="BlockedByActor.Immovable"/> is given in the
/// constructor, the abstract graph will additionally account for a subset of immovable actors (referred to
/// internally as super-immovable actors) using the same rules as <see cref="Locomotor.CanMoveFreelyInto"/>. It
/// will be aware of changes to actors on the map and update the abstract graph when this occurs. Other types of
/// blocking actors will not be accounted for in the heuristic.</para>
///
/// <para>If the obstacle on the map is from terrain (e.g. a cliff or lake) the heuristic will work well. If the
/// obstacle is from a blocking actor (trees, walls, buildings, units) the heuristic is unaware of these. Therefore
/// the same problem where the search goes in the wrong direction is possible, e.g. through a choke-point that has
/// been walled off. In this scenario the performance benefit will be lost, as the search will have to explore more
/// nodes until it can get around the obstacle.</para>
/// obstacle is from super-immovable actor (e.g. trees, walls, buildings) and
/// <see cref="BlockedByActor.Immovable"/> was given, the heuristic will work well. If the obstacle is from other
/// actors (e.g. units) then the heuristic is unaware of these. Therefore the same problem where the search goes in
/// the wrong direction is possible, e.g. through a choke-point that has units blocking it. In this scenario the
/// performance benefit will be lost, as the search will have to explore more nodes until it can get around the
/// obstacle.</para>
///
/// <para>In summary, the <see cref="HierarchicalPathFinder"/> reduces the performance impact of path searches that
/// must go around terrain, but does not improve performance of searches that must go around actors.</para>
/// must go around terrain, and some types of actor, but does not improve performance of searches that must go
/// around the remaining types of actor.</para>
/// </remarks>
public sealed class HierarchicalPathFinder
{
Expand All @@ -93,8 +100,10 @@ public sealed class HierarchicalPathFinder

readonly World world;
readonly Locomotor locomotor;
readonly IActorMap actorMap;
readonly Func<CPos, CPos, int> costEstimator;
readonly HashSet<int> dirtyGridIndexes = new HashSet<int>();
readonly HashSet<CPos> cellsWithSuperImmovableActor;
Grid mapBounds;
int gridXs;
int gridYs;
Expand Down Expand Up @@ -224,10 +233,11 @@ public List<GraphConnection> GetConnections(CPos position)
}
}

public HierarchicalPathFinder(World world, Locomotor locomotor)
public HierarchicalPathFinder(World world, Locomotor locomotor, IActorMap actorMap, BlockedByActor check)
{
this.world = world;
this.locomotor = locomotor;
this.actorMap = actorMap;
if (locomotor.Info.TerrainSpeeds.Count == 0)
return;

Expand All @@ -241,6 +251,26 @@ public HierarchicalPathFinder(World world, Locomotor locomotor)
// When we build the cost table, it depends on the movement costs of the cells at that time.
// When this changes, we must update the cost table.
locomotor.CellCostChanged += RequireCostRefreshInCell;

if (check == BlockedByActor.Immovable)
{
// When we account for immovable actors, it depends on the actors on the map.
// When this changes, we must update the cost table.
actorMap.CellUpdated += RequireBlockingRefreshInCell;

// Determine immovable cells from actors already on the map.
cellsWithSuperImmovableActor = actorMap.AllActors()
.Where(ActorIsSuperImmovable)
.SelectMany(a =>
a.OccupiesSpace.OccupiedCells()
.Select(oc => oc.Cell)
.Where(c => ActorCellIsSuperImmovable(a, c)))
.ToHashSet();
}
else if (check != BlockedByActor.None)
throw new System.ComponentModel.InvalidEnumArgumentException(
$"{nameof(HierarchicalPathFinder)} supports {nameof(BlockedByActor.None)} " +
$"and {nameof(BlockedByActor.Immovable)} only for {nameof(check)}");
}

public (
Expand Down Expand Up @@ -300,6 +330,12 @@ GridInfo BuildGrid(int gridX, int gridY, ICustomMovementLayer[] customMovementLa
{
var singleAbstractCellForLayer = new CPos?[customMovementLayers.Length];
var localCellToAbstractCell = new Dictionary<CPos, CPos>();

// When accounting for immovable actors, use a custom cost so those cells become invalid paths.
var customCost = cellsWithSuperImmovableActor == null
? (Func<CPos, int>)null
: c => cellsWithSuperImmovableActor.Contains(c) ? PathGraph.PathCostForInvalidPath : 0;

for (byte gridLayer = 0; gridLayer < customMovementLayers.Length; gridLayer++)
{
if (gridLayer != 0 &&
Expand All @@ -314,7 +350,7 @@ GridInfo BuildGrid(int gridX, int gridY, ICustomMovementLayer[] customMovementLa
for (var x = gridX; x < grid.BottomRight.X; x++)
{
var cell = new CPos(x, y, gridLayer);
if (locomotor.MovementCostForCell(cell) != PathGraph.MovementCostForUnreachableCell)
if (CellIsAccessible(cell))
accessibleCells.Add(cell);
}
}
Expand Down Expand Up @@ -364,7 +400,7 @@ CPos AbstractCellForLocalCells(List<CPos> cells, byte layer)
{
var src = accessibleCells.First();
using (var search = GetLocalPathSearch(
null, new[] { src }, src, null, null, BlockedByActor.None, false, grid, 100))
null, new[] { src }, src, customCost, null, BlockedByActor.None, false, grid, 100))
{
var localCellsInRegion = search.ExpandAll();
var abstractCell = AbstractCellForLocalCells(localCellsInRegion, gridLayer);
Expand Down Expand Up @@ -420,6 +456,24 @@ void BuildCostTable()
!customMovementLayers[gridLayer].EnabledForLocomotor(locomotor.Info)))
continue;

void AddEdgesIfMovementAllowedBetweenCells(CPos cell, CPos candidateCell)
{
if (!MovementAllowedBetweenCells(cell, candidateCell))
return;

var gridInfo = gridInfos[GridIndex(cell)];
var abstractCell = gridInfo.AbstractCellForLocalCell(cell);
if (abstractCell == null)
return;

var gridInfoAdjacent = gridInfos[GridIndex(candidateCell)];
var abstractCellAdjacent = gridInfoAdjacent.AbstractCellForLocalCell(candidateCell);
if (abstractCellAdjacent == null)
return;

abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value));
}

// Searches along edges of all grids within a layer.
// Checks for the local edge cell if we can traverse to any of the three adjacent cells in the next grid.
// Builds connections in the abstract graph when any local cells have connections.
Expand All @@ -432,28 +486,14 @@ void AddAbstractEdges(int xIncrement, int yIncrement, CVec adjacentVec, int2 off
for (var x = startX; x < startX + GridSize; x += xIncrement)
{
var cell = new CPos(x, y, gridLayer);
if (locomotor.MovementCostForCell(cell) == PathGraph.MovementCostForUnreachableCell)
if (!CellIsAccessible(cell))
continue;

var adjacentCell = cell + adjacentVec;
for (var i = -1; i <= 1; i++)
{
var candidateCell = adjacentCell + i * new CVec(adjacentVec.Y, adjacentVec.X);
if (locomotor.MovementCostToEnterCell(null, cell, candidateCell, BlockedByActor.None, null) !=
PathGraph.MovementCostForUnreachableCell)
{
var gridInfo = gridInfos[GridIndex(cell)];
var abstractCell = gridInfo.AbstractCellForLocalCell(cell);
if (abstractCell == null)
continue;

var gridInfoAdjacent = gridInfos[GridIndex(candidateCell)];
var abstractCellAdjacent = gridInfoAdjacent.AbstractCellForLocalCell(candidateCell);
if (abstractCellAdjacent == null)
continue;

abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value));
}
AddEdgesIfMovementAllowedBetweenCells(cell, candidateCell);
}
}
}
Expand All @@ -479,7 +519,7 @@ void AddAbstractCustomLayerEdges()
for (var x = gridX; x < gridX + GridSize; x++)
{
var cell = new CPos(x, y, gridLayer);
if (locomotor.MovementCostForCell(cell) == PathGraph.MovementCostForUnreachableCell)
if (!CellIsAccessible(cell))
continue;

CPos candidateCell;
Expand All @@ -496,21 +536,7 @@ void AddAbstractCustomLayerEdges()
continue;
}

if (locomotor.MovementCostToEnterCell(null, cell, candidateCell, BlockedByActor.None, null) ==
PathGraph.MovementCostForUnreachableCell)
continue;

var gridInfo = gridInfos[GridIndex(cell)];
var abstractCell = gridInfo.AbstractCellForLocalCell(cell);
if (abstractCell == null)
continue;

var gridInfoAdjacent = gridInfos[GridIndex(candidateCell)];
var abstractCellAdjacent = gridInfoAdjacent.AbstractCellForLocalCell(candidateCell);
if (abstractCellAdjacent == null)
continue;

abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value));
AddEdgesIfMovementAllowedBetweenCells(cell, candidateCell);
}
}
}
Expand Down Expand Up @@ -544,6 +570,89 @@ void RequireCostRefreshInCell(CPos cell, short oldCost, short newCost)
dirtyGridIndexes.Add(GridIndex(cell));
}

bool CellIsAccessible(CPos cell)
{
return locomotor.MovementCostForCell(cell) != PathGraph.MovementCostForUnreachableCell &&
(cellsWithSuperImmovableActor == null || !cellsWithSuperImmovableActor.Contains(cell));
}

bool MovementAllowedBetweenCells(CPos accessibleSrcCell, CPos destCell)
{
return locomotor.MovementCostToEnterCell(
null, accessibleSrcCell, destCell, BlockedByActor.None, null) != PathGraph.MovementCostForUnreachableCell &&
(cellsWithSuperImmovableActor == null || !cellsWithSuperImmovableActor.Contains(destCell));
}

/// <summary>
/// When actors change for a cell, marks the grid it belongs to as out of date.
/// </summary>
void RequireBlockingRefreshInCell(CPos cell)
{
var cellHasSuperImmovableActor = false;
var actors = actorMap.GetActorsAt(cell);
foreach (var actor in actors)
{
if (ActorIsSuperImmovable(actor) && ActorCellIsSuperImmovable(actor, cell))
{
cellHasSuperImmovableActor = true;
break;
}
}

if (cellHasSuperImmovableActor)
{
if (cellsWithSuperImmovableActor.Add(cell))
dirtyGridIndexes.Add(GridIndex(cell));
}
else
{
if (cellsWithSuperImmovableActor.Remove(cell))
dirtyGridIndexes.Add(GridIndex(cell));
}
}

/// <summary>
/// <see cref="BlockedByActor.Immovable"/> defines immovability based on the mobile trait. The blocking rules
/// in <see cref="Locomotor.CanMoveFreelyInto"/> allow units to pass even these immovable actors if they are
/// temporary blockers (e.g. gates) or crushable by the locomotor. Since our abstract graph must work for any
/// actor, we have to be conservative and can only consider "super-immovable" actors in the graph - ones we
/// know cannot be passed by some actors due to these rules.
/// Both this and <see cref="ActorCellIsSuperImmovable"/> must be true for a cell to be super-immovable.
///
/// This method is dependant on the logic in <see cref="Locomotor.CanMoveFreelyInto"/> and
/// <see cref="Locomotor.UpdateCellBlocking"/>. This method must be kept in sync with changes in the locomotor
/// rules.
/// </summary>
bool ActorIsSuperImmovable(Actor actor)
{
var mobile = actor.OccupiesSpace as Mobile;
var isMovable = mobile != null && !mobile.IsTraitDisabled && !mobile.IsTraitPaused && !mobile.IsImmovable;
if (isMovable)
return false;

var isTemporaryBlocker = world.RulesContainTemporaryBlocker && actor.TraitOrDefault<ITemporaryBlocker>() != null;
if (isTemporaryBlocker)
return false;

var crushables = actor.TraitsImplementing<ICrushable>();
foreach (var crushable in crushables)
if (world.NoPlayersMask != crushable.CrushableBy(actor, locomotor.Info.Crushes))
return false;

return true;
}

/// <summary>
/// The blocking rules additionally allow transit only cells of a <see cref="Building"/> to be considered
/// passable. Therefore we cannot consider these cells to be "super-immovable".
/// Both this and <see cref="ActorIsSuperImmovable"/> must be true for a cell to be super-immovable.
/// </summary>
bool ActorCellIsSuperImmovable(Actor actor, CPos cell)
{
var isTransitOnly = actor.OccupiesSpace is Building building && building.TransitOnlyCells().Contains(cell);
return !isTransitOnly;
}

int GridIndex(CPos cellInGrid)
{
return
Expand Down Expand Up @@ -741,7 +850,12 @@ static Grid GetGrid(CPos cellInGrid, Grid mapBounds)

/// <summary>
/// Determines if a path exists between source and target.
/// Only terrain is taken into account, i.e. as if <see cref="BlockedByActor.None"/> was given.
/// When <see cref="BlockedByActor.None"/> was given, only terrain is taken into account,
/// i.e. as if <see cref="BlockedByActor.None"/> was used when finding a path.
/// When <see cref="BlockedByActor.Immovable"/> was given, a subset of immovable actors are also taken into
/// account. If the method returns false, there is definitely no path. If it returns true there could be a
/// path, but it is possible that there is no path because of an immovable actor that does not belong to the
/// subset of actors that can be accounted for. So be careful.
/// This would apply for any actor using the same <see cref="Locomotor"/> as this <see cref="HierarchicalPathFinder"/>.
/// </summary>
public bool PathExists(CPos source, CPos target)
Expand Down
12 changes: 12 additions & 0 deletions OpenRA.Mods.Common/Traits/World/ActorMap.cs
Expand Up @@ -403,6 +403,18 @@ public bool AnyActorsAt(CPos a, SubCell sub, Func<Actor, bool> withCondition)
return AnyActorsAt(uv, layer, sub, withCondition);
}

public IEnumerable<Actor> AllActors()
{
foreach (var layer in influence)
{
if (layer == null)
continue;
foreach (var node in layer)
for (var i = node; i != null; i = i.Next)
yield return i.Actor;
}
}

public void AddInfluence(Actor self, IOccupySpace ios)
{
foreach (var c in ios.OccupiedCells())
Expand Down
Expand Up @@ -48,6 +48,11 @@ public class HierarchicalPathFinderOverlay : IRenderAnnotations, IWorldLoaded, I
/// </summary>
public Locomotor Locomotor { get; set; }

/// <summary>
/// The blocking check selected in the UI which the overlay will display.
/// </summary>
public BlockedByActor Check { get; set; } = BlockedByActor.Immovable;

public HierarchicalPathFinderOverlay(HierarchicalPathFinderOverlayInfo info)
{
this.info = info;
Expand Down Expand Up @@ -88,7 +93,7 @@ IEnumerable<IRenderable> IRenderAnnotations.RenderAnnotations(Actor self, WorldR
: new[] { Locomotor };
foreach (var locomotor in locomotors)
{
var (abstractGraph, abstractDomains) = pathFinder.GetOverlayDataForLocomotor(locomotor);
var (abstractGraph, abstractDomains) = pathFinder.GetOverlayDataForLocomotor(locomotor, Check);

// Locomotor doesn't allow movement, nothing to display.
if (abstractGraph == null || abstractDomains == null)
Expand Down
6 changes: 6 additions & 0 deletions OpenRA.Mods.Common/Traits/World/Locomotor.cs
Expand Up @@ -311,6 +311,9 @@ public SubCell GetAvailableSubCell(Actor self, CPos cell, BlockedByActor check,
return world.ActorMap.FreeSubCell(cell, preferredSubCell);
}

/// <remarks>This logic is replicated in <see cref="HierarchicalPathFinder.ActorIsSuperImmovable"/> and
/// <see cref="HierarchicalPathFinder.ActorCellIsSuperImmovable"/>. If this method is updated please update
/// those as well.</remarks>
bool IsBlockedBy(Actor actor, Actor otherActor, Actor ignoreActor, CPos cell, BlockedByActor check, CellFlag cellFlag)
{
if (otherActor == ignoreActor)
Expand Down Expand Up @@ -450,6 +453,9 @@ void UpdateCellCost(CPos cell)
}
}

/// <remarks>This logic is replicated in <see cref="HierarchicalPathFinder.ActorIsSuperImmovable"/> and
/// <see cref="HierarchicalPathFinder.ActorCellIsSuperImmovable"/>. If this method is updated please update
/// those as well.</remarks>
void UpdateCellBlocking(CPos cell)
{
using (new PerfSample("locomotor_cache"))
Expand Down

0 comments on commit 8f9abbc

Please sign in to comment.