- 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:
David Jiménez
2015-02-20 08:59:07 +01:00
parent 8659a3e71e
commit 54ae572303
28 changed files with 1727 additions and 667 deletions

View File

@@ -33,25 +33,21 @@ namespace OpenRA
T Trait<T>();
IEnumerable<T> TraitsImplementing<T>();
T TraitInfo<T>();
IEnumerable<IRenderable> Render(WorldRenderer wr);
}
public class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding, IEquatable<Actor>, IActor
{
public ActorInfo Info { get; private set; }
public readonly ActorInfo Info;
ActorInfo IActor.Info { get { return this.Info; } }
public readonly World World;
IWorld IActor.World { get { return World; } }
IWorld IActor.World
{
get { return World; }
}
public readonly uint ActorID;
uint IActor.ActorID { get { return this.ActorID; } }
public uint ActorID { get; private set; }
[Sync]
public Player Owner { get; set; }
[Sync] public Player Owner { get; set; }
public bool IsInWorld { get; internal set; }
public bool Destroyed { get; private set; }
@@ -224,11 +220,6 @@ namespace OpenRA
return World.TraitDict.WithInterface<T>(this);
}
public T TraitInfo<T>()
{
return Info.Traits.Get<T>();
}
public bool HasTrait<T>()
{
return World.TraitDict.Contains<T>(this);

View File

@@ -24,7 +24,7 @@ namespace OpenRA
readonly T[] entries;
public CellLayer(Map map)
public CellLayer(IMap map)
: this(map.TileShape, new Size(map.MapSize.X, map.MapSize.Y)) { }
public CellLayer(TileShape shape, Size size)
@@ -45,6 +45,21 @@ namespace OpenRA
Array.Copy(anotherLayer.entries, entries, entries.Length);
}
public static CellLayer<T> CreateInstance(Func<MPos, T> initialCellValueFactory, Size size, TileShape tileShape)
{
var cellLayer = new CellLayer<T>(tileShape, size);
for (var v = 0; v < size.Height; v++)
{
for (var u = 0; u < size.Width; u++)
{
var mpos = new MPos(u, v);
cellLayer[mpos] = initialCellValueFactory(mpos);
}
}
return cellLayer;
}
// Resolve an array index from cell coordinates
int Index(CPos cell)
{

View File

@@ -132,12 +132,10 @@ namespace OpenRA
get { return TileShape; }
}
[FieldLoader.Ignore]
public readonly WVec[] SubCellOffsets;
[FieldLoader.Ignore] public readonly WVec[] SubCellOffsets;
public readonly SubCell DefaultSubCell;
public readonly SubCell LastSubCell;
[FieldLoader.Ignore]
public IFolder Container;
[FieldLoader.Ignore] public IFolder Container;
public string Path { get; private set; }
// Yaml map data
@@ -157,8 +155,7 @@ namespace OpenRA
public WVec OffsetOfSubCell(SubCell subCell) { return SubCellOffsets[(int)subCell]; }
[FieldLoader.LoadUsing("LoadOptions")]
public MapOptions Options;
[FieldLoader.LoadUsing("LoadOptions")] public MapOptions Options;
static object LoadOptions(MiniYaml y)
{
@@ -170,8 +167,7 @@ namespace OpenRA
return options;
}
[FieldLoader.LoadUsing("LoadVideos")]
public MapVideos Videos;
[FieldLoader.LoadUsing("LoadVideos")] public MapVideos Videos;
static object LoadVideos(MiniYaml y)
{
@@ -183,37 +179,26 @@ namespace OpenRA
return videos;
}
[FieldLoader.Ignore]
public Lazy<Dictionary<string, ActorReference>> Actors;
[FieldLoader.Ignore] public Lazy<Dictionary<string, ActorReference>> Actors;
public int PlayerCount { get { return Players.Count(p => p.Value.Playable); } }
public Rectangle Bounds;
// Yaml map data
[FieldLoader.Ignore]
public Dictionary<string, PlayerReference> Players = new Dictionary<string, PlayerReference>();
[FieldLoader.Ignore]
public Lazy<List<SmudgeReference>> Smudges;
[FieldLoader.Ignore] public Dictionary<string, PlayerReference> Players = new Dictionary<string, PlayerReference>();
[FieldLoader.Ignore] public Lazy<List<SmudgeReference>> Smudges;
[FieldLoader.Ignore]
public List<MiniYamlNode> RuleDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore]
public List<MiniYamlNode> SequenceDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore]
public List<MiniYamlNode> VoxelSequenceDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore]
public List<MiniYamlNode> WeaponDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore]
public List<MiniYamlNode> VoiceDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore]
public List<MiniYamlNode> NotificationDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore]
public List<MiniYamlNode> TranslationDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore] public List<MiniYamlNode> RuleDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore] public List<MiniYamlNode> SequenceDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore] public List<MiniYamlNode> VoxelSequenceDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore] public List<MiniYamlNode> WeaponDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore] public List<MiniYamlNode> VoiceDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore] public List<MiniYamlNode> NotificationDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore] public List<MiniYamlNode> TranslationDefinitions = new List<MiniYamlNode>();
// Binary map data
[FieldLoader.Ignore]
public byte TileFormat = 2;
[FieldLoader.Ignore] public byte TileFormat = 2;
public int2 MapSize;
@@ -223,25 +208,18 @@ namespace OpenRA
set { MapSize = value; }
}
[FieldLoader.Ignore]
public Lazy<CellLayer<TerrainTile>> MapTiles;
[FieldLoader.Ignore]
public Lazy<CellLayer<ResourceTile>> MapResources;
[FieldLoader.Ignore]
public Lazy<CellLayer<byte>> MapHeight;
[FieldLoader.Ignore] public Lazy<CellLayer<TerrainTile>> MapTiles;
[FieldLoader.Ignore] public Lazy<CellLayer<ResourceTile>> MapResources;
[FieldLoader.Ignore] public Lazy<CellLayer<byte>> MapHeight;
[FieldLoader.Ignore]
public CellLayer<byte> CustomTerrain;
[FieldLoader.Ignore] public CellLayer<byte> CustomTerrain;
[FieldLoader.Ignore]
Lazy<TileSet> cachedTileSet;
[FieldLoader.Ignore]
Lazy<Ruleset> rules;
[FieldLoader.Ignore] Lazy<TileSet> cachedTileSet;
[FieldLoader.Ignore] Lazy<Ruleset> rules;
public Ruleset Rules { get { return rules != null ? rules.Value : null; } }
public SequenceProvider SequenceProvider { get { return Rules.Sequences[Tileset]; } }
[FieldLoader.Ignore]
public CellRegion Cells;
[FieldLoader.Ignore] public CellRegion Cells;
public static Map FromTileset(TileSet tileset)
{

View File

@@ -13,15 +13,28 @@ using System.Collections.Generic;
namespace OpenRA.Primitives
{
public class PriorityQueue<T>
where T : IComparable<T>
public interface IPriorityQueue<T>
{
List<T[]> items = new List<T[]>();
void Add(T item);
bool Empty { get; }
T Peek();
T Pop();
}
public class PriorityQueue<T> : IPriorityQueue<T>
{
readonly List<T[]> items;
readonly IComparer<T> comparer;
int level, index;
public PriorityQueue()
public PriorityQueue() : this(Comparer<T>.Default)
{
items.Add(new T[1]);
}
public PriorityQueue(IComparer<T> comparer)
{
items = new List<T[]> { new T[1] };
this.comparer = comparer;
}
public void Add(T item)
@@ -29,7 +42,7 @@ namespace OpenRA.Primitives
var addLevel = level;
var addIndex = index;
while (addLevel >= 1 && Above(addLevel, addIndex).CompareTo(item) > 0)
while (addLevel >= 1 && comparer.Compare(Above(addLevel, addIndex), item) > 0)
{
items[addLevel][addIndex] = Above(addLevel, addIndex);
--addLevel;
@@ -88,10 +101,10 @@ namespace OpenRA.Primitives
}
if (downLevel <= level && downIndex < index - 1 &&
At(downLevel, downIndex).CompareTo(At(downLevel, downIndex + 1)) >= 0)
comparer.Compare(At(downLevel, downIndex), At(downLevel, downIndex + 1)) >= 0)
++downIndex;
if (val.CompareTo(At(downLevel, downIndex)) <= 0)
if (comparer.Compare(val, At(downLevel, downIndex)) <= 0)
{
items[intoLevel][intoIndex] = val;
return;

View File

@@ -121,13 +121,15 @@ namespace OpenRA
RenderPlayer = LocalPlayer;
}
public Actor WorldActor { get; private set; }
public readonly Actor WorldActor;
IActor IWorld.WorldActor { get { return WorldActor; } }
public Map Map { get; private set; }
public readonly Map Map;
IMap IWorld.Map { get { return Map; } }
public TileSet TileSet { get; private set; }
public readonly TileSet TileSet;
TileSet IWorld.TileSet { get { return TileSet; } }
public readonly ActorMap ActorMap;
public readonly ScreenMap ScreenMap;
public readonly WorldType Type;

View File

@@ -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;

View File

@@ -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))

