Merge pull request #3375 from i80and/pathingdomains

Pathingdomains
This commit is contained in:
Paul Chote
2013-07-12 23:01:38 -07:00
10 changed files with 299 additions and 17 deletions

View File

@@ -20,6 +20,7 @@ Also thanks to:
* Akseli Virtanen (RAGEQUIT)
* Andrew Perkins
* Andrew Riedi
* Andrew Aldridge (i80and)
* Andreas Beck (baxtor)
* Barnaby Smith (mvi)
* Bellator

View File

@@ -73,5 +73,17 @@ namespace OpenRA
}
public override string ToString() { return "{0},{1}".F(X, Y); }
public static readonly CVec[] directions =
{
new CVec(-1, -1),
new CVec(-1, 0),
new CVec(-1, 1),
new CVec(0, -1),
new CVec(0, 1),
new CVec(1, -1),
new CVec(1, 0),
new CVec(1, 1),
};
}
}

View File

@@ -218,6 +218,12 @@ namespace OpenRA.Mods.RA
foreach (var c in TileSprites[currentTemplate].Keys)
self.World.Map.CustomTerrain[c.X, c.Y] = GetTerrainType(c);
// If this bridge repair operation connects two pathfinding domains,
// update the domain index.
var domainIndex = self.World.WorldActor.TraitOrDefault<DomainIndex>();
if (domainIndex != null)
domainIndex.UpdateCells(self.World, TileSprites[currentTemplate].Keys);
if (LongBridgeSegmentIsDead() && !killedUnits)
{
killedUnits = true;

View File

@@ -25,6 +25,8 @@ namespace OpenRA.Mods.RA.Move
public class PathFinder
{
readonly static List<CPos> emptyPath = new List<CPos>(0);
readonly World world;
public PathFinder(World world) { this.world = world; }
@@ -50,11 +52,20 @@ namespace OpenRA.Mods.RA.Move
Log.Write("debug", "Actor {0} asked for a path from {1} tick(s) ago", self.ActorID, world.FrameNumber - cached.tick);
if (world.FrameNumber - cached.tick > MaxPathAge)
CachedPaths.Remove(cached);
return new List<CPos>(cached.result);
return emptyPath;
}
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 pb = FindBidiPath(
PathSearch.FromPoint(world, mi, self, target, from, true),
PathSearch.FromPoint(world, mi, self, from, target, true).InReverse()
@@ -86,6 +97,17 @@ namespace OpenRA.Mods.RA.Move
.Where(t => (t.CenterPosition - target).LengthSquared <= rangeSquared
&& mi.CanEnterCell(self.World, self, t, null, true, true));
// 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.Count() == 0)
return emptyPath;
}
var path = FindBidiPath(
PathSearch.FromPoints(world, mi, self, tilesInRange, src, true),
PathSearch.FromPoint(world, mi, self, src, targetCell, true).InReverse()
@@ -124,7 +146,7 @@ namespace OpenRA.Mods.RA.Move
}
// no path exists
return new List<CPos>(0);
return emptyPath;
}
}
@@ -189,7 +211,7 @@ namespace OpenRA.Mods.RA.Move
return path;
}
return new List<CPos>(0);
return emptyPath;
}
}

View File

