When this hits the case that "As both ends are accessible, we can freely swap them." - we must note that we are reversing the path search and pass the information into the HierarchicalPathFinder. When a normal path search occurs, the actor trying to pathfind will never check its own location - and thus never gets blocked by itself. When a search is reversed, the search will check the actors location. If we inform the search it is doing done in reverse, it will special case this scenario and avoid the actor blocking itself. But if it is not told about this scenario, then this special case is not applied and no path will be found when in fact a path is possible.
288 lines
13 KiB
C#
288 lines
13 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Graphics;
|
|
using OpenRA.Mods.Common.Pathfinder;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
[TraitLocation(SystemActors.World)]
|
|
[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)
|
|
{
|
|
return new PathFinder(init.Self);
|
|
}
|
|
}
|
|
|
|
public class PathFinder : IPathFinder, IWorldLoaded
|
|
{
|
|
public static readonly List<CPos> NoPath = new(0);
|
|
|
|
/// <summary>
|
|
/// When searching for paths, use a default weight of 125% to reduce
|
|
/// computation effort - even if this means paths may be sub-optimal.
|
|
/// </summary>
|
|
const int DefaultHeuristicWeightPercentage = 125;
|
|
|
|
readonly World world;
|
|
PathFinderOverlay pathFinderOverlay;
|
|
Dictionary<Locomotor, HierarchicalPathFinder> hierarchicalPathFindersBlockedByNoneByLocomotor;
|
|
Dictionary<Locomotor, HierarchicalPathFinder> hierarchicalPathFindersBlockedByImmovableByLocomotor;
|
|
|
|
public PathFinder(Actor self)
|
|
{
|
|
world = self.World;
|
|
}
|
|
|
|
public (
|
|
IReadOnlyDictionary<CPos, List<GraphConnection>> AbstractGraph,
|
|
IReadOnlyDictionary<CPos, uint> AbstractDomains) GetOverlayDataForLocomotor(
|
|
Locomotor locomotor, BlockedByActor check)
|
|
{
|
|
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.
|
|
var locomotors = w.WorldActor.TraitsImplementing<Locomotor>().ToList();
|
|
hierarchicalPathFindersBlockedByNoneByLocomotor = locomotors.ToDictionary(
|
|
locomotor => 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>
|
|
/// Calculates a path for the actor from multiple possible sources to target.
|
|
/// Returned path is *reversed* and given target to source.
|
|
/// The shortest path between a source and the target is returned.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// It is allowed for an actor to occupy an inaccessible space and move out of it if another adjacent cell is
|
|
/// accessible, but it is not allowed to move into an inaccessible target space. Therefore it is vitally
|
|
/// important to not mix up the source and target locations. A path can exist from an inaccessible source space
|
|
/// to an accessible target space, but if those parameters as swapped then no path can exist.
|
|
/// </para>
|
|
/// <para>
|
|
/// Searches that provide multiple source cells are slower than those than provide only a single source cell,
|
|
/// as optimizations are possible for the single source case. Use searches from multiple source cells
|
|
/// sparingly.
|
|
/// </para>
|
|
/// </remarks>
|
|
public List<CPos> FindPathToTargetCell(
|
|
Actor self, IEnumerable<CPos> sources, CPos target, BlockedByActor check,
|
|
Func<CPos, int> customCost = null,
|
|
Actor ignoreActor = null,
|
|
bool laneBias = true)
|
|
{
|
|
return FindPathToTarget(self, sources.ToList(), target, check, customCost, ignoreActor, false, laneBias);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates a path for the actor from source to multiple possible targets.
|
|
/// Returned path is *reversed* and given target to source.
|
|
/// The shortest path between the source and a target is returned.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// It is allowed for an actor to occupy an inaccessible space and move out of it if another adjacent cell is
|
|
/// accessible, but it is not allowed to move into an inaccessible target space. Therefore it is vitally
|
|
/// important to not mix up the source and target locations. A path can exist from an inaccessible source space
|
|
/// to an accessible target space, but if those parameters as swapped then no path can exist.
|
|
/// </para>
|
|
/// <para>
|
|
/// Searches that provide multiple target cells are slower than those than provide only a single target cell,
|
|
/// as optimizations are possible for the single target case. Use searches to multiple target cells
|
|
/// sparingly.
|
|
/// </para>
|
|
/// </remarks>
|
|
public List<CPos> FindPathToTargetCells(
|
|
Actor self, CPos source, IEnumerable<CPos> targets, BlockedByActor check,
|
|
Func<CPos, int> customCost = null,
|
|
Actor ignoreActor = null,
|
|
bool laneBias = true)
|
|
{
|
|
// We can reuse existing search infrastructure by swapping the single source and multiple targets,
|
|
// and calling the existing methods that allow multiple sources and one target.
|
|
// However there is a case of asymmetry we must handle, an actor may move out of a inaccessible source,
|
|
// but may not move onto a inaccessible target. We must account for this when performing the swap.
|
|
var targetsList = targets.ToList();
|
|
if (targetsList.Count == 0)
|
|
return NoPath;
|
|
|
|
// As targets must be accessible, determine accessible targets in advance so when they becomes the sources
|
|
// we don't accidentally allow an inaccessible position to become viable.
|
|
var locomotor = GetActorLocomotor(self);
|
|
var accessibleTargets = targetsList
|
|
.Where(target =>
|
|
PathSearch.CellAllowsMovement(self.World, locomotor, target, customCost)
|
|
&& locomotor.MovementCostToEnterCell(self, target, check, ignoreActor, true) != PathGraph.MovementCostForUnreachableCell)
|
|
.ToList();
|
|
if (accessibleTargets.Count == 0)
|
|
return NoPath;
|
|
|
|
// When checking if the source location is accessible, we must also ignore self.
|
|
// So that when it becomes a target, we don't consider the location blocked by ourselves!
|
|
List<CPos> path;
|
|
var sourceIsAccessible =
|
|
PathSearch.CellAllowsMovement(self.World, locomotor, source, customCost)
|
|
&& locomotor.MovementCostToEnterCell(self, source, check, ignoreActor, true) != PathGraph.MovementCostForUnreachableCell;
|
|
if (sourceIsAccessible)
|
|
{
|
|
// As both ends are accessible, we can freely swap them.
|
|
path = FindPathToTarget(self, targetsList, source, check, customCost, ignoreActor, true, laneBias);
|
|
}
|
|
else
|
|
{
|
|
// When we treat the source as a target, we need to be able to path to it.
|
|
// We know this would fail as it is inaccessible but we need an exception to be made.
|
|
// A hierarchical path search doesn't support this ability,
|
|
// but the local pathfinder can deal with it when doing reverse searches.
|
|
pathFinderOverlay?.NewRecording(self, accessibleTargets, source);
|
|
using (var search = PathSearch.ToTargetCell(
|
|
world, locomotor, self, accessibleTargets, source, check, DefaultHeuristicWeightPercentage,
|
|
customCost, ignoreActor, laneBias, inReverse: true, recorder: pathFinderOverlay?.RecordLocalEdges(self)))
|
|
path = search.FindPath();
|
|
}
|
|
|
|
// Since we swapped the positions, we need to reverse the path to swap it back.
|
|
path.Reverse();
|
|
return path;
|
|
}
|
|
|
|
List<CPos> FindPathToTarget(
|
|
Actor self, List<CPos> sources, CPos target, BlockedByActor check,
|
|
Func<CPos, int> customCost, Actor ignoreActor, bool inReverse, bool laneBias)
|
|
{
|
|
if (sources.Count == 0)
|
|
return NoPath;
|
|
|
|
var locomotor = GetActorLocomotor(self);
|
|
|
|
// If the target cell is inaccessible, bail early.
|
|
// The destination cell must allow movement and also have a reachable movement cost.
|
|
if (!PathSearch.CellAllowsMovement(self.World, locomotor, target, customCost)
|
|
|| locomotor.MovementCostToEnterCell(self, target, check, ignoreActor, inReverse) == PathGraph.MovementCostForUnreachableCell)
|
|
return NoPath;
|
|
|
|
// When searching from only one source cell, some optimizations are possible.
|
|
if (sources.Count == 1)
|
|
{
|
|
var source = sources[0];
|
|
|
|
// For adjacent cells on the same layer, we can return the path without invoking a full search.
|
|
if (source.Layer == target.Layer && (source - target).LengthSquared < 3)
|
|
{
|
|
// If the source cell is inaccessible, there is no path.
|
|
// Unlike the destination cell, the source cell is allowed to have an unreachable movement cost.
|
|
if (!PathSearch.CellAllowsMovement(self.World, locomotor, source, customCost))
|
|
return NoPath;
|
|
return new List<CPos>(2) { target, source };
|
|
}
|
|
|
|
// Use a hierarchical path search, which performs a guided bidirectional search.
|
|
return GetHierarchicalPathFinder(locomotor, check, ignoreActor).FindPath(
|
|
self, source, target, check, DefaultHeuristicWeightPercentage, customCost, ignoreActor, inReverse, laneBias, pathFinderOverlay);
|
|
}
|
|
|
|
// Use a hierarchical path search, which performs a guided unidirectional search.
|
|
return GetHierarchicalPathFinder(locomotor, check, ignoreActor).FindPath(
|
|
self, sources, target, check, DefaultHeuristicWeightPercentage, customCost, ignoreActor, inReverse, 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.
|
|
/// The shortest path between a source and a discovered target is returned.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Searches with this method are slower than <see cref="FindPathToTargetCell"/> due to the need to search for
|
|
/// and discover an acceptable target cell. Use this search sparingly.
|
|
/// </remarks>
|
|
public List<CPos> FindPathToTargetCellByPredicate(
|
|
Actor self, IEnumerable<CPos> sources, Func<CPos, bool> targetPredicate, BlockedByActor check,
|
|
Func<CPos, int> customCost = null,
|
|
Actor ignoreActor = null,
|
|
bool laneBias = true)
|
|
{
|
|
pathFinderOverlay?.NewRecording(self, sources, null);
|
|
|
|
// With no pre-specified target location, we can only use a unidirectional search.
|
|
using (var search = PathSearch.ToTargetCellByPredicate(
|
|
world, GetActorLocomotor(self), self, sources, targetPredicate, check, customCost, ignoreActor, laneBias, pathFinderOverlay?.RecordLocalEdges(self)))
|
|
return search.FindPath();
|
|
}
|
|
|
|
/// <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.
|
|
/// This would apply for any actor using the given <see cref="Locomotor"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// It is allowed for an actor to occupy an inaccessible space and move out of it if another adjacent cell is
|
|
/// accessible, but it is not allowed to move into an inaccessible target space. Therefore it is vitally
|
|
/// important to not mix up the source and target locations. A path can exist from an inaccessible source space
|
|
/// to an accessible target space, but if those parameters as swapped then no path can exist.
|
|
/// </remarks>
|
|
public bool PathExistsForLocomotor(Locomotor locomotor, CPos source, CPos target)
|
|
{
|
|
return hierarchicalPathFindersBlockedByNoneByLocomotor[locomotor].PathExists(source, target);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a path exists between source and target.
|
|
/// Terrain and a *subset* of immovable actors are taken into account,
|
|
/// i.e. as if a subset of <see cref="BlockedByActor.Immovable"/> was given.
|
|
/// This would apply for any actor using the given <see cref="Locomotor"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// It is allowed for an actor to occupy an inaccessible space and move out of it if another adjacent cell is
|
|
/// accessible, but it is not allowed to move into an inaccessible target space. Therefore it is vitally
|
|
/// important to not mix up the source and target locations. A path can exist from an inaccessible source space
|
|
/// to an accessible target space, but if those parameters as swapped then no path can exist.
|
|
/// As only a subset of immovable actors are taken into account,
|
|
/// this method can return false positives, indicating a path might exist where none is possible.
|
|
/// </remarks>
|
|
public bool PathMightExistForLocomotorBlockedByImmovable(Locomotor locomotor, CPos source, CPos target)
|
|
{
|
|
return hierarchicalPathFindersBlockedByImmovableByLocomotor[locomotor].PathExists(source, target);
|
|
}
|
|
|
|
static Locomotor GetActorLocomotor(Actor self)
|
|
{
|
|
// PERF: This PathFinder trait requires the use of Mobile, so we can be sure that is in use.
|
|
// We can save some performance by avoiding querying for the Locomotor trait and retrieving it from Mobile.
|
|
return ((Mobile)self.OccupiesSpace).Locomotor;
|
|
}
|
|
}
|
|
}
|