View File

@@ -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);

View File

@@ -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" />

View 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();
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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 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>();
var mi = self.Info.Traits.Get<IMobileInfo>();
// If a water-land transition is required, bail early
var domainIndex = self.World.WorldActor.TraitOrDefault<DomainIndex>();
var domainIndex = world.WorldActor.TraitOrDefault<DomainIndex>();
if (domainIndex != null)
{
var passable = mi.GetMovementClass(world.TileSet);
if (!domainIndex.IsPassable(from, target, (uint)passable))
if (!domainIndex.IsPassable(source, 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);
PathSearch.FromPoint(world, mi, self, target, source, true),
PathSearch.FromPoint(world, mi, self, source, target, true).Reverse());
CheckSanePath2(pb, from, target);
CheckSanePath2(pb, source, 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);
}
return pb;
}
public List<CPos> FindUnitPathToRange(CPos src, SubCell srcSub, WPos target, WRange range, Actor self)
{
using (new PerfSample("Pathfinder"))
public List<CPos> FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WRange range, IActor self)
{
var mi = self.Info.Traits.Get<MobileInfo>();
var targetCell = self.World.Map.CellContaining(target);
var targetCell = world.Map.CellContaining(target);
var rangeSquared = range.Range * range.Range;
// Correct for SubCell offset
target -= self.World.Map.OffsetOfSubCell(srcSub);
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, self, t));
.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 = self.World.WorldActor.TraitOrDefault<DomainIndex>();
var domainIndex = world.WorldActor.TraitOrDefault<DomainIndex>();
if (domainIndex != null)
{
var passable = mi.GetMovementClass(world.TileSet);
tilesInRange = new List<CPos>(tilesInRange.Where(t => domainIndex.IsPassable(src, t, (uint)passable)));
tilesInRange = new List<CPos>(tilesInRange.Where(t => domainIndex.IsPassable(source, t, (uint)passable)));
if (!tilesInRange.Any())
return EmptyPath;
}
var path = FindBidiPath(
PathSearch.FromPoints(world, mi, self, tilesInRange, src, true),
PathSearch.FromPoint(world, mi, self, src, targetCell, true).Reverse());
PathSearch.FromPoints(world, mi, self, tilesInRange, source, true),
PathSearch.FromPoint(world, mi, self, source, targetCell, true).Reverse());
return path;
}
}
public List<CPos> FindPath(PathSearch search)
{
using (new PerfSample("Pathfinder"))
{
using (search)
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.Queue.Empty)
while (!search.OpenQueue.Empty)
{
var p = search.Expand(world);
if (search.Heuristic(p) == 0)
var p = search.Expand();
if (search.IsTarget(p))
{
path = MakePath(search.CellInfo, p);
path = MakePath(search.Graph, 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 (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;
}
}
static List<CPos> MakePath(CellLayer<CellInfo> cellInfo, CPos destination)
{
var ret = new List<CPos>();
var pathNode = destination;
while (cellInfo[pathNode].Path != pathNode)
{
ret.Add(pathNode);
pathNode = cellInfo[pathNode].Path;
}
ret.Add(pathNode);
CheckSanePath(ret);
return ret;
}
// Searches from both ends toward each other
public List<CPos> FindBidiPath(PathSearch fromSrc, PathSearch fromDest)
{
using (new PerfSample("Pathfinder"))
{
using (fromSrc)
using (fromDest)
// 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.Queue.Empty && !fromDest.Queue.Empty)
var dbg = world.WorldActor.TraitOrDefault<PathfinderDebugOverlay>();
if (dbg != null && dbg.Visible)
{
/* make some progress on the first search */
var p = fromSrc.Expand(world);
fromSrc.Debug = true;
fromDest.Debug = true;
}
if (fromDest.CellInfo[p].Seen &&
fromDest.CellInfo[p].MinCost < float.PositiveInfinity)
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)
{
path = MakeBidiPath(fromSrc, fromDest, p);
break;
}
/* make some progress on the second search */
var q = fromDest.Expand(world);
// make some progress on the second search
var q = fromDest.Expand();
if (fromSrc.CellInfo[q].Seen &&
fromSrc.CellInfo[q].MinCost < float.PositiveInfinity)
if (fromSrc.Graph[q].Status == CellStatus.Closed &&
fromSrc.Graph[q].CostSoFar < int.MaxValue)
{
path = MakeBidiPath(fromSrc, fromDest, q);
break;
}
}
var dbg = world.WorldActor.TraitOrDefault<PathfinderDebugOverlay>();
if (dbg != null)
if (dbg != null && dbg.Visible)
{
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);
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;
}
// 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].PreviousPos != currentNode)
{
ret.Add(currentNode);
currentNode = cellInfo[currentNode].PreviousPos;
}
static List<CPos> MakeBidiPath(PathSearch a, PathSearch b, CPos p)
ret.Add(currentNode);
CheckSanePath(ret);
return ret;
}
static List<CPos> MakeBidiPath(IPathSearch a, IPathSearch b, CPos confluenceNode)
{
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);
}
}
}

View File

@@ -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(); }
}
}

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using OpenRA;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;

8
OpenRA.Test/App.config Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="mods/common"></probing>
</assemblyBinding>
</runtime>
</configuration>

206
OpenRA.Test/Fakes.cs Normal file
View File

@@ -0,0 +1,206 @@
#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.Linq;
using System.Text;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Test
{
public class FakeActor : IActor
{
IWorld world;
public ActorInfo Info
{
get { throw new NotImplementedException("No need to implement this yet"); }
}
public IWorld World
{
get { return world; }
}
public uint ActorID
{
get { return 1; }
}
public Player Owner
{
get { return null; }
set { }
}
public T TraitOrDefault<T>()
{
return default(T);
}
public T Trait<T>()
{
return default(T);
}
public IEnumerable<T> TraitsImplementing<T>()
{
throw new NotImplementedException("No need to implement this yet");
}
public T TraitInfo<T>()
{
return default(T);
}
public IEnumerable<Graphics.IRenderable> Render(Graphics.WorldRenderer wr)
{
throw new NotImplementedException("No need to implement this yet");
}
public FakeActor(IWorld world)
{
// TODO: Complete member initialization
this.world = world;
}
public FakeActor()
{
// TODO: Complete member initialization
}
}
public class FakeWorld : IWorld
{
FakeActor worldactor;
IMap map;
public IActor WorldActor
{
get { return worldactor; }
}
public int WorldTick
{
get { return 50; }
}
public IMap Map
{
get { return map; }
}
public TileSet TileSet
{
get { throw new NotImplementedException("No need to implement this yet"); }
}
public FakeWorld(IMap map)
{
// TODO: Complete member initialization
this.map = map;
}
public FakeWorld(IMap map, FakeActor worldactor)
{
// TODO: Complete member initialization
this.map = map;
this.worldactor = worldactor;
}
}
public class FakeMobileInfo : IMobileInfo
{
Func<CPos, bool> conditions;
public int MovementCostForCell(World world, CPos cell)
{
if (conditions(cell))
return 125;
return int.MaxValue;
}
public bool CanEnterCell(World world, Actor self, CPos cell, out int movementCost, Actor ignoreActor = null, CellConditions check = CellConditions.All)
{
movementCost = MovementCostForCell(world, cell);
return conditions(cell);
}
public int GetMovementClass(TileSet tileset)
{
throw new NotImplementedException("No need to implement this yet");
}
public FakeMobileInfo(Func<CPos, bool> conditions)
{
this.conditions = conditions;
}
public bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All)
{
return conditions(cell);
}
public object Create(ActorInitializer init)
{
throw new NotImplementedException();
}
}
public class FakeMap : IMap
{
int width;
int height;
public FakeMap(int width, int height)
{
// TODO: Complete member initialization
this.width = width;
this.height = height;
}
public TileShape TileShape
{
get { return TileShape.Rectangle; }
}
public int2 MapSize
{
get { return new int2(width, height); }
set { throw new NotImplementedException("No need to implement this yet"); }
}
public bool Contains(CPos cell)
{
return cell.X >= 0 && cell.X < width && cell.Y >= 0 && cell.Y < height;
}
public CPos CellContaining(WPos pos)
{
throw new NotImplementedException("No need to implement this yet");
}
public WVec OffsetOfSubCell(Traits.SubCell subCell)
{
throw new NotImplementedException("No need to implement this yet");
}
public IEnumerable<CPos> FindTilesInCircle(CPos center, int maxRange)
{
throw new NotImplementedException("No need to implement this yet");
}
public WPos CenterOfCell(CPos cell)
{
throw new NotImplementedException("No need to implement this yet");
}
}
}

