Merge pull request #7430 from Rydra/upstream/pf-optimized
[Discussion PR] Complete refactor of Pathfinder
This commit is contained in:
@@ -22,14 +22,32 @@ using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding, IEquatable<Actor>
|
||||
public interface IActor
|
||||
{
|
||||
ActorInfo Info { get; }
|
||||
IWorld World { get; }
|
||||
uint ActorID { get; }
|
||||
Player Owner { get; set; }
|
||||
|
||||
T TraitOrDefault<T>();
|
||||
T Trait<T>();
|
||||
IEnumerable<T> TraitsImplementing<T>();
|
||||
|
||||
IEnumerable<IRenderable> Render(WorldRenderer wr);
|
||||
}
|
||||
|
||||
public class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding, IEquatable<Actor>, IActor
|
||||
{
|
||||
public readonly ActorInfo Info;
|
||||
public readonly World World;
|
||||
public readonly uint ActorID;
|
||||
ActorInfo IActor.Info { get { return this.Info; } }
|
||||
|
||||
[Sync]
|
||||
public Player Owner;
|
||||
public readonly World World;
|
||||
IWorld IActor.World { get { return World; } }
|
||||
|
||||
public readonly uint ActorID;
|
||||
uint IActor.ActorID { get { return this.ActorID; } }
|
||||
|
||||
[Sync] public Player Owner { get; set; }
|
||||
|
||||
public bool IsInWorld { get; internal set; }
|
||||
public bool Destroyed { get; private set; }
|
||||
@@ -235,7 +253,7 @@ namespace OpenRA
|
||||
{
|
||||
World.AddFrameEndTask(w =>
|
||||
{
|
||||
if (this.Destroyed)
|
||||
if (Destroyed)
|
||||
return;
|
||||
|
||||
var oldOwner = Owner;
|
||||
|
||||
19
OpenRA.Game/CacheStorage.cs
Normal file
19
OpenRA.Game/CacheStorage.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
#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
|
||||
{
|
||||
public interface ICacheStorage<T>
|
||||
{
|
||||
void Remove(string key);
|
||||
void Store(string key, T data);
|
||||
T Retrieve(string key);
|
||||
}
|
||||
}
|
||||
25
OpenRA.Game/LogProxy.cs
Normal file
25
OpenRA.Game/LogProxy.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
#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
|
||||
{
|
||||
public interface ILog
|
||||
{
|
||||
void Write(string channel, string format, params object[] args);
|
||||
}
|
||||
|
||||
public class LogProxy : ILog
|
||||
{
|
||||
public void Write(string channel, string format, params object[] args)
|
||||
{
|
||||
Log.Write(channel, format, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -111,12 +111,28 @@ namespace OpenRA
|
||||
MissionSelector = 4
|
||||
}
|
||||
|
||||
public class Map
|
||||
public interface IMap
|
||||
{
|
||||
TileShape TileShape { get; }
|
||||
|
||||
int2 MapSize { get; set; }
|
||||
bool Contains(CPos cell);
|
||||
CPos CellContaining(WPos pos);
|
||||
WVec OffsetOfSubCell(SubCell subCell);
|
||||
IEnumerable<CPos> FindTilesInCircle(CPos center, int maxRange);
|
||||
WPos CenterOfCell(CPos cell);
|
||||
}
|
||||
|
||||
public class Map : IMap
|
||||
{
|
||||
public const int MaxTilesInCircleRange = 50;
|
||||
public readonly TileShape TileShape;
|
||||
[FieldLoader.Ignore]
|
||||
public readonly WVec[] SubCellOffsets;
|
||||
TileShape IMap.TileShape
|
||||
{
|
||||
get { return TileShape; }
|
||||
}
|
||||
|
||||
[FieldLoader.Ignore] public readonly WVec[] SubCellOffsets;
|
||||
public readonly SubCell DefaultSubCell;
|
||||
public readonly SubCell LastSubCell;
|
||||
[FieldLoader.Ignore] public IFolder Container;
|
||||
@@ -139,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)
|
||||
{
|
||||
@@ -152,8 +167,7 @@ namespace OpenRA
|
||||
return options;
|
||||
}
|
||||
|
||||
[FieldLoader.LoadUsing("LoadVideos")]
|
||||
public MapVideos Videos;
|
||||
[FieldLoader.LoadUsing("LoadVideos")] public MapVideos Videos;
|
||||
|
||||
static object LoadVideos(MiniYaml y)
|
||||
{
|
||||
@@ -188,6 +202,12 @@ namespace OpenRA
|
||||
|
||||
public int2 MapSize;
|
||||
|
||||
int2 IMap.MapSize
|
||||
{
|
||||
get { return MapSize; }
|
||||
set { MapSize = value; }
|
||||
}
|
||||
|
||||
[FieldLoader.Ignore] public Lazy<CellLayer<TerrainTile>> MapTiles;
|
||||
[FieldLoader.Ignore] public Lazy<CellLayer<ResourceTile>> MapResources;
|
||||
[FieldLoader.Ignore] public Lazy<CellLayer<byte>> MapHeight;
|
||||
|
||||
@@ -84,6 +84,8 @@
|
||||
<Compile Include="Activities\Activity.cs" />
|
||||
<Compile Include="Activities\CallFunc.cs" />
|
||||
<Compile Include="Actor.cs" />
|
||||
<Compile Include="CacheStorage.cs" />
|
||||
<Compile Include="LogProxy.cs" />
|
||||
<Compile Include="MPos.cs" />
|
||||
<Compile Include="GameRules\Warhead.cs" />
|
||||
<Compile Include="Graphics\QuadRenderer.cs" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,7 +24,15 @@ namespace OpenRA
|
||||
{
|
||||
public enum WorldType { Regular, Shellmap }
|
||||
|
||||
public class World
|
||||
public interface IWorld
|
||||
{
|
||||
IActor WorldActor { get; }
|
||||
int WorldTick { get; }
|
||||
IMap Map { get; }
|
||||
TileSet TileSet { get; }
|
||||
}
|
||||
|
||||
public class World : IWorld
|
||||
{
|
||||
class ActorIDComparer : IComparer<Actor>
|
||||
{
|
||||
@@ -114,8 +122,14 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public readonly Actor WorldActor;
|
||||
IActor IWorld.WorldActor { get { return WorldActor; } }
|
||||
|
||||
public readonly Map Map;
|
||||
IMap IWorld.Map { get { return Map; } }
|
||||
|
||||
public readonly TileSet TileSet;
|
||||
TileSet IWorld.TileSet { get { return TileSet; } }
|
||||
|
||||
public readonly ActorMap ActorMap;
|
||||
public readonly ScreenMap ScreenMap;
|
||||
public readonly WorldType Type;
|
||||
|
||||
@@ -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,8 +29,16 @@ namespace OpenRA.Mods.Common.Traits
|
||||
All = TransientActors | BlockedByMovers
|
||||
}
|
||||
|
||||
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>
|
||||
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.")]
|
||||
@@ -165,11 +173,61 @@ namespace OpenRA.Mods.Common.Traits
|
||||
return true;
|
||||
}
|
||||
|
||||
public int TileSetMovementHash(TileSet tileSet)
|
||||
{
|
||||
var terrainInfos = TilesetTerrainInfo[tileSet];
|
||||
|
||||
// Compute and return the hash using aggregate
|
||||
return terrainInfos.Aggregate(terrainInfos.Length,
|
||||
(current, terrainInfo) => unchecked(current * 31 + terrainInfo.Cost));
|
||||
}
|
||||
|
||||
public bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All)
|
||||
{
|
||||
if (MovementCostForCell(world, cell) == int.MaxValue)
|
||||
return false;
|
||||
|
||||
return CanMoveFreelyInto(world, self, cell, ignoreActor, check);
|
||||
}
|
||||
|
||||
// Determines whether the actor is blocked by other Actors
|
||||
public bool CanMoveFreelyInto(World world, Actor self, CPos cell, Actor ignoreActor, CellConditions check)
|
||||
{
|
||||
if (SharesCell && world.ActorMap.HasFreeSubCell(cell))
|
||||
return true;
|
||||
|
||||
if (check.HasFlag(CellConditions.TransientActors))
|
||||
{
|
||||
var canIgnoreMovingAllies = self != null && !check.HasFlag(CellConditions.BlockedByMovers);
|
||||
var needsCellExclusively = self == null || Crushes == null || !Crushes.Any();
|
||||
foreach (var a in world.ActorMap.GetUnitsAt(cell))
|
||||
{
|
||||
if (a == ignoreActor)
|
||||
continue;
|
||||
|
||||
// Neutral/enemy units are blockers. Allied units that are moving are not blockers.
|
||||
if (canIgnoreMovingAllies && self.Owner.Stances[a.Owner] == Stance.Ally && IsMovingInMyDirection(self, a)) continue;
|
||||
|
||||
// Non-sharable unit can enter a cell with shareable units only if it can crush all of them.
|
||||
if (needsCellExclusively)
|
||||
return false;
|
||||
var crushables = a.TraitsImplementing<ICrushable>();
|
||||
if (!crushables.Any())
|
||||
return false;
|
||||
foreach (var crushable in crushables)
|
||||
if (!crushable.CrushableBy(Crushes, self.Owner))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CanEnterCell(World world, Actor self, CPos cell, out int movementCost, Actor ignoreActor = null, CellConditions check = CellConditions.All)
|
||||
{
|
||||
if ((movementCost = MovementCostForCell(world, cell)) == int.MaxValue)
|
||||
return false;
|
||||
|
||||
if (SharesCell && world.ActorMap.HasFreeSubCell(cell))
|
||||
return true;
|
||||
|
||||
@@ -506,6 +564,11 @@ namespace OpenRA.Mods.Common.Traits
|
||||
return Info.CanEnterCell(self.World, self, cell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers);
|
||||
}
|
||||
|
||||
public bool CanMoveFreelyInto(CPos cell, Actor ignoreActor = null, bool checkTransientActors = true)
|
||||
{
|
||||
return Info.CanMoveFreelyInto(self.World, self, cell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers);
|
||||
}
|
||||
|
||||
public void EnteringCell(Actor self)
|
||||
{
|
||||
var crushables = self.World.ActorMap.GetUnitsAt(ToCell).Where(a => a != self)
|
||||
|
||||
@@ -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;
|
||||
|
||||
8
OpenRA.Test/App.config
Normal file
8
OpenRA.Test/App.config
Normal 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
206
OpenRA.Test/Fakes.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
273
OpenRA.Test/PathfinderTests.cs
Normal file
273
OpenRA.Test/PathfinderTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
thirdparty/fetch-thirdparty-deps.ps1
vendored
8
thirdparty/fetch-thirdparty-deps.ps1
vendored
@@ -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
|
||||
}
|
||||
|
||||
7
thirdparty/fetch-thirdparty-deps.sh
vendored
7
thirdparty/fetch-thirdparty-deps.sh
vendored
@@ -57,4 +57,11 @@ if [ ! -f Mono.Nat.dll ]; then
|
||||
nuget install Mono.Nat -Version 1.2.21
|
||||
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
|
||||
Reference in New Issue
Block a user