- Introduced Unit Testing capabilities to the PathFinder trait and algorithm.
Introduced also a small Unit test project to prove it. - Separated caching capabilities from PathFinder class to increase cohesion and maintainability. Refactored the pathfinding algorithm by extracting methods based on responsibilities like calculating costs and reordering functions. These changes should provide a in average a small increase in pathfinding performance and maintainability. - Optimized the pathfinder algorithm to reuse calculations like the MovementCost and heuristics. - Introduced base classes, IPathSearch and IPriorityQueue interfaces, and restructured code to ease readability and testability - Renamed the PathFinder related classes to more appropriate names. Made the traits rely on the interface IPathfinder instead of concrete PathFinder implementation. - Massive performance improvements - Solved error with harvesters' Heuristic - Updated the heuristic to ease redability and adjustability. D can be adjusted to offer best paths by decreasing and more performance by increasing it - Refactored the CellLayer<CellInfo> creation in its own Singleton class - Extracted the graph abstraction onto an IGraph interface, making the Pathfinder agnostic to the definition of world and terrain. This abstraction can help in the future to be able to cache graphs for similar classes and their costs, speeding up the pathfinder and being able to feed the A* algorithm with different types of graphs like Hierarchical graphs
This commit is contained in:
@@ -12,6 +12,7 @@ using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using OpenRA.Activities;
|
||||
using OpenRA.Mods.Common.Pathfinder;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Traits;
|
||||
|
||||
@@ -51,31 +52,34 @@ namespace OpenRA.Mods.Common.Activities
|
||||
var searchRadiusSquared = searchRadius * searchRadius;
|
||||
|
||||
// Find harvestable resources nearby:
|
||||
var path = self.World.WorldActor.Trait<PathFinder>().FindPath(
|
||||
var path = self.World.WorldActor.Trait<IPathFinder>().FindPath(
|
||||
PathSearch.Search(self.World, mobileInfo, self, true)
|
||||
.WithHeuristic(loc =>
|
||||
{
|
||||
// Avoid this cell:
|
||||
if (avoidCell.HasValue && loc == avoidCell.Value) return 1;
|
||||
if (avoidCell.HasValue && loc == avoidCell.Value)
|
||||
return Constants.CellCost;
|
||||
|
||||
// Don't harvest out of range:
|
||||
var distSquared = (loc - searchFromLoc).LengthSquared;
|
||||
if (distSquared > searchRadiusSquared)
|
||||
return int.MaxValue;
|
||||
return Constants.CellCost * 2;
|
||||
|
||||
// Get the resource at this location:
|
||||
var resType = resLayer.GetResource(loc);
|
||||
|
||||
if (resType == null) return 1;
|
||||
if (resType == null)
|
||||
return Constants.CellCost;
|
||||
|
||||
// Can the harvester collect this kind of resource?
|
||||
if (!harvInfo.Resources.Contains(resType.Info.Name)) return 1;
|
||||
if (!harvInfo.Resources.Contains(resType.Info.Name))
|
||||
return Constants.CellCost;
|
||||
|
||||
if (territory != null)
|
||||
{
|
||||
// Another harvester has claimed this resource:
|
||||
ResourceClaim claim;
|
||||
if (territory.IsClaimedByAnyoneElse(self, loc, out claim)) return 1;
|
||||
if (territory.IsClaimedByAnyoneElse(self, loc, out claim))
|
||||
return Constants.CellCost;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -13,6 +13,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenRA.Activities;
|
||||
using OpenRA.Mods.Common.Pathfinder;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
@@ -45,7 +46,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
moveDisablers = self.TraitsImplementing<IDisableMove>();
|
||||
|
||||
getPath = () =>
|
||||
self.World.WorldActor.Trait<PathFinder>().FindPath(
|
||||
self.World.WorldActor.Trait<IPathFinder>().FindPath(
|
||||
PathSearch.FromPoint(self.World, mobile.Info, self, mobile.ToCell, destination, false)
|
||||
.WithoutLaneBias());
|
||||
this.destination = destination;
|
||||
@@ -61,7 +62,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
mobile = self.Trait<Mobile>();
|
||||
moveDisablers = self.TraitsImplementing<IDisableMove>();
|
||||
|
||||
getPath = () => self.World.WorldActor.Trait<PathFinder>()
|
||||
getPath = () => self.World.WorldActor.Trait<IPathFinder>()
|
||||
.FindUnitPath(mobile.ToCell, destination, self);
|
||||
this.destination = destination;
|
||||
this.nearEnough = nearEnough;
|
||||
@@ -72,7 +73,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
mobile = self.Trait<Mobile>();
|
||||
moveDisablers = self.TraitsImplementing<IDisableMove>();
|
||||
|
||||
getPath = () => self.World.WorldActor.Trait<PathFinder>()
|
||||
getPath = () => self.World.WorldActor.Trait<IPathFinder>()
|
||||
.FindUnitPathToRange(mobile.FromCell, subCell, self.World.Map.CenterOfSubCell(destination, subCell), nearEnough, self);
|
||||
this.destination = destination;
|
||||
this.nearEnough = nearEnough;
|
||||
@@ -84,7 +85,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
moveDisablers = self.TraitsImplementing<IDisableMove>();
|
||||
|
||||
getPath = () =>
|
||||
self.World.WorldActor.Trait<PathFinder>().FindPath(
|
||||
self.World.WorldActor.Trait<IPathFinder>().FindPath(
|
||||
PathSearch.FromPoint(self.World, mobile.Info, self, mobile.ToCell, destination, false)
|
||||
.WithIgnoredActor(ignoredActor));
|
||||
|
||||
@@ -103,7 +104,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
if (!target.IsValidFor(self))
|
||||
return NoPath;
|
||||
|
||||
return self.World.WorldActor.Trait<PathFinder>().FindUnitPathToRange(
|
||||
return self.World.WorldActor.Trait<IPathFinder>().FindUnitPathToRange(
|
||||
mobile.ToCell, mobile.ToSubCell, target.CenterPosition, range, self);
|
||||
};
|
||||
|
||||
@@ -132,7 +133,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
return hash;
|
||||
}
|
||||
|
||||
List<CPos> EvalPath(Actor self, Mobile mobile)
|
||||
List<CPos> EvalPath()
|
||||
{
|
||||
var path = getPath().TakeWhile(a => a != mobile.ToCell).ToList();
|
||||
mobile.PathHash = HashList(path);
|
||||
@@ -155,7 +156,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
return this;
|
||||
}
|
||||
|
||||
path = EvalPath(self, mobile);
|
||||
path = EvalPath();
|
||||
SanityCheckPath(mobile);
|
||||
}
|
||||
|
||||
@@ -167,7 +168,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
|
||||
destination = path[0];
|
||||
|
||||
var nextCell = PopPath(self, mobile);
|
||||
var nextCell = PopPath(self);
|
||||
if (nextCell == null)
|
||||
return this;
|
||||
|
||||
@@ -202,7 +203,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
throw new InvalidOperationException("(Move) Sanity check failed");
|
||||
}
|
||||
|
||||
Pair<CPos, SubCell>? PopPath(Actor self, Mobile mobile)
|
||||
Pair<CPos, SubCell>? PopPath(Actor self)
|
||||
{
|
||||
if (path.Count == 0)
|
||||
return null;
|
||||
@@ -210,7 +211,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
var nextCell = path[path.Count - 1];
|
||||
|
||||
// Next cell in the move is blocked by another actor
|
||||
if (!mobile.CanEnterCell(nextCell, ignoredActor, true))
|
||||
if (!mobile.CanMoveFreelyInto(nextCell, ignoredActor, true))
|
||||
{
|
||||
// Are we close enough?
|
||||
var cellRange = nearEnough.Range / 1024;
|
||||
@@ -246,7 +247,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
|
||||
// Calculate a new path
|
||||
mobile.RemoveInfluence();
|
||||
var newPath = EvalPath(self, mobile);
|
||||
var newPath = EvalPath();
|
||||
mobile.AddInfluence();
|
||||
|
||||
if (newPath.Count != 0)
|
||||
@@ -372,7 +373,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
var fromSubcellOffset = self.World.Map.OffsetOfSubCell(mobile.FromSubCell);
|
||||
var toSubcellOffset = self.World.Map.OffsetOfSubCell(mobile.ToSubCell);
|
||||
|
||||
var nextCell = parent.PopPath(self, mobile);
|
||||
var nextCell = parent.PopPath(self);
|
||||
if (nextCell != null)
|
||||
{
|
||||
if (IsTurn(mobile, nextCell.Value.First))
|
||||
|
||||
@@ -12,6 +12,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Activities;
|
||||
using OpenRA.Mods.Common.Pathfinder;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Traits;
|
||||
|
||||
@@ -22,7 +23,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
static readonly List<CPos> NoPath = new List<CPos>();
|
||||
|
||||
readonly Mobile mobile;
|
||||
readonly PathFinder pathFinder;
|
||||
readonly IPathFinder pathFinder;
|
||||
readonly DomainIndex domainIndex;
|
||||
readonly uint movementClass;
|
||||
|
||||
@@ -36,7 +37,7 @@ namespace OpenRA.Mods.Common.Activities
|
||||
Target = target;
|
||||
|
||||
mobile = self.Trait<Mobile>();
|
||||
pathFinder = self.World.WorldActor.Trait<PathFinder>();
|
||||
pathFinder = self.World.WorldActor.Trait<IPathFinder>();
|
||||
domainIndex = self.World.WorldActor.Trait<DomainIndex>();
|
||||
movementClass = (uint)mobile.Info.GetMovementClass(self.World.TileSet);
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
<Compile Include="Orders\GlobalButtonOrderGenerator.cs" />
|
||||
<Compile Include="Orders\PlaceBuildingOrderGenerator.cs" />
|
||||
<Compile Include="Orders\UnitOrderTargeter.cs" />
|
||||
<Compile Include="Pathfinder\CellInfo.cs" />
|
||||
<Compile Include="PlayerExtensions.cs" />
|
||||
<Compile Include="ServerTraits\ColorValidator.cs" />
|
||||
<Compile Include="ServerTraits\LobbyCommands.cs" />
|
||||
@@ -435,9 +436,15 @@
|
||||
<Compile Include="Traits\World\PaletteFromCurrentTileset.cs" />
|
||||
<Compile Include="Traits\World\PaletteFromFile.cs" />
|
||||
<Compile Include="Traits\World\PaletteFromRGBA.cs" />
|
||||
<Compile Include="Pathfinder\CellInfoLayerManager.cs" />
|
||||
<Compile Include="Pathfinder\Constants.cs" />
|
||||
<Compile Include="Pathfinder\PathGraph.cs" />
|
||||
<Compile Include="Pathfinder\PathFinderCacheDecorator.cs" />
|
||||
<Compile Include="Pathfinder\PathCacheStorage.cs" />
|
||||
<Compile Include="Traits\World\PathFinder.cs" />
|
||||
<Compile Include="Traits\World\PathfinderDebugOverlay.cs" />
|
||||
<Compile Include="Traits\World\PathSearch.cs" />
|
||||
<Compile Include="Pathfinder\PathSearch.cs" />
|
||||
<Compile Include="Pathfinder\BasePathSearch.cs" />
|
||||
<Compile Include="Traits\World\PlayerPaletteFromCurrentTileset.cs" />
|
||||
<Compile Include="Traits\World\RadarPings.cs" />
|
||||
<Compile Include="Traits\World\ResourceClaim.cs" />
|
||||
|
||||
203
OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs
Normal file
203
OpenRA.Mods.Common/Pathfinder/BasePathSearch.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
public interface IPathSearch
|
||||
{
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The Graph used by the A*
|
||||
/// </summary>
|
||||
IGraph<CellInfo> Graph { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The open queue where nodes that are worth to consider are stored by their estimator
|
||||
/// </summary>
|
||||
IPriorityQueue<CPos> OpenQueue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the analyzed nodes by the expand function
|
||||
/// </summary>
|
||||
IEnumerable<Pair<CPos, int>> Considered { get; }
|
||||
|
||||
bool Debug { get; set; }
|
||||
|
||||
Player Owner { get; }
|
||||
|
||||
int MaxCost { get; }
|
||||
|
||||
IPathSearch Reverse();
|
||||
|
||||
IPathSearch WithCustomBlocker(Func<CPos, bool> customBlock);
|
||||
|
||||
IPathSearch WithIgnoredActor(Actor b);
|
||||
|
||||
IPathSearch WithHeuristic(Func<CPos, int> h);
|
||||
|
||||
IPathSearch WithCustomCost(Func<CPos, int> w);
|
||||
|
||||
IPathSearch WithoutLaneBias();
|
||||
|
||||
IPathSearch FromPoint(CPos from);
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether a location is a target based on its estimate
|
||||
/// (An estimate of 0 means that the location and the unit's goal
|
||||
/// are the same. There could be multiple goals).
|
||||
/// </summary>
|
||||
/// <param name="location">The location to assess</param>
|
||||
/// <returns>Whether the location is a target</returns>
|
||||
bool IsTarget(CPos location);
|
||||
|
||||
CPos Expand();
|
||||
}
|
||||
|
||||
public abstract class BasePathSearch : IPathSearch
|
||||
{
|
||||
public IGraph<CellInfo> Graph { get; set; }
|
||||
|
||||
// The Id of a Pathsearch is computed by its properties.
|
||||
// So two PathSearch instances with the same parameters will
|
||||
// Compute the same Id. This is used for caching purposes.
|
||||
public string Id
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(this.Graph.Actor.ActorID);
|
||||
while (!startPoints.Empty)
|
||||
{
|
||||
var startpoint = startPoints.Pop();
|
||||
builder.Append(startpoint.X);
|
||||
builder.Append(startpoint.Y);
|
||||
builder.Append(Graph[startpoint].EstimatedTotal);
|
||||
}
|
||||
|
||||
builder.Append(Graph.InReverse);
|
||||
if (Graph.IgnoredActor != null) builder.Append(Graph.IgnoredActor.ActorID);
|
||||
builder.Append(Graph.LaneBias);
|
||||
|
||||
id = builder.ToString();
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
public IPriorityQueue<CPos> OpenQueue { get; protected set; }
|
||||
|
||||
public abstract IEnumerable<Pair<CPos, int>> Considered { get; }
|
||||
|
||||
public Player Owner { get { return this.Graph.Actor.Owner; } }
|
||||
public int MaxCost { get; protected set; }
|
||||
public bool Debug { get; set; }
|
||||
string id;
|
||||
protected Func<CPos, int> heuristic;
|
||||
|
||||
// This member is used to compute the ID of PathSearch.
|
||||
// Essentially, it represents a collection of the initial
|
||||
// points considered and their Heuristics to reach
|
||||
// the target. It pretty match identifies, in conjunction of the Actor,
|
||||
// a deterministic set of calculations
|
||||
protected IPriorityQueue<CPos> startPoints;
|
||||
|
||||
protected BasePathSearch(IGraph<CellInfo> graph)
|
||||
{
|
||||
Graph = graph;
|
||||
OpenQueue = new PriorityQueue<CPos>(new PositionComparer(Graph));
|
||||
startPoints = new PriorityQueue<CPos>(new PositionComparer(Graph));
|
||||
Debug = false;
|
||||
MaxCost = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default: Diagonal distance heuristic. More information:
|
||||
/// http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
|
||||
/// </summary>
|
||||
/// <returns>A delegate that calculates the estimation for a node</returns>
|
||||
protected static Func<CPos, int> DefaultEstimator(CPos destination)
|
||||
{
|
||||
return here =>
|
||||
{
|
||||
var diag = Math.Min(Math.Abs(here.X - destination.X), Math.Abs(here.Y - destination.Y));
|
||||
var straight = Math.Abs(here.X - destination.X) + Math.Abs(here.Y - destination.Y);
|
||||
|
||||
// According to the information link, this is the shape of the function.
|
||||
// We just extract factors to simplify.
|
||||
// Possible simplification: var h = Constants.CellCost * (straight + (Constants.Sqrt2 - 2) * diag);
|
||||
return Constants.CellCost * straight + (Constants.DiagonalCellCost - 2 * Constants.CellCost) * diag;
|
||||
};
|
||||
}
|
||||
|
||||
public IPathSearch Reverse()
|
||||
{
|
||||
Graph.InReverse = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IPathSearch WithCustomBlocker(Func<CPos, bool> customBlock)
|
||||
{
|
||||
Graph.CustomBlock = customBlock;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IPathSearch WithIgnoredActor(Actor b)
|
||||
{
|
||||
Graph.IgnoredActor = b;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IPathSearch WithHeuristic(Func<CPos, int> h)
|
||||
{
|
||||
heuristic = h;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IPathSearch WithCustomCost(Func<CPos, int> w)
|
||||
{
|
||||
Graph.CustomCost = w;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IPathSearch WithoutLaneBias()
|
||||
{
|
||||
Graph.LaneBias = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IPathSearch FromPoint(CPos from)
|
||||
{
|
||||
if (this.Graph.World.Map.Contains(from))
|
||||
AddInitialCell(from);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
protected abstract void AddInitialCell(CPos cell);
|
||||
|
||||
public bool IsTarget(CPos location)
|
||||
{
|
||||
var locInfo = Graph[location];
|
||||
return locInfo.EstimatedTotal - locInfo.CostSoFar == 0;
|
||||
}
|
||||
|
||||
public abstract CPos Expand();
|
||||
}
|
||||
}
|
||||
78
OpenRA.Mods.Common/Pathfinder/CellInfo.cs
Normal file
78
OpenRA.Mods.Common/Pathfinder/CellInfo.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the three states that a node in the graph can have.
|
||||
/// Based on A* algorithm specification
|
||||
/// </summary>
|
||||
public enum CellStatus
|
||||
{
|
||||
Unvisited,
|
||||
Open,
|
||||
Closed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores information about nodes in the pathfinding graph
|
||||
/// </summary>
|
||||
public struct CellInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The cost to move from the start up to this node
|
||||
/// </summary>
|
||||
public readonly int CostSoFar;
|
||||
|
||||
/// <summary>
|
||||
/// The estimation of how far is the node from our goal
|
||||
/// </summary>
|
||||
public readonly int EstimatedTotal;
|
||||
|
||||
/// <summary>
|
||||
/// The previous node of this one that follows the shortest path
|
||||
/// </summary>
|
||||
public readonly CPos PreviousPos;
|
||||
|
||||
/// <summary>
|
||||
/// The status of this node
|
||||
/// </summary>
|
||||
public readonly CellStatus Status;
|
||||
|
||||
public CellInfo(int costSoFar, int estimatedTotal, CPos previousPos, CellStatus status)
|
||||
{
|
||||
CostSoFar = costSoFar;
|
||||
PreviousPos = previousPos;
|
||||
Status = status;
|
||||
EstimatedTotal = estimatedTotal;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two nodes according to their estimations
|
||||
/// </summary>
|
||||
public class PositionComparer : IComparer<CPos>
|
||||
{
|
||||
readonly IGraph<CellInfo> graph;
|
||||
|
||||
public PositionComparer(IGraph<CellInfo> graph)
|
||||
{
|
||||
this.graph = graph;
|
||||
}
|
||||
|
||||
public int Compare(CPos x, CPos y)
|
||||
{
|
||||
return Math.Sign(graph[x].EstimatedTotal - graph[y].EstimatedTotal);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
OpenRA.Mods.Common/Pathfinder/CellInfoLayerManager.cs
Normal file
111
OpenRA.Mods.Common/Pathfinder/CellInfoLayerManager.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
public interface ICellInfoLayerManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a CellLayer of Nodes from the pool
|
||||
/// </summary>
|
||||
CellLayer<CellInfo> GetFromPool();
|
||||
|
||||
/// <summary>
|
||||
/// Puts a CellLayer into the pool
|
||||
/// </summary>
|
||||
void PutBackIntoPool(CellLayer<CellInfo> ci);
|
||||
|
||||
/// <summary>
|
||||
/// Creates (or obtains from the pool) a CellLayer given a map
|
||||
/// </summary>
|
||||
CellLayer<CellInfo> NewLayer(IMap map);
|
||||
}
|
||||
|
||||
public sealed class CellInfoLayerManager : ICellInfoLayerManager
|
||||
{
|
||||
readonly Queue<CellLayer<CellInfo>> cellInfoPool = new Queue<CellLayer<CellInfo>>();
|
||||
readonly object defaultCellInfoLayerSync = new object();
|
||||
CellLayer<CellInfo> defaultCellInfoLayer;
|
||||
|
||||
static ICellInfoLayerManager instance = new CellInfoLayerManager();
|
||||
|
||||
public static ICellInfoLayerManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetInstance(ICellInfoLayerManager cellInfoLayerManager)
|
||||
{
|
||||
instance = cellInfoLayerManager;
|
||||
}
|
||||
|
||||
public CellLayer<CellInfo> GetFromPool()
|
||||
{
|
||||
lock (cellInfoPool)
|
||||
return cellInfoPool.Dequeue();
|
||||
}
|
||||
|
||||
public void PutBackIntoPool(CellLayer<CellInfo> ci)
|
||||
{
|
||||
lock (cellInfoPool)
|
||||
cellInfoPool.Enqueue(ci);
|
||||
}
|
||||
|
||||
public CellLayer<CellInfo> NewLayer(IMap map)
|
||||
{
|
||||
CellLayer<CellInfo> result = null;
|
||||
var mapSize = new Size(map.MapSize.X, map.MapSize.Y);
|
||||
|
||||
// HACK: Uses a static cache so that double-ended searches (which have two PathSearch instances)
|
||||
// can implicitly share data. The PathFinder should allocate the CellInfo array and pass it
|
||||
// explicitly to the things that need to share it.
|
||||
while (cellInfoPool.Count > 0)
|
||||
{
|
||||
var cellInfo = GetFromPool();
|
||||
if (cellInfo.Size != mapSize || cellInfo.Shape != map.TileShape)
|
||||
{
|
||||
Log.Write("debug", "Discarding old pooled CellInfo of wrong size.");
|
||||
continue;
|
||||
}
|
||||
|
||||
result = cellInfo;
|
||||
break;
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
result = new CellLayer<CellInfo>(map);
|
||||
|
||||
lock (defaultCellInfoLayerSync)
|
||||
{
|
||||
if (defaultCellInfoLayer == null ||
|
||||
defaultCellInfoLayer.Size != mapSize ||
|
||||
defaultCellInfoLayer.Shape != map.TileShape)
|
||||
{
|
||||
defaultCellInfoLayer =
|
||||
CellLayer<CellInfo>.CreateInstance(
|
||||
mpos => new CellInfo(int.MaxValue, int.MaxValue, mpos.ToCPos(map as Map), CellStatus.Unvisited),
|
||||
new Size(map.MapSize.X, map.MapSize.Y),
|
||||
map.TileShape);
|
||||
}
|
||||
|
||||
result.CopyValuesFrom(defaultCellInfoLayer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
OpenRA.Mods.Common/Pathfinder/Constants.cs
Normal file
29
OpenRA.Mods.Common/Pathfinder/Constants.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Min cost to arrive from once cell to an adjacent one
|
||||
/// (125 according to runtime tests where we could assess the cost
|
||||
/// a unit took to move one cell horizontally)
|
||||
/// </summary>
|
||||
public const int CellCost = 125;
|
||||
|
||||
/// <summary>
|
||||
/// Min cost to arrive from once cell to a diagonal adjacent one
|
||||
/// (125 * Sqrt(2) according to runtime tests where we could assess the cost
|
||||
/// a unit took to move one cell diagonally)
|
||||
/// </summary>
|
||||
public const int DiagonalCellCost = 177;
|
||||
}
|
||||
}
|
||||
75
OpenRA.Mods.Common/Pathfinder/PathCacheStorage.cs
Normal file
75
OpenRA.Mods.Common/Pathfinder/PathCacheStorage.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
public class PathCacheStorage : ICacheStorage<List<CPos>>
|
||||
{
|
||||
class CachedPath
|
||||
{
|
||||
public List<CPos> Result;
|
||||
public int Tick;
|
||||
}
|
||||
|
||||
const int MaxPathAge = 50;
|
||||
readonly IWorld world;
|
||||
Dictionary<string, CachedPath> cachedPaths = new Dictionary<string, CachedPath>(100);
|
||||
|
||||
public PathCacheStorage(IWorld world)
|
||||
{
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
cachedPaths.Remove(key);
|
||||
}
|
||||
|
||||
public void Store(string key, List<CPos> data)
|
||||
{
|
||||
// Eventually clean up the cachedPaths dictionary
|
||||
if (cachedPaths.Count >= 100)
|
||||
foreach (var cachedPath in cachedPaths.Where(p => IsExpired(p.Value)).ToList())
|
||||
cachedPaths.Remove(cachedPath.Key);
|
||||
|
||||
cachedPaths.Add(key, new CachedPath
|
||||
{
|
||||
Tick = world.WorldTick,
|
||||
Result = data
|
||||
});
|
||||
}
|
||||
|
||||
public List<CPos> Retrieve(string key)
|
||||
{
|
||||
CachedPath cached;
|
||||
if (cachedPaths.TryGetValue(key, out cached))
|
||||
{
|
||||
if (IsExpired(cached))
|
||||
{
|
||||
cachedPaths.Remove(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
cached.Tick = world.WorldTick;
|
||||
return cached.Result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bool IsExpired(CachedPath path)
|
||||
{
|
||||
return world.WorldTick - path.Tick > MaxPathAge;
|
||||
}
|
||||
}
|
||||
}
|
||||
104
OpenRA.Mods.Common/Pathfinder/PathFinderCacheDecorator.cs
Normal file
104
OpenRA.Mods.Common/Pathfinder/PathFinderCacheDecorator.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// A decorator used to cache the pathfinder (Decorator design pattern)
|
||||
/// </summary>
|
||||
public class PathFinderCacheDecorator : IPathFinder
|
||||
{
|
||||
readonly IPathFinder pathFinder;
|
||||
readonly ICacheStorage<List<CPos>> cacheStorage;
|
||||
|
||||
public PathFinderCacheDecorator(IPathFinder pathFinder, ICacheStorage<List<CPos>> cacheStorage)
|
||||
{
|
||||
this.pathFinder = pathFinder;
|
||||
this.cacheStorage = cacheStorage;
|
||||
}
|
||||
|
||||
public List<CPos> FindUnitPath(CPos source, CPos target, IActor self)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
var key = "FindUnitPath" + self.ActorID + source.X + source.Y + target.X + target.Y;
|
||||
var cachedPath = cacheStorage.Retrieve(key);
|
||||
|
||||
if (cachedPath != null)
|
||||
return cachedPath;
|
||||
|
||||
var pb = pathFinder.FindUnitPath(source, target, self);
|
||||
|
||||
cacheStorage.Store(key, pb);
|
||||
|
||||
return pb;
|
||||
}
|
||||
}
|
||||
|
||||
public List<CPos> FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WRange range, IActor self)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
var key = "FindUnitPathToRange" + self.ActorID + source.X + source.Y + target.X + target.Y;
|
||||
var cachedPath = cacheStorage.Retrieve(key);
|
||||
|
||||
if (cachedPath != null)
|
||||
return cachedPath;
|
||||
|
||||
var pb = pathFinder.FindUnitPathToRange(source, srcSub, target, range, self);
|
||||
|
||||
cacheStorage.Store(key, pb);
|
||||
|
||||
return pb;
|
||||
}
|
||||
}
|
||||
|
||||
public List<CPos> FindPath(IPathSearch search)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
var key = "FindPath" + search.Id;
|
||||
var cachedPath = cacheStorage.Retrieve(key);
|
||||
|
||||
if (cachedPath != null)
|
||||
return cachedPath;
|
||||
|
||||
var pb = pathFinder.FindPath(search);
|
||||
|
||||
cacheStorage.Store(key, pb);
|
||||
|
||||
return pb;
|
||||
}
|
||||
}
|
||||
|
||||
public List<CPos> FindBidiPath(IPathSearch fromSrc, IPathSearch fromDest)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
var key = "FindBidiPath" + fromSrc.Id + fromDest.Id;
|
||||
var cachedPath = cacheStorage.Retrieve(key);
|
||||
|
||||
if (cachedPath != null)
|
||||
return cachedPath;
|
||||
|
||||
var pb = pathFinder.FindBidiPath(fromSrc, fromDest);
|
||||
|
||||
cacheStorage.Store(key, pb);
|
||||
|
||||
return pb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
198
OpenRA.Mods.Common/Pathfinder/PathGraph.cs
Normal file
198
OpenRA.Mods.Common/Pathfinder/PathGraph.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a graph with nodes and edges
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of node used in the graph</typeparam>
|
||||
public interface IGraph<T> : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all the Connections for a given node in the graph
|
||||
/// </summary>
|
||||
ICollection<GraphConnection> GetConnections(CPos position);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an object given a node in the graph
|
||||
/// </summary>
|
||||
T this[CPos pos] { get; set; }
|
||||
|
||||
Func<CPos, bool> CustomBlock { get; set; }
|
||||
|
||||
Func<CPos, int> CustomCost { get; set; }
|
||||
|
||||
int LaneBias { get; set; }
|
||||
|
||||
bool InReverse { get; set; }
|
||||
|
||||
IActor IgnoredActor { get; set; }
|
||||
|
||||
IWorld World { get; }
|
||||
|
||||
IActor Actor { get; }
|
||||
}
|
||||
|
||||
public struct GraphConnection
|
||||
{
|
||||
public readonly CPos Source;
|
||||
public readonly CPos Destination;
|
||||
public readonly int Cost;
|
||||
|
||||
public GraphConnection(CPos source, CPos destination, int cost)
|
||||
{
|
||||
Source = source;
|
||||
Destination = destination;
|
||||
Cost = cost;
|
||||
}
|
||||
}
|
||||
|
||||
public class PathGraph : IGraph<CellInfo>
|
||||
{
|
||||
public IActor Actor { get; private set; }
|
||||
public IWorld World { get; private set; }
|
||||
public Func<CPos, bool> CustomBlock { get; set; }
|
||||
public Func<CPos, int> CustomCost { get; set; }
|
||||
public int LaneBias { get; set; }
|
||||
public bool InReverse { get; set; }
|
||||
public IActor IgnoredActor { get; set; }
|
||||
|
||||
readonly CellConditions checkConditions;
|
||||
readonly IMobileInfo mobileInfo;
|
||||
CellLayer<CellInfo> cellInfo;
|
||||
|
||||
public const int InvalidNode = int.MaxValue;
|
||||
|
||||
public PathGraph(CellLayer<CellInfo> cellInfo, IMobileInfo mobileInfo, IActor actor, IWorld world, bool checkForBlocked)
|
||||
{
|
||||
this.cellInfo = cellInfo;
|
||||
World = world;
|
||||
this.mobileInfo = mobileInfo;
|
||||
Actor = actor;
|
||||
LaneBias = 1;
|
||||
checkConditions = checkForBlocked ? CellConditions.TransientActors : CellConditions.None;
|
||||
}
|
||||
|
||||
// Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed
|
||||
// to be reached more cheaply by a path through our parent cell which does not include the current cell.
|
||||
// For horizontal/vertical directions, the set is the three cells 'ahead'. For diagonal directions, the set
|
||||
// is the three cells ahead, plus the two cells to the side, which we cannot exclude without knowing if
|
||||
// the cell directly between them and our parent is passable.
|
||||
static readonly CVec[][] DirectedNeighbors = {
|
||||
new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(-1, 0), new CVec(-1, 1) },
|
||||
new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1) },
|
||||
new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) },
|
||||
new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1) },
|
||||
CVec.Directions,
|
||||
new[] { new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) },
|
||||
new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) },
|
||||
new[] { new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) },
|
||||
new[] { new CVec(1, -1), new CVec(1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) },
|
||||
};
|
||||
|
||||
public ICollection<GraphConnection> GetConnections(CPos position)
|
||||
{
|
||||
var previousPos = cellInfo[position].PreviousPos;
|
||||
|
||||
var dx = position.X - previousPos.X;
|
||||
var dy = position.Y - previousPos.Y;
|
||||
var index = dy * 3 + dx + 4;
|
||||
|
||||
var validNeighbors = new LinkedList<GraphConnection>();
|
||||
var directions = DirectedNeighbors[index];
|
||||
for (var i = 0; i < directions.Length; i++)
|
||||
{
|
||||
var neighbor = position + directions[i];
|
||||
var movementCost = GetCostToNode(neighbor, directions[i]);
|
||||
if (movementCost != InvalidNode)
|
||||
validNeighbors.AddLast(new GraphConnection(position, neighbor, movementCost));
|
||||
}
|
||||
|
||||
return validNeighbors;
|
||||
}
|
||||
|
||||
int GetCostToNode(CPos destNode, CVec direction)
|
||||
{
|
||||
int movementCost;
|
||||
if (mobileInfo.CanEnterCell(
|
||||
World as World,
|
||||
Actor as Actor,
|
||||
destNode,
|
||||
out movementCost,
|
||||
IgnoredActor as Actor,
|
||||
checkConditions) && !(CustomBlock != null && CustomBlock(destNode)))
|
||||
{
|
||||
return CalculateCellCost(destNode, direction, movementCost);
|
||||
}
|
||||
|
||||
return InvalidNode;
|
||||
}
|
||||
|
||||
int CalculateCellCost(CPos neighborCPos, CVec direction, int movementCost)
|
||||
{
|
||||
var cellCost = movementCost;
|
||||
|
||||
if (direction.X * direction.Y != 0)
|
||||
cellCost = (cellCost * 34) / 24;
|
||||
|
||||
if (CustomCost != null)
|
||||
cellCost += CustomCost(neighborCPos);
|
||||
|
||||
// directional bonuses for smoother flow!
|
||||
if (LaneBias != 0)
|
||||
{
|
||||
var ux = neighborCPos.X + (InReverse ? 1 : 0) & 1;
|
||||
var uy = neighborCPos.Y + (InReverse ? 1 : 0) & 1;
|
||||
|
||||
if ((ux == 0 && direction.Y < 0) || (ux == 1 && direction.Y > 0))
|
||||
cellCost += LaneBias;
|
||||
|
||||
if ((uy == 0 && direction.X < 0) || (uy == 1 && direction.X > 0))
|
||||
cellCost += LaneBias;
|
||||
}
|
||||
|
||||
return cellCost;
|
||||
}
|
||||
|
||||
bool disposed;
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
disposed = true;
|
||||
|
||||
CellInfoLayerManager.Instance.PutBackIntoPool(cellInfo);
|
||||
cellInfo = null;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~PathGraph() { Dispose(); }
|
||||
|
||||
public CellInfo this[CPos pos]
|
||||
{
|
||||
get
|
||||
{
|
||||
return cellInfo[pos];
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
cellInfo[pos] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
OpenRA.Mods.Common/Pathfinder/PathSearch.cs
Normal file
129
OpenRA.Mods.Common/Pathfinder/PathSearch.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Mods.Common.Pathfinder
|
||||
{
|
||||
public sealed class PathSearch : BasePathSearch
|
||||
{
|
||||
public override IEnumerable<Pair<CPos, int>> Considered
|
||||
{
|
||||
get { return considered; }
|
||||
}
|
||||
|
||||
LinkedList<Pair<CPos, int>> considered;
|
||||
|
||||
#region Constructors
|
||||
|
||||
private PathSearch(IGraph<CellInfo> graph)
|
||||
: base(graph)
|
||||
{
|
||||
considered = new LinkedList<Pair<CPos, int>>();
|
||||
}
|
||||
|
||||
public static IPathSearch Search(IWorld world, IMobileInfo mi, IActor self, bool checkForBlocked)
|
||||
{
|
||||
var graph = new PathGraph(CellInfoLayerManager.Instance.NewLayer(world.Map), mi, self, world, checkForBlocked);
|
||||
return new PathSearch(graph);
|
||||
}
|
||||
|
||||
public static IPathSearch FromPoint(IWorld world, IMobileInfo mi, IActor self, CPos from, CPos target, bool checkForBlocked)
|
||||
{
|
||||
var graph = new PathGraph(CellInfoLayerManager.Instance.NewLayer(world.Map), mi, self, world, checkForBlocked);
|
||||
var search = new PathSearch(graph)
|
||||
{
|
||||
heuristic = DefaultEstimator(target)
|
||||
};
|
||||
|
||||
if (world.Map.Contains(from))
|
||||
search.AddInitialCell(from);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
public static IPathSearch FromPoints(IWorld world, IMobileInfo mi, IActor self, IEnumerable<CPos> froms, CPos target, bool checkForBlocked)
|
||||
{
|
||||
var graph = new PathGraph(CellInfoLayerManager.Instance.NewLayer(world.Map), mi, self, world, checkForBlocked);
|
||||
var search = new PathSearch(graph)
|
||||
{
|
||||
heuristic = DefaultEstimator(target)
|
||||
};
|
||||
|
||||
foreach (var sl in froms.Where(sl => world.Map.Contains(sl)))
|
||||
search.AddInitialCell(sl);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
protected override void AddInitialCell(CPos location)
|
||||
{
|
||||
Graph[location] = new CellInfo(0, heuristic(location), location, CellStatus.Open);
|
||||
OpenQueue.Add(location);
|
||||
startPoints.Add(location);
|
||||
considered.AddLast(new Pair<CPos, int>(location, 0));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// This function analyzes the neighbors of the most promising node in the Pathfinding graph
|
||||
/// using the A* algorithm (A-star) and returns that node
|
||||
/// </summary>
|
||||
/// <returns>The most promising node of the iteration</returns>
|
||||
public override CPos Expand()
|
||||
{
|
||||
var currentMinNode = OpenQueue.Pop();
|
||||
|
||||
var currentCell = Graph[currentMinNode];
|
||||
Graph[currentMinNode] = new CellInfo(currentCell.CostSoFar, currentCell.EstimatedTotal, currentCell.PreviousPos, CellStatus.Closed);
|
||||
|
||||
foreach (var connection in Graph.GetConnections(currentMinNode))
|
||||
{
|
||||
// Calculate the cost up to that point
|
||||
var gCost = currentCell.CostSoFar + connection.Cost;
|
||||
|
||||
var neighborCPos = connection.Destination;
|
||||
var neighborCell = Graph[neighborCPos];
|
||||
|
||||
// Cost is even higher; next direction:
|
||||
if (gCost >= neighborCell.CostSoFar)
|
||||
continue;
|
||||
|
||||
// Now we may seriously consider this direction using heuristics. If the cell has
|
||||
// already been processed, we can reuse the result (just the difference between the
|
||||
// estimated total and the cost so far
|
||||
int hCost;
|
||||
if (neighborCell.Status == CellStatus.Open || neighborCell.Status == CellStatus.Closed)
|
||||
hCost = neighborCell.EstimatedTotal - neighborCell.CostSoFar;
|
||||
else
|
||||
hCost = heuristic(neighborCPos);
|
||||
|
||||
Graph[neighborCPos] = new CellInfo(gCost, gCost + hCost, currentMinNode, CellStatus.Open);
|
||||
|
||||
if (neighborCell.Status != CellStatus.Open)
|
||||
OpenQueue.Add(neighborCPos);
|
||||
|
||||
if (Debug)
|
||||
{
|
||||
if (gCost > MaxCost)
|
||||
MaxCost = gCost;
|
||||
|
||||
considered.AddLast(new Pair<CPos, int>(neighborCPos, gCost));
|
||||
}
|
||||
}
|
||||
|
||||
return currentMinNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using System.Linq;
|
||||
using OpenRA.Activities;
|
||||
using OpenRA.Mods.Common.Activities;
|
||||
using OpenRA.Mods.Common.Orders;
|
||||
using OpenRA.Mods.Common.Pathfinder;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
@@ -115,12 +116,12 @@ namespace OpenRA.Mods.Common.Traits
|
||||
var refs = (
|
||||
from r in self.World.ActorsWithTrait<IAcceptResources>()
|
||||
where r.Actor != ignore && r.Actor.Owner == self.Owner && IsAcceptableProcType(r.Actor)
|
||||
let linkedHarvs = self.World.ActorsWithTrait<Harvester>().Where(a => a.Trait.LinkedProc == r.Actor).Count()
|
||||
let linkedHarvs = self.World.ActorsWithTrait<Harvester>().Count(a => a.Trait.LinkedProc == r.Actor)
|
||||
select new { Location = r.Actor.Location + r.Trait.DeliveryOffset, Actor = r.Actor, Occupancy = linkedHarvs }).ToDictionary(r => r.Location);
|
||||
|
||||
// Start a search from each refinery's delivery location:
|
||||
var mi = self.Info.Traits.Get<MobileInfo>();
|
||||
var path = self.World.WorldActor.Trait<PathFinder>().FindPath(
|
||||
var path = self.World.WorldActor.Trait<IPathFinder>().FindPath(
|
||||
PathSearch.FromPoints(self.World, mi, self, refs.Values.Select(r => r.Location), self.Location, false)
|
||||
.WithCustomCost((loc) =>
|
||||
{
|
||||
@@ -374,23 +375,25 @@ namespace OpenRA.Mods.Common.Traits
|
||||
var territory = self.World.WorldActor.TraitOrDefault<ResourceClaimLayer>();
|
||||
|
||||
// Find any harvestable resources:
|
||||
var path = self.World.WorldActor.Trait<PathFinder>().FindPath(
|
||||
var path = self.World.WorldActor.Trait<IPathFinder>().FindPath(
|
||||
PathSearch.Search(self.World, mobileInfo, self, true)
|
||||
.WithHeuristic(loc =>
|
||||
{
|
||||
// Get the resource at this location:
|
||||
var resType = resLayer.GetResource(loc);
|
||||
|
||||
if (resType == null) return 1;
|
||||
if (resType == null)
|
||||
return Constants.CellCost;
|
||||
|
||||
// Can the harvester collect this kind of resource?
|
||||
if (!harvInfo.Resources.Contains(resType.Info.Name)) return 1;
|
||||
if (!harvInfo.Resources.Contains(resType.Info.Name))
|
||||
return Constants.CellCost;
|
||||
|
||||
// Another harvester has claimed this resource:
|
||||
if (territory != null)
|
||||
{
|
||||
ResourceClaim claim;
|
||||
if (territory.IsClaimedByAnyoneElse(self, loc, out claim)) return 1;
|
||||
if (territory.IsClaimedByAnyoneElse(self, loc, out claim))
|
||||
return Constants.CellCost;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -29,17 +29,16 @@ namespace OpenRA.Mods.Common.Traits
|
||||
All = TransientActors | BlockedByMovers
|
||||
}
|
||||
|
||||
public interface IMobileInfo
|
||||
public interface IMobileInfo : IMoveInfo
|
||||
{
|
||||
int MovementCostForCell(World world, CPos cell);
|
||||
bool CanEnterCell(World world, Actor self, CPos cell, out int movementCost, Actor ignoreActor = null, CellConditions check = CellConditions.All);
|
||||
bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All);
|
||||
|
||||
int GetMovementClass(TileSet tileset);
|
||||
}
|
||||
|
||||
[Desc("Unit is able to move.")]
|
||||
public class MobileInfo : ITraitInfo, IOccupySpaceInfo, IFacingInfo, IMoveInfo, UsesInit<FacingInit>, UsesInit<LocationInit>, UsesInit<SubCellInit>, IMobileInfo
|
||||
public class MobileInfo : IMobileInfo, IOccupySpaceInfo, IFacingInfo, UsesInit<FacingInit>, UsesInit<LocationInit>, UsesInit<SubCellInit>
|
||||
{
|
||||
[FieldLoader.LoadUsing("LoadSpeeds")]
|
||||
[Desc("Set Water: 0 for ground units and lower the value on rough terrain.")]
|
||||
@@ -565,7 +564,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
return Info.CanEnterCell(self.World, self, cell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers);
|
||||
}
|
||||
|
||||
public bool CanMoveFreely(CPos cell, Actor ignoreActor = null, bool checkTransientActors = true)
|
||||
public bool CanMoveFreelyInto(CPos cell, Actor ignoreActor = null, bool checkTransientActors = true)
|
||||
{
|
||||
return Info.CanMoveFreelyInto(self.World, self, cell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenRA;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Mods.Common.Pathfinder;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
@@ -22,222 +20,221 @@ namespace OpenRA.Mods.Common.Traits
|
||||
[Desc("Calculates routes for mobile units based on the A* search algorithm.", " Attach this to the world actor.")]
|
||||
public class PathFinderInfo : ITraitInfo
|
||||
{
|
||||
public object Create(ActorInitializer init) { return new PathFinder(init.World); }
|
||||
public object Create(ActorInitializer init)
|
||||
{
|
||||
return new PathFinderCacheDecorator(new PathFinder(init.World), new PathCacheStorage(init.World));
|
||||
}
|
||||
}
|
||||
|
||||
public class PathFinder
|
||||
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, IActor self);
|
||||
|
||||
List<CPos> FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WRange range, IActor self);
|
||||
|
||||
/// <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
|
||||
{
|
||||
const int MaxPathAge = 50; /* x 40ms ticks */
|
||||
static readonly List<CPos> EmptyPath = new List<CPos>(0);
|
||||
readonly IWorld world;
|
||||
|
||||
readonly World world;
|
||||
public PathFinder(World world) { this.world = world; }
|
||||
|
||||
class CachedPath
|
||||
public PathFinder(IWorld world)
|
||||
{
|
||||
public CPos From;
|
||||
public CPos To;
|
||||
public List<CPos> Result;
|
||||
public int Tick;
|
||||
public Actor Actor;
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
List<CachedPath> cachedPaths = new List<CachedPath>();
|
||||
|
||||
public List<CPos> FindUnitPath(CPos from, CPos target, Actor self)
|
||||
public List<CPos> FindUnitPath(CPos source, CPos target, IActor self)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
var mi = self.Info.Traits.Get<IMobileInfo>();
|
||||
|
||||
// If a water-land transition is required, bail early
|
||||
var domainIndex = world.WorldActor.TraitOrDefault<DomainIndex>();
|
||||
if (domainIndex != null)
|
||||
{
|
||||
var cached = cachedPaths.FirstOrDefault(p => p.From == from && p.To == target && p.Actor == self);
|
||||
if (cached != null)
|
||||
{
|
||||
Log.Write("debug", "Actor {0} asked for a path from {1} tick(s) ago", self.ActorID, world.WorldTick - cached.Tick);
|
||||
if (world.WorldTick - cached.Tick > MaxPathAge)
|
||||
cachedPaths.Remove(cached);
|
||||
return new List<CPos>(cached.Result);
|
||||
}
|
||||
|
||||
var mi = self.Info.Traits.Get<MobileInfo>();
|
||||
|
||||
// If a water-land transition is required, bail early
|
||||
var domainIndex = self.World.WorldActor.TraitOrDefault<DomainIndex>();
|
||||
if (domainIndex != null)
|
||||
{
|
||||
var passable = mi.GetMovementClass(world.TileSet);
|
||||
if (!domainIndex.IsPassable(from, target, (uint)passable))
|
||||
return EmptyPath;
|
||||
}
|
||||
|
||||
var fromPoint = PathSearch.FromPoint(world, mi, self, target, from, true)
|
||||
.WithIgnoredActor(self);
|
||||
var fromPointReverse = PathSearch.FromPoint(world, mi, self, from, target, true)
|
||||
.WithIgnoredActor(self)
|
||||
.Reverse();
|
||||
var pb = FindBidiPath(
|
||||
fromPoint,
|
||||
fromPointReverse);
|
||||
|
||||
CheckSanePath2(pb, from, target);
|
||||
|
||||
cachedPaths.RemoveAll(p => world.WorldTick - p.Tick > MaxPathAge);
|
||||
cachedPaths.Add(new CachedPath { From = from, To = target, Actor = self, Result = pb, Tick = world.WorldTick });
|
||||
return new List<CPos>(pb);
|
||||
var passable = mi.GetMovementClass(world.TileSet);
|
||||
if (!domainIndex.IsPassable(source, target, (uint)passable))
|
||||
return EmptyPath;
|
||||
}
|
||||
|
||||
var pb = FindBidiPath(
|
||||
PathSearch.FromPoint(world, mi, self, target, source, true),
|
||||
PathSearch.FromPoint(world, mi, self, source, target, true).Reverse());
|
||||
|
||||
CheckSanePath2(pb, source, target);
|
||||
|
||||
return pb;
|
||||
}
|
||||
|
||||
public List<CPos> FindUnitPathToRange(CPos src, SubCell srcSub, WPos target, WRange range, Actor self)
|
||||
public List<CPos> FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WRange range, IActor self)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
var mi = self.Info.Traits.Get<MobileInfo>();
|
||||
var targetCell = world.Map.CellContaining(target);
|
||||
var rangeSquared = range.Range * range.Range;
|
||||
|
||||
// Correct for SubCell offset
|
||||
target -= world.Map.OffsetOfSubCell(srcSub);
|
||||
|
||||
// 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 = world.Map.FindTilesInCircle(targetCell, range.Range / 1024 + 1)
|
||||
.Where(t => (world.Map.CenterOfCell(t) - target).LengthSquared <= rangeSquared
|
||||
&& mi.CanEnterCell(self.World as World, self as Actor, 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
|
||||
var domainIndex = world.WorldActor.TraitOrDefault<DomainIndex>();
|
||||
if (domainIndex != null)
|
||||
{
|
||||
var mi = self.Info.Traits.Get<MobileInfo>();
|
||||
var targetCell = self.World.Map.CellContaining(target);
|
||||
var rangeSquared = range.Range * range.Range;
|
||||
var passable = mi.GetMovementClass(world.TileSet);
|
||||
tilesInRange = new List<CPos>(tilesInRange.Where(t => domainIndex.IsPassable(source, t, (uint)passable)));
|
||||
if (!tilesInRange.Any())
|
||||
return EmptyPath;
|
||||
}
|
||||
|
||||
// Correct for SubCell offset
|
||||
target -= self.World.Map.OffsetOfSubCell(srcSub);
|
||||
var path = FindBidiPath(
|
||||
PathSearch.FromPoints(world, mi, self, tilesInRange, source, true),
|
||||
PathSearch.FromPoint(world, mi, self, source, targetCell, true).Reverse());
|
||||
|
||||
// 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 = world.Map.FindTilesInCircle(targetCell, range.Range / 1024 + 1)
|
||||
.Where(t => (world.Map.CenterOfCell(t) - target).LengthSquared <= rangeSquared &&
|
||||
mi.CanEnterCell(self.World, self, t));
|
||||
return path;
|
||||
}
|
||||
|
||||
// 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
|
||||
var domainIndex = self.World.WorldActor.TraitOrDefault<DomainIndex>();
|
||||
if (domainIndex != null)
|
||||
public List<CPos> FindPath(IPathSearch search)
|
||||
{
|
||||
var dbg = world.WorldActor.TraitOrDefault<PathfinderDebugOverlay>();
|
||||
if (dbg != null && dbg.Visible)
|
||||
search.Debug = true;
|
||||
|
||||
List<CPos> path = null;
|
||||
|
||||
while (!search.OpenQueue.Empty)
|
||||
{
|
||||
var p = search.Expand();
|
||||
if (search.IsTarget(p))
|
||||
{
|
||||
var passable = mi.GetMovementClass(world.TileSet);
|
||||
tilesInRange = new List<CPos>(tilesInRange.Where(t => domainIndex.IsPassable(src, t, (uint)passable)));
|
||||
if (!tilesInRange.Any())
|
||||
return EmptyPath;
|
||||
path = MakePath(search.Graph, p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var path = FindBidiPath(
|
||||
PathSearch.FromPoints(world, mi, self, tilesInRange, src, true),
|
||||
PathSearch.FromPoint(world, mi, self, src, targetCell, true).Reverse());
|
||||
if (dbg != null && dbg.Visible)
|
||||
dbg.AddLayer(search.Considered, search.MaxCost, search.Owner);
|
||||
|
||||
search.Graph.Dispose();
|
||||
|
||||
if (path != null)
|
||||
return path;
|
||||
}
|
||||
|
||||
// no path exists
|
||||
return EmptyPath;
|
||||
}
|
||||
|
||||
public List<CPos> FindPath(PathSearch search)
|
||||
// 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)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
List<CPos> path = null;
|
||||
|
||||
var dbg = world.WorldActor.TraitOrDefault<PathfinderDebugOverlay>();
|
||||
if (dbg != null && dbg.Visible)
|
||||
{
|
||||
using (search)
|
||||
fromSrc.Debug = true;
|
||||
fromDest.Debug = true;
|
||||
}
|
||||
|
||||
while (!fromSrc.OpenQueue.Empty && !fromDest.OpenQueue.Empty)
|
||||
{
|
||||
// make some progress on the first search
|
||||
var p = fromSrc.Expand();
|
||||
|
||||
if (fromDest.Graph[p].Status == CellStatus.Closed &&
|
||||
fromDest.Graph[p].CostSoFar < int.MaxValue)
|
||||
{
|
||||
List<CPos> path = null;
|
||||
|
||||
while (!search.Queue.Empty)
|
||||
{
|
||||
var p = search.Expand(world);
|
||||
if (search.Heuristic(p) == 0)
|
||||
{
|
||||
path = MakePath(search.CellInfo, p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var dbg = world.WorldActor.TraitOrDefault<PathfinderDebugOverlay>();
|
||||
if (dbg != null)
|
||||
dbg.AddLayer(search.Considered.Select(p => new Pair<CPos, int>(p, search.CellInfo[p].MinCost)), search.MaxCost, search.Owner);
|
||||
|
||||
if (path != null)
|
||||
return path;
|
||||
path = MakeBidiPath(fromSrc, fromDest, p);
|
||||
break;
|
||||
}
|
||||
|
||||
// no path exists
|
||||
return EmptyPath;
|
||||
// make some progress on the second search
|
||||
var q = fromDest.Expand();
|
||||
|
||||
if (fromSrc.Graph[q].Status == CellStatus.Closed &&
|
||||
fromSrc.Graph[q].CostSoFar < int.MaxValue)
|
||||
{
|
||||
path = MakeBidiPath(fromSrc, fromDest, q);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dbg != null && dbg.Visible)
|
||||
{
|
||||
dbg.AddLayer(fromSrc.Considered, fromSrc.MaxCost, fromSrc.Owner);
|
||||
dbg.AddLayer(fromDest.Considered, fromDest.MaxCost, fromDest.Owner);
|
||||
}
|
||||
|
||||
fromSrc.Graph.Dispose();
|
||||
fromDest.Graph.Dispose();
|
||||
|
||||
if (path != null)
|
||||
return path;
|
||||
|
||||
return EmptyPath;
|
||||
}
|
||||
|
||||
static List<CPos> MakePath(CellLayer<CellInfo> cellInfo, CPos destination)
|
||||
// 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 pathNode = destination;
|
||||
var currentNode = destination;
|
||||
|
||||
while (cellInfo[pathNode].Path != pathNode)
|
||||
while (cellInfo[currentNode].PreviousPos != currentNode)
|
||||
{
|
||||
ret.Add(pathNode);
|
||||
pathNode = cellInfo[pathNode].Path;
|
||||
ret.Add(currentNode);
|
||||
currentNode = cellInfo[currentNode].PreviousPos;
|
||||
}
|
||||
|
||||
ret.Add(pathNode);
|
||||
ret.Add(currentNode);
|
||||
CheckSanePath(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Searches from both ends toward each other
|
||||
public List<CPos> FindBidiPath(PathSearch fromSrc, PathSearch fromDest)
|
||||
static List<CPos> MakeBidiPath(IPathSearch a, IPathSearch b, CPos confluenceNode)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
using (fromSrc)
|
||||
using (fromDest)
|
||||
{
|
||||
List<CPos> path = null;
|
||||
|
||||
while (!fromSrc.Queue.Empty && !fromDest.Queue.Empty)
|
||||
{
|
||||
/* make some progress on the first search */
|
||||
var p = fromSrc.Expand(world);
|
||||
|
||||
if (fromDest.CellInfo[p].Seen &&
|
||||
fromDest.CellInfo[p].MinCost < float.PositiveInfinity)
|
||||
{
|
||||
path = MakeBidiPath(fromSrc, fromDest, p);
|
||||
break;
|
||||
}
|
||||
|
||||
/* make some progress on the second search */
|
||||
var q = fromDest.Expand(world);
|
||||
|
||||
if (fromSrc.CellInfo[q].Seen &&
|
||||
fromSrc.CellInfo[q].MinCost < float.PositiveInfinity)
|
||||
{
|
||||
path = MakeBidiPath(fromSrc, fromDest, q);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var dbg = world.WorldActor.TraitOrDefault<PathfinderDebugOverlay>();
|
||||
if (dbg != null)
|
||||
{
|
||||
dbg.AddLayer(fromSrc.Considered.Select(p => new Pair<CPos, int>(p, fromSrc.CellInfo[p].MinCost)), fromSrc.MaxCost, fromSrc.Owner);
|
||||
dbg.AddLayer(fromDest.Considered.Select(p => new Pair<CPos, int>(p, fromDest.CellInfo[p].MinCost)), fromDest.MaxCost, fromDest.Owner);
|
||||
}
|
||||
|
||||
if (path != null)
|
||||
return path;
|
||||
}
|
||||
|
||||
return EmptyPath;
|
||||
}
|
||||
}
|
||||
|
||||
static List<CPos> MakeBidiPath(PathSearch a, PathSearch b, CPos p)
|
||||
{
|
||||
var ca = a.CellInfo;
|
||||
var cb = b.CellInfo;
|
||||
var ca = a.Graph;
|
||||
var cb = b.Graph;
|
||||
|
||||
var ret = new List<CPos>();
|
||||
|
||||
var q = p;
|
||||
while (ca[q].Path != q)
|
||||
var q = confluenceNode;
|
||||
while (ca[q].PreviousPos != q)
|
||||
{
|
||||
ret.Add(q);
|
||||
q = ca[q].Path;
|
||||
q = ca[q].PreviousPos;
|
||||
}
|
||||
|
||||
ret.Add(q);
|
||||
|
||||
ret.Reverse();
|
||||
|
||||
q = p;
|
||||
while (cb[q].Path != q)
|
||||
q = confluenceNode;
|
||||
while (cb[q].PreviousPos != q)
|
||||
{
|
||||
q = cb[q].Path;
|
||||
q = cb[q].PreviousPos;
|
||||
ret.Add(q);
|
||||
}
|
||||
|
||||
@@ -246,22 +243,22 @@ namespace OpenRA.Mods.Common.Traits
|
||||
}
|
||||
|
||||
[Conditional("SANITY_CHECKS")]
|
||||
static void CheckSanePath(List<CPos> path)
|
||||
static void CheckSanePath(IList<CPos> path)
|
||||
{
|
||||
if (path.Count == 0)
|
||||
return;
|
||||
var prev = path[0];
|
||||
for (var i = 0; i < path.Count; i++)
|
||||
foreach (var cell in path)
|
||||
{
|
||||
var d = path[i] - prev;
|
||||
var d = cell - prev;
|
||||
if (Math.Abs(d.X) > 1 || Math.Abs(d.Y) > 1)
|
||||
throw new InvalidOperationException("(PathFinder) path sanity check failed");
|
||||
prev = path[i];
|
||||
prev = cell;
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("SANITY_CHECKS")]
|
||||
static void CheckSanePath2(List<CPos> path, CPos src, CPos dest)
|
||||
static void CheckSanePath2(IList<CPos> path, CPos src, CPos dest)
|
||||
{
|
||||
if (path.Count == 0)
|
||||
return;
|
||||
@@ -272,35 +269,4 @@ namespace OpenRA.Mods.Common.Traits
|
||||
throw new InvalidOperationException("(PathFinder) sanity check failed: doesn't come from src");
|
||||
}
|
||||
}
|
||||
|
||||
public struct CellInfo
|
||||
{
|
||||
public int MinCost;
|
||||
public CPos Path;
|
||||
public bool Seen;
|
||||
|
||||
public CellInfo(int minCost, CPos path, bool seen)
|
||||
{
|
||||
MinCost = minCost;
|
||||
Path = path;
|
||||
Seen = seen;
|
||||
}
|
||||
}
|
||||
|
||||
public struct PathDistance : IComparable<PathDistance>
|
||||
{
|
||||
public readonly int EstTotal;
|
||||
public readonly CPos Location;
|
||||
|
||||
public PathDistance(int estTotal, CPos location)
|
||||
{
|
||||
EstTotal = estTotal;
|
||||
Location = location;
|
||||
}
|
||||
|
||||
public int CompareTo(PathDistance other)
|
||||
{
|
||||
return Math.Sign(EstTotal - other.EstTotal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using OpenRA;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
public sealed class PathSearch : IDisposable
|
||||
{
|
||||
public CellLayer<CellInfo> CellInfo;
|
||||
public PriorityQueue<PathDistance> Queue;
|
||||
public Func<CPos, int> Heuristic;
|
||||
public bool CheckForBlocked;
|
||||
public Actor IgnoredActor;
|
||||
public bool InReverse;
|
||||
public HashSet<CPos> Considered;
|
||||
public Player Owner { get { return self.Owner; } }
|
||||
public int MaxCost;
|
||||
|
||||
Actor self;
|
||||
MobileInfo mobileInfo;
|
||||
Func<CPos, int> customCost;
|
||||
Func<CPos, bool> customBlock;
|
||||
int laneBias = 1;
|
||||
|
||||
public PathSearch(World world, MobileInfo mobileInfo, Actor self)
|
||||
{
|
||||
this.self = self;
|
||||
CellInfo = InitCellInfo();
|
||||
this.mobileInfo = mobileInfo;
|
||||
this.self = self;
|
||||
customCost = null;
|
||||
Queue = new PriorityQueue<PathDistance>();
|
||||
Considered = new HashSet<CPos>();
|
||||
MaxCost = 0;
|
||||
}
|
||||
|
||||
public static PathSearch Search(World world, MobileInfo mi, Actor self, bool checkForBlocked)
|
||||
{
|
||||
var search = new PathSearch(world, mi, self)
|
||||
{
|
||||
CheckForBlocked = checkForBlocked
|
||||
};
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
public static PathSearch FromPoint(World world, MobileInfo mi, Actor self, CPos from, CPos target, bool checkForBlocked)
|
||||
{
|
||||
var search = new PathSearch(world, mi, self)
|
||||
{
|
||||
Heuristic = DefaultEstimator(target),
|
||||
CheckForBlocked = checkForBlocked
|
||||
};
|
||||
|
||||
search.AddInitialCell(from);
|
||||
return search;
|
||||
}
|
||||
|
||||
public static PathSearch FromPoints(World world, MobileInfo mi, Actor self, IEnumerable<CPos> froms, CPos target, bool checkForBlocked)
|
||||
{
|
||||
var search = new PathSearch(world, mi, self)
|
||||
{
|
||||
Heuristic = DefaultEstimator(target),
|
||||
CheckForBlocked = checkForBlocked
|
||||
};
|
||||
|
||||
foreach (var sl in froms)
|
||||
search.AddInitialCell(sl);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
public static Func<CPos, int> DefaultEstimator(CPos destination)
|
||||
{
|
||||
return here =>
|
||||
{
|
||||
var diag = Math.Min(Math.Abs(here.X - destination.X), Math.Abs(here.Y - destination.Y));
|
||||
var straight = Math.Abs(here.X - destination.X) + Math.Abs(here.Y - destination.Y);
|
||||
|
||||
// HACK: this relies on fp and cell-size assumptions.
|
||||
var h = (3400 * diag / 24) + 100 * (straight - (2 * diag));
|
||||
return (int)(h * 1.001);
|
||||
};
|
||||
}
|
||||
|
||||
public PathSearch Reverse()
|
||||
{
|
||||
InReverse = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathSearch WithCustomBlocker(Func<CPos, bool> customBlock)
|
||||
{
|
||||
this.customBlock = customBlock;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathSearch WithIgnoredActor(Actor b)
|
||||
{
|
||||
IgnoredActor = b;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathSearch WithHeuristic(Func<CPos, int> h)
|
||||
{
|
||||
Heuristic = h;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathSearch WithCustomCost(Func<CPos, int> w)
|
||||
{
|
||||
customCost = w;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathSearch WithoutLaneBias()
|
||||
{
|
||||
laneBias = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathSearch FromPoint(CPos from)
|
||||
{
|
||||
AddInitialCell(from);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed
|
||||
// to be reached more cheaply by a path through our parent cell which does not include the current cell.
|
||||
// For horizontal/vertical directions, the set is the three cells 'ahead'. For diagonal directions, the set
|
||||
// is the three cells ahead, plus the two cells to the side, which we cannot exclude without knowing if
|
||||
// the cell directly between them and our parent is passable.
|
||||
static CVec[][] directedNeighbors = {
|
||||
new CVec[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(-1, 0), new CVec(-1, 1) },
|
||||
new CVec[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1) },
|
||||
new CVec[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) },
|
||||
new CVec[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1) },
|
||||
CVec.Directions,
|
||||
new CVec[] { new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) },
|
||||
new CVec[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) },
|
||||
new CVec[] { new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) },
|
||||
new CVec[] { new CVec(1, -1), new CVec(1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) },
|
||||
};
|
||||
|
||||
static CVec[] GetNeighbors(CPos p, CPos prev)
|
||||
{
|
||||
var dx = p.X - prev.X;
|
||||
var dy = p.Y - prev.Y;
|
||||
var index = dy * 3 + dx + 4;
|
||||
|
||||
return directedNeighbors[index];
|
||||
}
|
||||
|
||||
public CPos Expand(World world)
|
||||
{
|
||||
var p = Queue.Pop();
|
||||
while (CellInfo[p.Location].Seen)
|
||||
{
|
||||
if (Queue.Empty)
|
||||
return p.Location;
|
||||
|
||||
p = Queue.Pop();
|
||||
}
|
||||
|
||||
var pCell = CellInfo[p.Location];
|
||||
pCell.Seen = true;
|
||||
CellInfo[p.Location] = pCell;
|
||||
|
||||
var thisCost = mobileInfo.MovementCostForCell(world, p.Location);
|
||||
|
||||
if (thisCost == int.MaxValue)
|
||||
return p.Location;
|
||||
|
||||
if (customCost != null)
|
||||
{
|
||||
var c = customCost(p.Location);
|
||||
if (c == int.MaxValue)
|
||||
return p.Location;
|
||||
}
|
||||
|
||||
// This current cell is ok; check useful immediate directions:
|
||||
Considered.Add(p.Location);
|
||||
|
||||
var directions = GetNeighbors(p.Location, pCell.Path);
|
||||
|
||||
for (var i = 0; i < directions.Length; ++i)
|
||||
{
|
||||
var d = directions[i];
|
||||
|
||||
var newHere = p.Location + d;
|
||||
|
||||
// Is this direction flat-out unusable or already seen?
|
||||
if (!world.Map.Contains(newHere))
|
||||
continue;
|
||||
|
||||
if (CellInfo[newHere].Seen)
|
||||
continue;
|
||||
|
||||
// Now we may seriously consider this direction using heuristics:
|
||||
var costHere = mobileInfo.MovementCostForCell(world, newHere);
|
||||
|
||||
if (costHere == int.MaxValue)
|
||||
continue;
|
||||
|
||||
if (!mobileInfo.CanEnterCell(world, self, newHere, IgnoredActor, CheckForBlocked ? CellConditions.TransientActors : CellConditions.None))
|
||||
continue;
|
||||
|
||||
if (customBlock != null && customBlock(newHere))
|
||||
continue;
|
||||
|
||||
var est = Heuristic(newHere);
|
||||
if (est == int.MaxValue)
|
||||
continue;
|
||||
|
||||
var cellCost = costHere;
|
||||
if (d.X * d.Y != 0)
|
||||
cellCost = (cellCost * 34) / 24;
|
||||
|
||||
var userCost = 0;
|
||||
if (customCost != null)
|
||||
{
|
||||
userCost = customCost(newHere);
|
||||
cellCost += userCost;
|
||||
}
|
||||
|
||||
// directional bonuses for smoother flow!
|
||||
if (laneBias != 0)
|
||||
{
|
||||
var ux = newHere.X + (InReverse ? 1 : 0) & 1;
|
||||
var uy = newHere.Y + (InReverse ? 1 : 0) & 1;
|
||||
|
||||
if (ux == 0 && d.Y < 0)
|
||||
cellCost += laneBias;
|
||||
else if (ux == 1 && d.Y > 0)
|
||||
cellCost += laneBias;
|
||||
|
||||
if (uy == 0 && d.X < 0)
|
||||
cellCost += laneBias;
|
||||
else if (uy == 1 && d.X > 0)
|
||||
cellCost += laneBias;
|
||||
}
|
||||
|
||||
var newCost = CellInfo[p.Location].MinCost + cellCost;
|
||||
|
||||
// Cost is even higher; next direction:
|
||||
if (newCost > CellInfo[newHere].MinCost)
|
||||
continue;
|
||||
|
||||
var hereCell = CellInfo[newHere];
|
||||
hereCell.Path = p.Location;
|
||||
hereCell.MinCost = newCost;
|
||||
CellInfo[newHere] = hereCell;
|
||||
|
||||
Queue.Add(new PathDistance(newCost + est, newHere));
|
||||
|
||||
if (newCost > MaxCost)
|
||||
MaxCost = newCost;
|
||||
|
||||
Considered.Add(newHere);
|
||||
}
|
||||
|
||||
return p.Location;
|
||||
}
|
||||
|
||||
public void AddInitialCell(CPos location)
|
||||
{
|
||||
if (!self.World.Map.Contains(location))
|
||||
return;
|
||||
|
||||
CellInfo[location] = new CellInfo(0, location, false);
|
||||
Queue.Add(new PathDistance(Heuristic(location), location));
|
||||
}
|
||||
|
||||
static readonly Queue<CellLayer<CellInfo>> CellInfoPool = new Queue<CellLayer<CellInfo>>();
|
||||
static readonly object DefaultCellInfoLayerSync = new object();
|
||||
static CellLayer<CellInfo> defaultCellInfoLayer;
|
||||
|
||||
static CellLayer<CellInfo> GetFromPool()
|
||||
{
|
||||
lock (CellInfoPool)
|
||||
return CellInfoPool.Dequeue();
|
||||
}
|
||||
|
||||
static void PutBackIntoPool(CellLayer<CellInfo> ci)
|
||||
{
|
||||
lock (CellInfoPool)
|
||||
CellInfoPool.Enqueue(ci);
|
||||
}
|
||||
|
||||
CellLayer<CellInfo> InitCellInfo()
|
||||
{
|
||||
CellLayer<CellInfo> result = null;
|
||||
var map = self.World.Map;
|
||||
var mapSize = new Size(map.MapSize.X, map.MapSize.Y);
|
||||
|
||||
// HACK: Uses a static cache so that double-ended searches (which have two PathSearch instances)
|
||||
// can implicitly share data. The PathFinder should allocate the CellInfo array and pass it
|
||||
// explicitly to the things that need to share it.
|
||||
while (CellInfoPool.Count > 0)
|
||||
{
|
||||
var cellInfo = GetFromPool();
|
||||
if (cellInfo.Size != mapSize || cellInfo.Shape != map.TileShape)
|
||||
{
|
||||
Log.Write("debug", "Discarding old pooled CellInfo of wrong size.");
|
||||
continue;
|
||||
}
|
||||
|
||||
result = cellInfo;
|
||||
break;
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
result = new CellLayer<CellInfo>(map);
|
||||
|
||||
lock (DefaultCellInfoLayerSync)
|
||||
{
|
||||
if (defaultCellInfoLayer == null ||
|
||||
defaultCellInfoLayer.Size != mapSize ||
|
||||
defaultCellInfoLayer.Shape != map.TileShape)
|
||||
{
|
||||
defaultCellInfoLayer = new CellLayer<CellInfo>(map);
|
||||
for (var v = 0; v < mapSize.Height; v++)
|
||||
for (var u = 0; u < mapSize.Width; u++)
|
||||
defaultCellInfoLayer[new MPos(u, v)] = new CellInfo(int.MaxValue, new MPos(u, v).ToCPos(map), false);
|
||||
}
|
||||
|
||||
result.CopyValuesFrom(defaultCellInfoLayer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool disposed;
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
disposed = true;
|
||||
|
||||
PutBackIntoPool(CellInfo);
|
||||
CellInfo = null;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~PathSearch() { Dispose(); }
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using OpenRA;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
Reference in New Issue
Block a user