The path cache was originally a moderate benefit, but over time a couple of things have conspired against it: - Only paths with BlockedByActor.None are cached. Originally all paths regardless of blocking were cached but this was deemed unacceptable due to potentially returning outdated paths as actors move about. Paths with BlockedByActor.None are only invalidated if terrain conditions change, which are rarer. - Move will try and find a path 4 times, trying with a different BlockedByActor check each time. BlockedByActor.None is the last check and only reached if the other searches fail. This is a rare scenario. Overall, this means the hit rate for the cache is almost non-existent. Given the constraints on path validity it seems unlikely that the hit rate could be improved significantly, therefore it seems reasonable to remove the cache entirely to remove the overhead of cache management.
241 lines
7.0 KiB
C#
241 lines
7.0 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2021 The OpenRA Developers (see AUTHORS)
|
|
* 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.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Mods.Common.Pathfinder;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
[TraitLocation(SystemActors.World)]
|
|
[Desc("Calculates routes for mobile units based on the A* search algorithm.", " Attach this to the world actor.")]
|
|
public class PathFinderInfo : TraitInfo, Requires<LocomotorInfo>
|
|
{
|
|
public override object Create(ActorInitializer init)
|
|
{
|
|
return new PathFinder(init.World);
|
|
}
|
|
}
|
|
|
|
public interface IPathFinder
|
|
{
|
|
/// <summary>
|
|
/// Calculates a path for the actor from source to destination
|
|
/// </summary>
|
|
/// <returns>A path from start to target</returns>
|
|
List<CPos> FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, BlockedByActor check);
|
|
|
|
List<CPos> FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WDist range, Actor self, BlockedByActor check);
|
|
|
|
/// <summary>
|
|
/// Calculates a path given a search specification
|
|
/// </summary>
|
|
List<CPos> FindPath(IPathSearch search);
|
|
|
|
/// <summary>
|
|
/// Calculates a path given two search specifications, and
|
|
/// then returns a path when both search intersect each other
|
|
/// TODO: This should eventually disappear
|
|
/// </summary>
|
|
List<CPos> FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest);
|
|
}
|
|
|
|
public class PathFinder : IPathFinder
|
|
{
|
|
static readonly List<CPos> EmptyPath = new List<CPos>(0);
|
|
readonly World world;
|
|
DomainIndex domainIndex;
|
|
bool cached;
|
|
|
|
public PathFinder(World world)
|
|
{
|
|
this.world = world;
|
|
}
|
|
|
|
public List<CPos> FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor, BlockedByActor check)
|
|
{
|
|
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
|
|
var locomotor = ((Mobile)self.OccupiesSpace).Locomotor;
|
|
|
|
if (!cached)
|
|
{
|
|
domainIndex = world.WorldActor.TraitOrDefault<DomainIndex>();
|
|
cached = true;
|
|
}
|
|
|
|
// If a water-land transition is required, bail early
|
|
if (domainIndex != null && !domainIndex.IsPassable(source, target, locomotor))
|
|
return EmptyPath;
|
|
|
|
var distance = source - target;
|
|
var canMoveFreely = locomotor.CanMoveFreelyInto(self, target, check, null);
|
|
if (distance.LengthSquared < 3 && !canMoveFreely)
|
|
return new List<CPos> { };
|
|
|
|
if (source.Layer == target.Layer && distance.LengthSquared < 3 && canMoveFreely)
|
|
return new List<CPos> { target };
|
|
|
|
List<CPos> pb;
|
|
|
|
using (var fromSrc = PathSearch.FromPoint(world, locomotor, self, target, source, check).WithIgnoredActor(ignoreActor))
|
|
using (var fromDest = PathSearch.FromPoint(world, locomotor, self, source, target, check).WithIgnoredActor(ignoreActor).Reverse())
|
|
pb = FindBidiPath(fromSrc, fromDest);
|
|
|
|
return pb;
|
|
}
|
|
|
|
public List<CPos> FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WDist range, Actor self, BlockedByActor check)
|
|
{
|
|
if (!cached)
|
|
{
|
|
domainIndex = world.WorldActor.TraitOrDefault<DomainIndex>();
|
|
cached = true;
|
|
}
|
|
|
|
// PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait.
|
|
var mobile = (Mobile)self.OccupiesSpace;
|
|
var locomotor = mobile.Locomotor;
|
|
|
|
var targetCell = world.Map.CellContaining(target);
|
|
|
|
// Correct for SubCell offset
|
|
target -= world.Map.Grid.OffsetOfSubCell(srcSub);
|
|
|
|
var rangeLengthSquared = range.LengthSquared;
|
|
var map = world.Map;
|
|
|
|
// Select only the tiles that are within range from the requested SubCell
|
|
// This assumes that the SubCell does not change during the path traversal
|
|
var tilesInRange = map.FindTilesInCircle(targetCell, range.Length / 1024 + 1)
|
|
.Where(t => (map.CenterOfCell(t) - target).LengthSquared <= rangeLengthSquared
|
|
&& mobile.Info.CanEnterCell(world, self, t));
|
|
|
|
// See if there is any cell within range that does not involve a cross-domain request
|
|
// Really, we only need to check the circle perimeter, but it's not clear that would be a performance win
|
|
if (domainIndex != null)
|
|
{
|
|
tilesInRange = new List<CPos>(tilesInRange.Where(t => domainIndex.IsPassable(source, t, locomotor)));
|
|
if (!tilesInRange.Any())
|
|
return EmptyPath;
|
|
}
|
|
|
|
using (var fromSrc = PathSearch.FromPoints(world, locomotor, self, tilesInRange, source, check))
|
|
using (var fromDest = PathSearch.FromPoint(world, locomotor, self, source, targetCell, check).Reverse())
|
|
return FindBidiPath(fromSrc, fromDest);
|
|
}
|
|
|
|
public List<CPos> FindPath(IPathSearch search)
|
|
{
|
|
List<CPos> path = null;
|
|
|
|
while (search.CanExpand)
|
|
{
|
|
var p = search.Expand();
|
|
if (search.IsTarget(p))
|
|
{
|
|
path = MakePath(search.Graph, p);
|
|
break;
|
|
}
|
|
}
|
|
|
|
search.Graph.Dispose();
|
|
|
|
if (path != null)
|
|
return path;
|
|
|
|
// no path exists
|
|
return EmptyPath;
|
|
}
|
|
|
|
// Searches from both ends toward each other. This is used to prevent blockings in case we find
|
|
// units in the middle of the path that prevent us to continue.
|
|
public List<CPos> FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest)
|
|
{
|
|
List<CPos> path = null;
|
|
|
|
while (fromSrc.CanExpand && fromDest.CanExpand)
|
|
{
|
|
// make some progress on the first search
|
|
var p = fromSrc.Expand();
|
|
if (fromDest.Graph[p].Status == CellStatus.Closed &&
|
|
fromDest.Graph[p].CostSoFar != PathGraph.PathCostForInvalidPath)
|
|
{
|
|
path = MakeBidiPath(fromSrc, fromDest, p);
|
|
break;
|
|
}
|
|
|
|
// make some progress on the second search
|
|
var q = fromDest.Expand();
|
|
if (fromSrc.Graph[q].Status == CellStatus.Closed &&
|
|
fromSrc.Graph[q].CostSoFar != PathGraph.PathCostForInvalidPath)
|
|
{
|
|
path = MakeBidiPath(fromSrc, fromDest, q);
|
|
break;
|
|
}
|
|
}
|
|
|
|
fromSrc.Graph.Dispose();
|
|
fromDest.Graph.Dispose();
|
|
|
|
if (path != null)
|
|
return path;
|
|
|
|
return EmptyPath;
|
|
}
|
|
|
|
// Build the path from the destination. When we find a node that has the same previous
|
|
// position than itself, that node is the source node.
|
|
static List<CPos> MakePath(IGraph<CellInfo> cellInfo, CPos destination)
|
|
{
|
|
var ret = new List<CPos>();
|
|
var currentNode = destination;
|
|
|
|
while (cellInfo[currentNode].PreviousNode != currentNode)
|
|
{
|
|
ret.Add(currentNode);
|
|
currentNode = cellInfo[currentNode].PreviousNode;
|
|
}
|
|
|
|
ret.Add(currentNode);
|
|
return ret;
|
|
}
|
|
|
|
static List<CPos> MakeBidiPath(IPathSearch a, IPathSearch b, CPos confluenceNode)
|
|
{
|
|
var ca = a.Graph;
|
|
var cb = b.Graph;
|
|
|
|
var ret = new List<CPos>();
|
|
|
|
var q = confluenceNode;
|
|
while (ca[q].PreviousNode != q)
|
|
{
|
|
ret.Add(q);
|
|
q = ca[q].PreviousNode;
|
|
}
|
|
|
|
ret.Add(q);
|
|
|
|
ret.Reverse();
|
|
|
|
q = confluenceNode;
|
|
while (cb[q].PreviousNode != q)
|
|
{
|
|
q = cb[q].PreviousNode;
|
|
ret.Add(q);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
}
|
|
}
|