@@ -44,7 +44,7 @@ namespace OpenRA.Mods.RA.Move
queue = new PriorityQueue<PathDistance>();
considered = new HashSet<CPos>();
maxCost = 0;
nextDirections = directions.Select(d => new Pair<CVec, int>(d, 0)).ToArray();
nextDirections = CVec.directions.Select(d => new Pair<CVec, int>(d, 0)).ToArray();
}
public PathSearch InReverse()
@@ -190,18 +190,6 @@ namespace OpenRA.Mods.RA.Move
return p.Location;
}
static readonly CVec[] directions =
{
new CVec( -1, -1 ),
new CVec( -1, 0 ),
new CVec( -1, 1 ),
new CVec( 0, -1 ),
new CVec( 0, 1 ),
new CVec( 1, -1 ),
new CVec( 1, 0 ),
new CVec( 1, 1 ),
};
public void AddInitialCell(CPos location)
{
if (!world.Map.IsInMap(location.X, location.Y))

View File

@@ -457,6 +457,7 @@
<Compile Include="Widgets\SlidingContainerWidget.cs" />
<Compile Include="Widgets\ResourceBarWidget.cs" />
<Compile Include="Widgets\Logic\SimpleTooltipLogic.cs" />
<Compile Include="World\DomainIndex.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenRA.FileFormats\OpenRA.FileFormats.csproj">

View File

@@ -0,0 +1,249 @@
#region Copyright & License Information
/*
* Copyright 2007-2013 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.FileFormats;
using OpenRA.Mods.RA.Move;
using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA.Mods.RA
{
// Identify untraversable regions of the map for faster pathfinding, especially with AI
class DomainIndexInfo : TraitInfo<DomainIndex> {}
public class DomainIndex : IWorldLoaded
{
Dictionary<uint, MovementClassDomainIndex> domainIndexes;
public void WorldLoaded(World world)
{
domainIndexes = new Dictionary<uint, MovementClassDomainIndex>();
var movementClasses = new HashSet<uint>(
Rules.Info.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
{
Rectangle bounds;
uint movementClass;
int[,] domains;
Dictionary<int, HashSet<int>> transientConnections;
// Each terrain has an offset corresponding to its location in a
// movement class bitmask. This caches each offset.
Dictionary<string, int> terrainOffsets;
public MovementClassDomainIndex(World world, uint movementClass)
{
bounds = world.Map.Bounds;
this.movementClass = movementClass;
domains = new int[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)];
transientConnections = new Dictionary<int, HashSet<int>>();
terrainOffsets = new Dictionary<string, int>();
var terrains = world.TileSet.Terrain.OrderBy(t => t.Key).ToList();
foreach (var terrain in terrains)
{
var terrainOffset = terrains.FindIndex(x => x.Key == terrain.Key);
terrainOffsets[terrain.Key] = terrainOffset;
}
BuildDomains(world);
}
public bool IsPassable(CPos p1, CPos p2)
{
if (domains[p1.X, p1.Y] == domains[p2.X, p2.Y])
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(GetDomainOf(p1), GetDomainOf(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 neighbors = CVec.directions.Select(d => d + cell)
.Where(c => bounds.Contains(c.X, c.Y));
var found = false;
foreach (var neighbor in neighbors)
{
if (!dirtyCells.Contains(neighbor))
{
var neighborDomain = GetDomainOf(neighbor);
var match = CanTraverseTile(world, neighbor);
if (match) neighborDomains.Add(neighborDomain);
// Set ourselves to the first non-dirty neighbor we find.
if (!found)
{
SetDomain(cell, neighborDomain);
found = true;
}
}
}
}
foreach (var c1 in neighborDomains)
{
foreach (var c2 in neighborDomains)
CreateConnection(c1, c2);
}
}
int GetDomainOf(CPos p)
{
return domains[p.X, p.Y];
}
void SetDomain(CPos p, int domain)
{
domains[p.X, p.Y] = domain;
}
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);
var i = 0;
while (toProcess.Count() > 0)
{
var current = toProcess.Pop();
if (!transientConnections.ContainsKey(current))
continue;
foreach (int neighbor in transientConnections[current])
{
if (neighbor == d2)
return true;
if (!visited.Contains(neighbor))
toProcess.Push(neighbor);
}
visited.Add(current);
i += 1;
}
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 currentTileType = WorldUtils.GetTerrainType(world, p);
var terrainOffset = terrainOffsets[currentTileType];
return (movementClass & (1 << terrainOffset)) > 0;
}
void BuildDomains(World world)
{
var timer = new Stopwatch();
var map = world.Map;
var domain = 1;
var visited = new bool[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)];
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.X, start.Y])
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.X, n.Y])
continue;
var candidatePassable = CanTraverseTile(world, n);
if (candidatePassable != currentPassable)
{
toProcess.Enqueue(n);
continue;
}
visited[n.X, n.Y] = true;
SetDomain(n, domain);
// Don't crawl off the map, or add already-visited cells
var neighbors = CVec.directions.Select(d => n + d)
.Where(p => (p.X < map.Bounds.Right) && (p.X >= map.Bounds.Left)
&& (p.Y >= map.Bounds.Top) && (p.Y < map.Bounds.Bottom)
&& !visited[p.X, p.Y]);
foreach (var neighbor in neighbors)
domainQueue.Enqueue(neighbor);
}
domain += 1;
}
Log.Write("debug", "{0}: Found {1} domains. Took {2} s", map.Title, domain-1, timer.ElapsedTime());
}
}
}

View File

@@ -158,7 +158,7 @@ Player:
silo: 1
BuildingFractions:
proc: 17%
nuke: 10%
nuke: 10%
pyle: 7%
hand: 9%
hq: 1%
@@ -273,6 +273,7 @@ World:
ProductionQueueFromSelection:
ProductionTabsWidget: PRODUCTION_TABS
BibLayer:
DomainIndex:
ResourceLayer:
ResourceClaimLayer:
ResourceType@green-tib:

View File

@@ -354,6 +354,7 @@ World:
BibLayer:
BibTypes: bib3x, bib2x
BibWidths: 3, 2
DomainIndex:
ResourceLayer:
ResourceClaimLayer:
ResourceType@Spice:

View File

@@ -604,6 +604,7 @@ World:
Name: Soviet
Race: soviet
BibLayer:
DomainIndex:
ResourceLayer:
ResourceClaimLayer:
ResourceType@ore: