Moves Attack, Armament, Move, Air traits and activities as well as anything required by them to Mods.Common.
Extracts Exit from Production into its own trait.
This commit is contained in:
224
OpenRA.Mods.Common/Traits/World/DomainIndex.cs
Normal file
224
OpenRA.Mods.Common/Traits/World/DomainIndex.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2014 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.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[Desc("Identify untraversable regions of the map for faster pathfinding, especially with AI.",
|
||||
"This trait is required. Every mod needs it attached to the world actor.")]
|
||||
class DomainIndexInfo : TraitInfo<DomainIndex> { }
|
||||
|
||||
public class DomainIndex : IWorldLoaded
|
||||
{
|
||||
Dictionary<uint, MovementClassDomainIndex> domainIndexes;
|
||||
|
||||
public void WorldLoaded(World world, WorldRenderer wr)
|
||||
{
|
||||
domainIndexes = new Dictionary<uint, MovementClassDomainIndex>();
|
||||
var movementClasses = new HashSet<uint>(
|
||||
world.Map.Rules.Actors.Where(ai => ai.Value.Traits.Contains<MobileInfo>())
|
||||
.Select(ai => (uint)ai.Value.Traits.Get<MobileInfo>().GetMovementClass(world.TileSet)));
|
||||
|
||||
foreach (var mc in movementClasses)
|
||||
domainIndexes[mc] = new MovementClassDomainIndex(world, mc);
|
||||
}
|
||||
|
||||
public bool IsPassable(CPos p1, CPos p2, uint movementClass)
|
||||
{
|
||||
return domainIndexes[movementClass].IsPassable(p1, p2);
|
||||
}
|
||||
|
||||
/// Regenerate the domain index for a group of cells
|
||||
public void UpdateCells(World world, IEnumerable<CPos> cells)
|
||||
{
|
||||
var dirty = new HashSet<CPos>(cells);
|
||||
foreach (var index in domainIndexes)
|
||||
index.Value.UpdateCells(world, dirty);
|
||||
}
|
||||
}
|
||||
|
||||
class MovementClassDomainIndex
|
||||
{
|
||||
Map map;
|
||||
|
||||
uint movementClass;
|
||||
CellLayer<int> domains;
|
||||
Dictionary<int, HashSet<int>> transientConnections;
|
||||
|
||||
public MovementClassDomainIndex(World world, uint movementClass)
|
||||
{
|
||||
map = world.Map;
|
||||
this.movementClass = movementClass;
|
||||
domains = new CellLayer<int>(world.Map);
|
||||
transientConnections = new Dictionary<int, HashSet<int>>();
|
||||
|
||||
using (new PerfTimer("BuildDomains: {0}".F(world.Map.Title)))
|
||||
BuildDomains(world);
|
||||
}
|
||||
|
||||
public bool IsPassable(CPos p1, CPos p2)
|
||||
{
|
||||
if (!map.Contains(p1) || !map.Contains(p2))
|
||||
return false;
|
||||
|
||||
if (domains[p1] == domains[p2])
|
||||
return true;
|
||||
|
||||
// Even though p1 and p2 are in different domains, it's possible
|
||||
// that some dynamic terrain (i.e. bridges) may connect them.
|
||||
return HasConnection(domains[p1], domains[p2]);
|
||||
}
|
||||
|
||||
public void UpdateCells(World world, HashSet<CPos> dirtyCells)
|
||||
{
|
||||
var neighborDomains = new List<int>();
|
||||
|
||||
foreach (var cell in dirtyCells)
|
||||
{
|
||||
// Select all neighbors inside the map boundries
|
||||
var thisCell = cell; // benign closure hazard
|
||||
var neighbors = CVec.Directions.Select(d => d + thisCell)
|
||||
.Where(c => map.Contains(c));
|
||||
|
||||
var found = false;
|
||||
foreach (var n in neighbors)
|
||||
{
|
||||
if (!dirtyCells.Contains(n))
|
||||
{
|
||||
var neighborDomain = domains[n];
|
||||
if (CanTraverseTile(world, n))
|
||||
neighborDomains.Add(neighborDomain);
|
||||
|
||||
// Set ourselves to the first non-dirty neighbor we find.
|
||||
if (!found)
|
||||
{
|
||||
domains[cell] = neighborDomain;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var c1 in neighborDomains)
|
||||
foreach (var c2 in neighborDomains)
|
||||
CreateConnection(c1, c2);
|
||||
}
|
||||
|
||||
bool HasConnection(int d1, int d2)
|
||||
{
|
||||
// Search our connections graph for a possible route
|
||||
var visited = new HashSet<int>();
|
||||
var toProcess = new Stack<int>();
|
||||
toProcess.Push(d1);
|
||||
|
||||
while (toProcess.Any())
|
||||
{
|
||||
var current = toProcess.Pop();
|
||||
if (!transientConnections.ContainsKey(current))
|
||||
continue;
|
||||
|
||||
foreach (var neighbor in transientConnections[current])
|
||||
{
|
||||
if (neighbor == d2)
|
||||
return true;
|
||||
|
||||
if (!visited.Contains(neighbor))
|
||||
toProcess.Push(neighbor);
|
||||
}
|
||||
|
||||
visited.Add(current);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void CreateConnection(int d1, int d2)
|
||||
{
|
||||
if (!transientConnections.ContainsKey(d1))
|
||||
transientConnections[d1] = new HashSet<int>();
|
||||
if (!transientConnections.ContainsKey(d2))
|
||||
transientConnections[d2] = new HashSet<int>();
|
||||
|
||||
transientConnections[d1].Add(d2);
|
||||
transientConnections[d2].Add(d1);
|
||||
}
|
||||
|
||||
bool CanTraverseTile(World world, CPos p)
|
||||
{
|
||||
var terrainOffset = world.Map.GetTerrainIndex(p);
|
||||
return (movementClass & (1 << terrainOffset)) > 0;
|
||||
}
|
||||
|
||||
void BuildDomains(World world)
|
||||
{
|
||||
var map = world.Map;
|
||||
|
||||
var domain = 1;
|
||||
|
||||
var visited = new CellLayer<bool>(map);
|
||||
|
||||
var toProcess = new Queue<CPos>();
|
||||
toProcess.Enqueue(new CPos(map.Bounds.Left, map.Bounds.Top));
|
||||
|
||||
// Flood-fill over each domain
|
||||
while (toProcess.Count != 0)
|
||||
{
|
||||
var start = toProcess.Dequeue();
|
||||
|
||||
// Technically redundant with the check in the inner loop, but prevents
|
||||
// ballooning the domain counter.
|
||||
if (visited[start])
|
||||
continue;
|
||||
|
||||
var domainQueue = new Queue<CPos>();
|
||||
domainQueue.Enqueue(start);
|
||||
|
||||
var currentPassable = CanTraverseTile(world, start);
|
||||
|
||||
// Add all contiguous cells to our domain, and make a note of
|
||||
// any non-contiguous cells for future domains
|
||||
while (domainQueue.Count != 0)
|
||||
{
|
||||
var n = domainQueue.Dequeue();
|
||||
if (visited[n])
|
||||
continue;
|
||||
|
||||
var candidatePassable = CanTraverseTile(world, n);
|
||||
if (candidatePassable != currentPassable)
|
||||
{
|
||||
toProcess.Enqueue(n);
|
||||
continue;
|
||||
}
|
||||
|
||||
visited[n] = true;
|
||||
domains[n] = domain;
|
||||
|
||||
// Don't crawl off the map, or add already-visited cells
|
||||
var neighbors = CVec.Directions.Select(d => n + d)
|
||||
.Where(p => map.Contains(p) && !visited[p]);
|
||||
|
||||
foreach (var neighbor in neighbors)
|
||||
domainQueue.Enqueue(neighbor);
|
||||
}
|
||||
|
||||
domain += 1;
|
||||
}
|
||||
|
||||
Log.Write("debug", "Found {0} domains on map {1}.", domain - 1, map.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
306
OpenRA.Mods.Common/Traits/World/PathFinder.cs
Normal file
306
OpenRA.Mods.Common/Traits/World/PathFinder.cs
Normal file
@@ -0,0 +1,306 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2014 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.Linq;
|
||||
using OpenRA;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
|
||||
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 class PathFinder
|
||||
{
|
||||
const int MaxPathAge = 50; /* x 40ms ticks */
|
||||
static readonly List<CPos> emptyPath = new List<CPos>(0);
|
||||
|
||||
readonly World world;
|
||||
public PathFinder(World world) { this.world = world; }
|
||||
|
||||
class CachedPath
|
||||
{
|
||||
public CPos from;
|
||||
public CPos to;
|
||||
public List<CPos> result;
|
||||
public int tick;
|
||||
public Actor actor;
|
||||
}
|
||||
|
||||
List<CachedPath> CachedPaths = new List<CachedPath>();
|
||||
|
||||
public List<CPos> FindUnitPath(CPos from, CPos target, Actor self)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
var cached = CachedPaths.FirstOrDefault(p => p.from == from && p.to == target && p.actor == self);
|
||||
if (cached != null)
|
||||
{
|
||||
Log.Write("debug", "Actor {0} asked for a path from {1} tick(s) ago", self.ActorID, world.WorldTick - cached.tick);
|
||||
if (world.WorldTick - cached.tick > MaxPathAge)
|
||||
CachedPaths.Remove(cached);
|
||||
return new List<CPos>(cached.result);
|
||||
}
|
||||
|
||||
var mi = self.Info.Traits.Get<MobileInfo>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
public List<CPos> FindUnitPathToRange(CPos src, SubCell srcSub, WPos target, WRange range, Actor self)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
var mi = self.Info.Traits.Get<MobileInfo>();
|
||||
var targetCell = self.World.Map.CellContaining(target);
|
||||
var rangeSquared = range.Range * range.Range;
|
||||
|
||||
// Correct for SubCell offset
|
||||
target -= self.World.Map.OffsetOfSubCell(srcSub);
|
||||
|
||||
// Select only the tiles that are within range from the requested SubCell
|
||||
// This assumes that the SubCell does not change during the path traversal
|
||||
var tilesInRange = world.Map.FindTilesInCircle(targetCell, range.Range / 1024 + 1)
|
||||
.Where(t => (world.Map.CenterOfCell(t) - target).LengthSquared <= rangeSquared
|
||||
&& mi.CanEnterCell(self.World, self, t));
|
||||
|
||||
// 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)
|
||||
{
|
||||
var passable = mi.GetMovementClass(world.TileSet);
|
||||
tilesInRange = new List<CPos>(tilesInRange.Where(t => domainIndex.IsPassable(src, t, (uint)passable)));
|
||||
if (!tilesInRange.Any())
|
||||
return emptyPath;
|
||||
}
|
||||
|
||||
var path = FindBidiPath(
|
||||
PathSearch.FromPoints(world, mi, self, tilesInRange, src, true),
|
||||
PathSearch.FromPoint(world, mi, self, src, targetCell, true).Reverse());
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public List<CPos> FindPath(PathSearch search)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
using (search)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// no path exists
|
||||
return emptyPath;
|
||||
}
|
||||
}
|
||||
|
||||
static List<CPos> MakePath(CellLayer<CellInfo> cellInfo, CPos destination)
|
||||
{
|
||||
var ret = new List<CPos>();
|
||||
var pathNode = destination;
|
||||
|
||||
while (cellInfo[pathNode].Path != pathNode)
|
||||
{
|
||||
ret.Add(pathNode);
|
||||
pathNode = cellInfo[pathNode].Path;
|
||||
}
|
||||
|
||||
ret.Add(pathNode);
|
||||
CheckSanePath(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Searches from both ends toward each other
|
||||
public List<CPos> FindBidiPath(PathSearch fromSrc, PathSearch fromDest)
|
||||
{
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
using (fromSrc)
|
||||
using (fromDest)
|
||||
{
|
||||
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 ret = new List<CPos>();
|
||||
|
||||
var q = p;
|
||||
while (ca[q].Path != q)
|
||||
{
|
||||
ret.Add(q);
|
||||
q = ca[q].Path;
|
||||
}
|
||||
|
||||
ret.Add(q);
|
||||
|
||||
ret.Reverse();
|
||||
|
||||
q = p;
|
||||
while (cb[q].Path != q)
|
||||
{
|
||||
q = cb[q].Path;
|
||||
ret.Add(q);
|
||||
}
|
||||
|
||||
CheckSanePath(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
[Conditional("SANITY_CHECKS")]
|
||||
static void CheckSanePath(List<CPos> path)
|
||||
{
|
||||
if (path.Count == 0)
|
||||
return;
|
||||
var prev = path[0];
|
||||
for (var i = 0; i < path.Count; i++)
|
||||
{
|
||||
var d = path[i] - prev;
|
||||
if (Math.Abs(d.X) > 1 || Math.Abs(d.Y) > 1)
|
||||
throw new InvalidOperationException("(PathFinder) path sanity check failed");
|
||||
prev = path[i];
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("SANITY_CHECKS")]
|
||||
static void CheckSanePath2(List<CPos> path, CPos src, CPos dest)
|
||||
{
|
||||
if (path.Count == 0)
|
||||
return;
|
||||
|
||||
if (path[0] != dest)
|
||||
throw new InvalidOperationException("(PathFinder) sanity check failed: doesn't go to dest");
|
||||
if (path[path.Count - 1] != src)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
362
OpenRA.Mods.Common/Traits/World/PathSearch.cs
Normal file
362
OpenRA.Mods.Common/Traits/World/PathSearch.cs
Normal file
@@ -0,0 +1,362 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2014 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[u, v] = new CellInfo(int.MaxValue, Map.MapToCell(map.TileShape, new CPos(u, v)), 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(); }
|
||||
}
|
||||
}
|
||||
88
OpenRA.Mods.Common/Traits/World/PathfinderDebugOverlay.cs
Normal file
88
OpenRA.Mods.Common/Traits/World/PathfinderDebugOverlay.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2014 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 OpenRA;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[Desc("Required for the A* PathDebug from DeveloperMode. Attach this to the world actor.")]
|
||||
public class PathfinderDebugOverlayInfo : TraitInfo<PathfinderDebugOverlay> { }
|
||||
public class PathfinderDebugOverlay : IRenderOverlay, IWorldLoaded
|
||||
{
|
||||
Dictionary<Player, CellLayer<int>> layers;
|
||||
int refreshTick;
|
||||
World world;
|
||||
public bool Visible;
|
||||
|
||||
public void WorldLoaded(World w, WorldRenderer wr)
|
||||
{
|
||||
world = w;
|
||||
refreshTick = 0;
|
||||
layers = new Dictionary<Player, CellLayer<int>>(8);
|
||||
|
||||
// Enabled via Cheats menu
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
public void AddLayer(IEnumerable<Pair<CPos, int>> cellWeights, int maxWeight, Player pl)
|
||||
{
|
||||
if (maxWeight == 0) return;
|
||||
|
||||
CellLayer<int> layer;
|
||||
if (!layers.TryGetValue(pl, out layer))
|
||||
{
|
||||
layer = new CellLayer<int>(world.Map);
|
||||
layers.Add(pl, layer);
|
||||
}
|
||||
|
||||
foreach (var p in cellWeights)
|
||||
layer[p.First] = Math.Min(128, layer[p.First] + (maxWeight - p.Second) * 64 / maxWeight);
|
||||
}
|
||||
|
||||
public void Render(WorldRenderer wr)
|
||||
{
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
var qr = Game.Renderer.WorldQuadRenderer;
|
||||
var doDim = refreshTick - world.WorldTick <= 0;
|
||||
if (doDim) refreshTick = world.WorldTick + 20;
|
||||
|
||||
foreach (var pair in layers)
|
||||
{
|
||||
var c = (pair.Key != null) ? pair.Key.Color.RGB : Color.PaleTurquoise;
|
||||
var layer = pair.Value;
|
||||
|
||||
// Only render quads in viewing range:
|
||||
foreach (var cell in wr.Viewport.VisibleCells)
|
||||
{
|
||||
if (layer[cell] <= 0)
|
||||
continue;
|
||||
|
||||
var w = Math.Max(0, Math.Min(layer[cell], 128));
|
||||
if (doDim)
|
||||
layer[cell] = layer[cell] * 5 / 6;
|
||||
|
||||
// TODO: This doesn't make sense for isometric terrain
|
||||
var pos = wr.world.Map.CenterOfCell(cell);
|
||||
var tl = wr.ScreenPxPosition(pos - new WVec(512, 512, 0));
|
||||
var br = wr.ScreenPxPosition(pos + new WVec(511, 511, 0));
|
||||
qr.FillRect(RectangleF.FromLTRB(tl.X, tl.Y, br.X, br.Y), Color.FromArgb(w, c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user