View File

@@ -27,19 +27,30 @@
<HintPath>..\thirdparty\Eluant.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Moq, Version=4.2.1502.911, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\thirdparty\Moq.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="nunit.framework, Version=2.6.3.13283, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\thirdparty\nunit.framework.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="OpenRA.Mods.Common">
<HintPath>..\OpenRA.Mods.Common\bin\Debug\OpenRA.Mods.Common.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Drawing" />
</ItemGroup>
<ItemGroup>
<Compile Include="Fakes.cs" />
<Compile Include="OpenRA.Game\MiniYamlTest.cs" />
<Compile Include="OpenRA.Game\ActorInfoTest.cs" />
<Compile Include="OpenRA.Game\OrderTest.cs" />
<Compile Include="OpenRA.Game\PlatformTest.cs" />
<Compile Include="PathfinderTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj">
@@ -51,6 +62,9 @@
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@@ -0,0 +1,273 @@
#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.Diagnostics;
using System.Drawing;
using System.Linq;
using Moq;
using NUnit.Framework;
using OpenRA;
using OpenRA.Mods.Common.Pathfinder;
using OpenRA.Mods.Common.Traits;
using OpenRA.Test;
namespace PathfinderTests
{
[TestFixture]
public class PathfinderTests
{
const int Width = 128;
const int Height = 128;
IWorld world;
IMap map;
IActor actor;
[SetUp]
public void Setup()
{
map = new FakeMap(Width, Height);
var worldactor = new FakeActor();
world = new FakeWorld(map, worldactor);
actor = new FakeActor(world);
}
IMap BuildFakeMap(int mapWidth, int mapHeight)
{
var map = new Mock<IMap>();
map.SetupGet(m => m.TileShape).Returns(TileShape.Rectangle);
map.Setup(m => m.MapSize).Returns(new int2(mapWidth, mapHeight));
map.Setup(m => m.Contains(It.Is<CPos>(pos => pos.X >= 0 && pos.X < mapWidth && pos.Y >= 0 && pos.Y < mapHeight))).Returns(true);
return map.Object;
}
IWorld BuildFakeWorld(IMap map)
{
var world = new Mock<IWorld>();
world.SetupGet(m => m.Map).Returns(map);
world.SetupGet(m => m.WorldActor).Returns(new Mock<IActor>().Object);
return world.Object;
}
static bool IsValidPos(CPos pos, int mapWidth, int mapHeight)
{
return pos.X >= 0 && pos.X < mapWidth && pos.Y >= 0 && pos.Y < mapHeight;
}
[Test]
[Ignore]
public void FindPathOnRoughTerrainTest()
{
// Arrange
// Create the MobileInfo Mock. Playing with this can help to
// check the different paths and points a unit can walk into
var mi = new FakeMobileInfo(pos => !(!IsValidPos(pos, Width, Height) ||
(pos.X == 50 && pos.Y < 100) ||
(pos.X == 100 && pos.Y > 50)));
var from = new CPos(1, 1);
var target = new CPos(125, 75);
IPathSearch search;
Stopwatch stopwatch;
List<CPos> path1 = null;
List<CPos> path2 = null;
List<CPos> path3 = null;
List<CPos> path4 = null;
List<CPos> path5 = null;
List<CPos> path6 = null;
List<CPos> path7 = null;
List<CPos> path8 = null;
var pathfinder = new PathFinder(world);
// Act
stopwatch = new Stopwatch();
foreach (var a in Enumerable.Range(1, 50))
{
search = PathSearch.FromPoint(world, mi, actor, from, target, true);
stopwatch.Start();
path5 = pathfinder.FindPath(search);
stopwatch.Stop();
search = PathSearch.FromPoint(world, mi, actor, new CPos(0, 0), new CPos(51, 100), true);
stopwatch.Start();
path6 = pathfinder.FindPath(search);
stopwatch.Stop();
search = PathSearch.FromPoint(world, mi, actor, new CPos(0, 0), new CPos(49, 50), true);
stopwatch.Start();
path7 = pathfinder.FindPath(search);
stopwatch.Stop();
search = PathSearch.FromPoint(world, mi, actor, new CPos(127, 0), new CPos(50, 101), true);
stopwatch.Start();
path8 = pathfinder.FindPath(search);
}
Console.WriteLine("I took " + stopwatch.ElapsedMilliseconds + " ms with new pathfinder");
IPathSearch search2;
stopwatch = new Stopwatch();
foreach (var a in Enumerable.Range(1, 50))
{
search = PathSearch.FromPoint(world, mi, actor, from, target, true);
search2 = PathSearch.FromPoint(world, mi, actor, target, from, true).Reverse();
stopwatch.Start();
path5 = pathfinder.FindBidiPath(search, search2);
stopwatch.Stop();
search = PathSearch.FromPoint(world, mi, actor, new CPos(0, 0), new CPos(51, 100), true);
search2 = PathSearch.FromPoint(world, mi, actor, new CPos(51, 100), new CPos(0, 0), true).Reverse();
stopwatch.Start();
path6 = pathfinder.FindBidiPath(search, search2);
stopwatch.Stop();
search = PathSearch.FromPoint(world, mi, actor, new CPos(0, 0), new CPos(49, 50), true);
search2 = PathSearch.FromPoint(world, mi, actor, new CPos(49, 50), new CPos(0, 0), true).Reverse();
stopwatch.Start();
path7 = pathfinder.FindBidiPath(search, search2);
stopwatch.Stop();
search = PathSearch.FromPoint(world, mi, actor, new CPos(127, 0), new CPos(50, 101), true);
search2 = PathSearch.FromPoint(world, mi, actor, new CPos(50, 101), new CPos(127, 0), true).Reverse();
stopwatch.Start();
path8 = pathfinder.FindBidiPath(search, search2);
}
Console.WriteLine("I took " + stopwatch.ElapsedMilliseconds + " ms with new FindBidipathfinder");
}
/// <summary>
/// We can't rely on floating point math to be deterministic across all runtimes.
/// The cases that use this will need to be changed to use integer math
/// </summary>
public const double Sqrt2 = 1.414;
static int Est1(CPos here, CPos destination)
{
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);
// Min cost to arrive from once cell to an adjacent one
// (125 according to tests)
const int D = 100;
// According to the information link, this is the shape of the function.
// We just extract factors to simplify.
var h = D * straight + (D * Sqrt2 - 2 * D) * diag;
return (int)(h * 1.001);
}
static int Est2(CPos here, CPos destination)
{
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 = (100 * diag * Sqrt2) + 100 * (straight - (2 * diag));
return (int)(h * 1.001);
}
/// <summary>
/// Tests the refactor of the default heuristic for pathFinder
/// </summary>
[Test]
public void EstimatorsTest()
{
Assert.AreEqual(Est1(new CPos(0, 0), new CPos(20, 30)), Est2(new CPos(0, 0), new CPos(20, 30)));
}
[Test]
public void Remove1000StoredPaths()
{
var world = new Mock<IWorld>();
world.SetupGet(m => m.WorldTick).Returns(50);
var pathCacheStorage = new PathCacheStorage(world.Object);
var stopwatch = new Stopwatch();
for (var i = 0; i < 1100; i++)
{
if (i == 100)
{
// Let's make the world tick further so we can trigger the removals
// when storing more stuff
world.SetupGet(m => m.WorldTick).Returns(110);
stopwatch.Start();
}
pathCacheStorage.Store(i.ToString(), new List<CPos>());
if (i == 100)
{
stopwatch.Stop();
Console.WriteLine("I took " + stopwatch.ElapsedMilliseconds + " ms to remove 1000 stored paths");
}
}
}
/// <summary>
/// Test for the future feature of path smoothing for Pathfinder
/// </summary>
[Test]
public void RayCastingTest()
{
// Arrange
var sut = new RayCaster();
CPos source = new CPos(1, 3);
CPos target = new CPos(3, 0);
// Act
var valid = sut.RayCast(source, target);
// Assert
}
}
public class RayCaster
{
// Algorithm obtained in http://playtechs.blogspot.co.uk/2007/03/raytracing-on-grid.html
public IEnumerable<CPos> RayCast(CPos source, CPos target)
{
int dx = Math.Abs(target.X - source.X);
int dy = Math.Abs(target.Y - source.Y);
int x = source.X;
int y = source.Y;
int x_inc = (target.X > source.X) ? 1 : -1;
int y_inc = (target.Y > source.Y) ? 1 : -1;
int error = dx - dy;
dx *= 2;
dy *= 2;
for (int n = 1 + dx + dy; n > 0; --n)
{
yield return new CPos(x, y);
if (error > 0)
{
x += x_inc;
error -= dy;
}
else
{
y += y_inc;
error += dx;
}
}
}
public bool RayClear()
{
return true;
}
}
}

View File

@@ -105,3 +105,11 @@ if (!(Test-Path "windows/soft_oal.dll"))
cp OpenAL-Soft.1.16.0/bin/Win32/soft_oal.dll windows/soft_oal.dll
rmdir OpenAL-Soft.1.16.0 -Recurse
}
if (!(Test-Path "Moq.dll"))
{
echo "Fetching Moq from NuGet."
./nuget.exe install Moq -Version 4.2.1502.0911
cp Moq.4.2.1502.0911/lib/net40/Moq.dll .
rmdir Moq.4.2.1502.0911 -Recurse
}

View File

@@ -58,3 +58,10 @@ if [ ! -f Mono.Nat.dll ]; then
cp ./Mono.Nat.1.2.21.0/lib/net40/Mono.Nat.dll .
rm -rf Mono.Nat.1.2.21.0
fi
if [ ! -f Moq.dll ]; then
echo "Fetching Moq from NuGet."
nuget install Moq -Version 4.2.1502.0911
cp ./Moq.4.2.1502.0911/lib/net40/Moq.dll .
rm -rf Moq.4.2.1502.0911
fi