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 that always block. 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 this subset of 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 31, 2022
1 parent 44e9310 commit fced55c
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 68 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
223 changes: 178 additions & 45 deletions OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs

Large diffs are not rendered by default.

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.ActorIsBlocking"/> and
/// <see cref="HierarchicalPathFinder.ActorCellIsBlocking"/>. 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.ActorIsBlocking"/> and
/// <see cref="HierarchicalPathFinder.ActorCellIsBlocking"/>. If this method is updated please update those as
/// well.</remarks>
void UpdateCellBlocking(CPos cell)
{
using (new PerfSample("locomotor_cache"))
Expand Down
36 changes: 26 additions & 10 deletions OpenRA.Mods.Common/Traits/World/PathFinder.cs
Expand Up @@ -19,8 +19,8 @@
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.World)]
[Desc("Calculates routes for mobile units with locomotors based on the A* search algorithm.", " Attach this to the world actor.")]
public class PathFinderInfo : TraitInfo, Requires<LocomotorInfo>
[Desc("Calculates routes for mobile actors with locomotors based on the A* search algorithm.", " Attach this to the world actor.")]
public class PathFinderInfo : TraitInfo, Requires<LocomotorInfo>, Requires<ActorMapInfo>
{
public override object Create(ActorInitializer init)
{
Expand All @@ -40,7 +40,8 @@ public class PathFinder : IPathFinder, IWorldLoaded

readonly World world;
PathFinderOverlay pathFinderOverlay;
Dictionary<Locomotor, HierarchicalPathFinder> hierarchicalPathFindersByLocomotor;
Dictionary<Locomotor, HierarchicalPathFinder> hierarchicalPathFindersBlockedByNoneByLocomotor;
Dictionary<Locomotor, HierarchicalPathFinder> hierarchicalPathFindersBlockedByImmovableByLocomotor;

public PathFinder(Actor self)
{
Expand All @@ -49,19 +50,24 @@ public PathFinder(Actor self)

public (
IReadOnlyDictionary<CPos, List<GraphConnection>> AbstractGraph,
IReadOnlyDictionary<CPos, uint> AbstractDomains) GetOverlayDataForLocomotor(Locomotor locomotor)
IReadOnlyDictionary<CPos, uint> AbstractDomains) GetOverlayDataForLocomotor(
Locomotor locomotor, BlockedByActor check)
{
return hierarchicalPathFindersByLocomotor[locomotor].GetOverlayData();
return GetHierarchicalPathFinder(locomotor, check, null).GetOverlayData();
}

public void WorldLoaded(World w, WorldRenderer wr)
{
pathFinderOverlay = world.WorldActor.TraitOrDefault<PathFinderOverlay>();

// Requires<LocomotorInfo> ensures all Locomotors have been initialized.
hierarchicalPathFindersByLocomotor = w.WorldActor.TraitsImplementing<Locomotor>().ToDictionary(
var locomotors = w.WorldActor.TraitsImplementing<Locomotor>().ToList();
hierarchicalPathFindersBlockedByNoneByLocomotor = locomotors.ToDictionary(
locomotor => locomotor,
locomotor => new HierarchicalPathFinder(world, locomotor));
locomotor => new HierarchicalPathFinder(world, locomotor, w.ActorMap, BlockedByActor.None));
hierarchicalPathFindersBlockedByImmovableByLocomotor = locomotors.ToDictionary(
locomotor => locomotor,
locomotor => new HierarchicalPathFinder(world, locomotor, w.ActorMap, BlockedByActor.Immovable));
}

/// <summary>
Expand Down Expand Up @@ -110,15 +116,25 @@ public void WorldLoaded(World w, WorldRenderer wr)
}

// Use a hierarchical path search, which performs a guided bidirectional search.
return hierarchicalPathFindersByLocomotor[locomotor].FindPath(
return GetHierarchicalPathFinder(locomotor, check, ignoreActor).FindPath(
self, source, target, check, DefaultHeuristicWeightPercentage, customCost, ignoreActor, laneBias, pathFinderOverlay);
}

// Use a hierarchical path search, which performs a guided unidirectional search.
return hierarchicalPathFindersByLocomotor[locomotor].FindPath(
return GetHierarchicalPathFinder(locomotor, check, ignoreActor).FindPath(
self, sourcesList, target, check, DefaultHeuristicWeightPercentage, customCost, ignoreActor, laneBias, pathFinderOverlay);
}

HierarchicalPathFinder GetHierarchicalPathFinder(Locomotor locomotor, BlockedByActor check, Actor ignoreActor)
{
// If there is an actor to ignore, we cannot use an HPF that accounts for any blocking actors.
// One of the blocking actors might be the one we need to ignore!
var hpfs = check == BlockedByActor.None || ignoreActor != null
? hierarchicalPathFindersBlockedByNoneByLocomotor
: hierarchicalPathFindersBlockedByImmovableByLocomotor;
return hpfs[locomotor];
}

/// <summary>
/// Calculates a path for the actor from multiple possible sources, whilst searching for an acceptable target.
/// Returned path is *reversed* and given target to source.
Expand Down Expand Up @@ -149,7 +165,7 @@ public void WorldLoaded(World w, WorldRenderer wr)
/// </summary>
public bool PathExistsForLocomotor(Locomotor locomotor, CPos source, CPos target)
{
return hierarchicalPathFindersByLocomotor[locomotor].PathExists(source, target);
return hierarchicalPathFindersBlockedByNoneByLocomotor[locomotor].PathExists(source, target);
}

static Locomotor GetActorLocomotor(Actor self)
Expand Down
Expand Up @@ -42,6 +42,23 @@ public HierarchicalPathFinderOverlayLogic(Widget widget, World world)
locomotorSelector.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", locomotors.Length * 30, locomotors, setupItem);
};

var checks = new[] { BlockedByActor.None, BlockedByActor.Immovable };
var checkSelector = widget.Get<DropDownButtonWidget>("HPF_OVERLAY_CHECK");
checkSelector.OnMouseDown = _ =>
{
Func<BlockedByActor, ScrollItemWidget, ScrollItemWidget> setupItem = (option, template) =>
{
var item = ScrollItemWidget.Setup(
template,
() => hpfOverlay.Check == option,
() => hpfOverlay.Check = option);
item.Get<LabelWidget>("LABEL").GetText = () => option.ToString();
return item;
};
checkSelector.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", checks.Length * 30, checks, setupItem);
};
}
}
}
13 changes: 10 additions & 3 deletions mods/cnc/chrome/ingame.yaml
Expand Up @@ -1864,14 +1864,21 @@ Container@PLAYER_WIDGETS:
Logic: HierarchicalPathFinderOverlayLogic
X: WINDOW_RIGHT - WIDTH - 240
Y: 40
Width: 150
Height: 25
Width: 175
Height: 60
Children:
DropDownButton@HPF_OVERLAY_LOCOMOTOR:
Y: PARENT_TOP
Width: PARENT_RIGHT
Height: PARENT_BOTTOM
Height: 25
Text: Select Locomotor
Font: Regular
DropDownButton@HPF_OVERLAY_CHECK:
Y: PARENT_TOP + 35
Width: PARENT_RIGHT
Height: 25
Text: Select BlockedByActor
Font: Regular

Background@FMVPLAYER:
Width: WINDOW_RIGHT
Expand Down
13 changes: 10 additions & 3 deletions mods/d2k/chrome/ingame-player.yaml
Expand Up @@ -645,11 +645,18 @@ Container@PLAYER_WIDGETS:
Logic: HierarchicalPathFinderOverlayLogic
X: WINDOW_RIGHT - WIDTH - 231
Y: 40
Width: 150
Height: 25
Width: 175
Height: 60
Children:
DropDownButton@HPF_OVERLAY_LOCOMOTOR:
Y: PARENT_TOP
Width: PARENT_RIGHT
Height: PARENT_BOTTOM
Height: 25
Text: Select Locomotor
Font: Regular
DropDownButton@HPF_OVERLAY_CHECK:
Y: PARENT_TOP + 35
Width: PARENT_RIGHT
Height: 25
Text: Select BlockedByActor
Font: Regular
13 changes: 10 additions & 3 deletions mods/ra/chrome/ingame-player.yaml
Expand Up @@ -653,12 +653,19 @@ Container@PLAYER_WIDGETS:
Logic: HierarchicalPathFinderOverlayLogic
X: WINDOW_RIGHT - WIDTH - 260
Y: 40
Width: 150
Height: 25
Width: 175
Height: 60
Children:
DropDownButton@HPF_OVERLAY_LOCOMOTOR:
Y: PARENT_TOP
Width: PARENT_RIGHT
Height: PARENT_BOTTOM
Height: 25
Text: Select Locomotor
Font: Regular
DropDownButton@HPF_OVERLAY_CHECK:
Y: PARENT_TOP + 35
Width: PARENT_RIGHT
Height: 25
Text: Select BlockedByActor
Font: Regular

13 changes: 10 additions & 3 deletions mods/ts/chrome/ingame-player.yaml
Expand Up @@ -623,11 +623,18 @@ Container@PLAYER_WIDGETS:
Logic: HierarchicalPathFinderOverlayLogic
X: WINDOW_RIGHT - WIDTH - 245
Y: 40
Width: 150
Height: 25
Width: 175
Height: 60
Children:
DropDownButton@HPF_OVERLAY_LOCOMOTOR:
Y: PARENT_TOP
Width: PARENT_RIGHT
Height: PARENT_BOTTOM
Height: 25
Text: Select Locomotor
Font: Regular
DropDownButton@HPF_OVERLAY_CHECK:
Y: PARENT_TOP + 35
Width: PARENT_RIGHT
Height: 25
Text: Select BlockedByActor
Font: Regular

0 comments on commit fced55c

Please sign in to comment.