From 1fe2418e22cafed7ae7289db0a93946b9d254b01 Mon Sep 17 00:00:00 2001 From: i80and Date: Mon, 27 May 2013 10:41:50 -0500 Subject: [PATCH 1/8] Factor out the CVec direction list. --- OpenRA.Game/CVec.cs | 12 ++++++++++++ OpenRA.Mods.RA/Move/PathSearch.cs | 14 +------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/OpenRA.Game/CVec.cs b/OpenRA.Game/CVec.cs index d21a4c21ff..aad8d732d3 100644 --- a/OpenRA.Game/CVec.cs +++ b/OpenRA.Game/CVec.cs @@ -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), + }; } } diff --git a/OpenRA.Mods.RA/Move/PathSearch.cs b/OpenRA.Mods.RA/Move/PathSearch.cs index 2775e3a3de..efd79d2c7e 100755 --- a/OpenRA.Mods.RA/Move/PathSearch.cs +++ b/OpenRA.Mods.RA/Move/PathSearch.cs @@ -44,7 +44,7 @@ namespace OpenRA.Mods.RA.Move queue = new PriorityQueue(); considered = new HashSet(); maxCost = 0; - nextDirections = directions.Select(d => new Pair(d, 0)).ToArray(); + nextDirections = CVec.directions.Select(d => new Pair(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)) From d4cead218779890c4a45535c83eb9c99103381e1 Mon Sep 17 00:00:00 2001 From: i80and Date: Mon, 27 May 2013 09:08:01 -0500 Subject: [PATCH 2/8] Initial terrain domain work --- OpenRA.Game/DomainIndex.cs | 106 ++++++++++++++++++++++++++++++ OpenRA.Game/OpenRA.Game.csproj | 1 + OpenRA.Game/World.cs | 4 ++ OpenRA.Mods.RA/Move/PathFinder.cs | 10 +++ 4 files changed, 121 insertions(+) create mode 100644 OpenRA.Game/DomainIndex.cs diff --git a/OpenRA.Game/DomainIndex.cs b/OpenRA.Game/DomainIndex.cs new file mode 100644 index 0000000000..0207556e69 --- /dev/null +++ b/OpenRA.Game/DomainIndex.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +using OpenRA.Effects; +using OpenRA.FileFormats; +using OpenRA.Graphics; +using OpenRA.Network; +using OpenRA.Orders; +using OpenRA.Support; +using OpenRA.Traits; +using XRandom = OpenRA.Thirdparty.Random; + +namespace OpenRA +{ + public class DomainIndex + { + Rectangle bounds; + int[,] domains; + + public DomainIndex(World world) + { + bounds = world.Map.Bounds; + domains = new int[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)]; + + BuildDomains(world); + } + + public int GetDomainOf(CPos p) + { + return domains[p.X, p.Y]; + } + + public bool IsCrossDomain(CPos p1, CPos p2) + { + return GetDomainOf(p1) != GetDomainOf(p2); + } + + public void SetDomain(CPos p, int domain) + { + domains[p.X, p.Y] = domain; + } + + void BuildDomains(World world) + { + Map map = world.Map; + + int i = 1; + HashSet unassigned = new HashSet(); + + // Fill up our set of yet-unassigned map cells + for (int x = map.Bounds.Left; x < bounds.Right; x += 1) + { + for (int y = bounds.Top; y < bounds.Bottom; y += 1) + { + unassigned.Add(new CPos(x, y)); + } + } + + while (unassigned.Count != 0) + { + var start = unassigned.First(); + unassigned.Remove(start); + + // Wander around looking for water transitions + bool inWater = WorldUtils.GetTerrainInfo(world, start).IsWater; + Queue toProcess = new Queue(); + HashSet seen = new HashSet(); + toProcess.Enqueue(start); + + do + { + CPos p = toProcess.Dequeue(); + if (seen.Contains(p)) continue; + seen.Add(p); + + TerrainTypeInfo cellInfo = WorldUtils.GetTerrainInfo(world, p); + bool isWater = cellInfo.IsWater; + + // Check if we're still in one contiguous domain + if (inWater == isWater) + { + SetDomain(p, i); + unassigned.Remove(p); + + // Visit our neighbors, if we haven't already + foreach (var d in CVec.directions) + { + CPos nextPos = p + d; + if (nextPos.X >= map.Bounds.Left && nextPos.Y >= map.Bounds.Top && + nextPos.X < map.Bounds.Right && nextPos.Y < map.Bounds.Bottom) + { + if (!seen.Contains(nextPos)) toProcess.Enqueue(nextPos); + } + } + } + } while (toProcess.Count != 0); + + i += 1; + } + + Log.Write("debug", "{0}: Found {1} domains", map.Title, i-1); + } + } +} diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 0aca544852..2c5c9cebbf 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -85,6 +85,7 @@ + diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index 7c2d6177a6..d1ea2004a0 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -111,6 +111,7 @@ namespace OpenRA } } + public DomainIndex WorldDomains; internal World(Manifest manifest, Map map, OrderManager orderManager, bool isShellmap) { IsShellmap = isShellmap; @@ -121,6 +122,9 @@ namespace OpenRA TileSet = Rules.TileSets[Map.Tileset]; TileSet.LoadTiles(); + // Identify untraversable regions of the map for faster pathfinding, especially with AI + WorldDomains = new DomainIndex(this); + SharedRandom = new XRandom(orderManager.LobbyInfo.GlobalSettings.RandomSeed); WorldActor = CreateActor( "World", new TypeDictionary() ); diff --git a/OpenRA.Mods.RA/Move/PathFinder.cs b/OpenRA.Mods.RA/Move/PathFinder.cs index 5345bb0702..40d3e84fcd 100755 --- a/OpenRA.Mods.RA/Move/PathFinder.cs +++ b/OpenRA.Mods.RA/Move/PathFinder.cs @@ -44,6 +44,9 @@ namespace OpenRA.Mods.RA.Move { using (new PerfSample("Pathfinder")) { + // If a water-land transition is required, bail early + if (world.WorldDomains.IsCrossDomain(from, target)) return new List(0); + var cached = CachedPaths.FirstOrDefault(p => p.from == from && p.to == target && p.actor == self); if (cached != null) { @@ -72,6 +75,13 @@ namespace OpenRA.Mods.RA.Move { using (new PerfSample("Pathfinder")) { + // As with FindUnitPath, avoid trying to traverse domain transitions. + // In this case it's pretty sketchy; path false-negatives are possible. + if(world.WorldDomains.IsCrossDomain(src, target.ToCPos())) + { + return new List(0); + } + var mi = self.Info.Traits.Get(); var targetCell = target.ToCPos(); var rangeSquared = range.Range*range.Range; From 6fb01c7ab87ac49c1948c7771d617fa2437e8055 Mon Sep 17 00:00:00 2001 From: Andrew Aldridge Date: Wed, 29 May 2013 01:10:00 -0500 Subject: [PATCH 3/8] Properly deal with FindUnitPathToRange This fixes boats attacking land targets. --- OpenRA.Mods.RA/Move/PathFinder.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/OpenRA.Mods.RA/Move/PathFinder.cs b/OpenRA.Mods.RA/Move/PathFinder.cs index 40d3e84fcd..38d22bfe47 100755 --- a/OpenRA.Mods.RA/Move/PathFinder.cs +++ b/OpenRA.Mods.RA/Move/PathFinder.cs @@ -75,13 +75,6 @@ namespace OpenRA.Mods.RA.Move { using (new PerfSample("Pathfinder")) { - // As with FindUnitPath, avoid trying to traverse domain transitions. - // In this case it's pretty sketchy; path false-negatives are possible. - if(world.WorldDomains.IsCrossDomain(src, target.ToCPos())) - { - return new List(0); - } - var mi = self.Info.Traits.Get(); var targetCell = target.ToCPos(); var rangeSquared = range.Range*range.Range; @@ -94,7 +87,13 @@ namespace OpenRA.Mods.RA.Move // This assumes that the SubCell does not change during the path traversal var tilesInRange = world.FindTilesInCircle(targetCell, range.Range / 1024 + 1) .Where(t => (t.CenterPosition - target).LengthSquared <= rangeSquared - && mi.CanEnterCell(self.World, self, t, null, true, true)); + && mi.CanEnterCell(self.World, self, t, null, true, true) + && !world.WorldDomains.IsCrossDomain(src, t)); + + if(tilesInRange.Count() == 0) + { + return new List(0); + } var path = FindBidiPath( PathSearch.FromPoints(world, mi, self, tilesInRange, src, true), From ba885907ba170b4e7865cf824b09f82be47753b3 Mon Sep 17 00:00:00 2001 From: Andrew Aldridge Date: Wed, 26 Jun 2013 23:38:01 -0400 Subject: [PATCH 4/8] Use MobileInfo.GetMovementClass for domain indexing, clearing path for caching and smarter behavior * Move DomainIndex from being a manual hard-coded hook in World to an IWorldLoaded trait. --- OpenRA.Game/DomainIndex.cs | 106 ------------------- OpenRA.Game/OpenRA.Game.csproj | 1 - OpenRA.Game/World.cs | 4 - OpenRA.Mods.RA/Move/PathFinder.cs | 25 +++-- OpenRA.Mods.RA/OpenRA.Mods.RA.csproj | 1 + OpenRA.Mods.RA/World/DomainIndex.cs | 146 +++++++++++++++++++++++++++ mods/cnc/rules/system.yaml | 3 +- mods/d2k/rules/system.yaml | 1 + mods/ra/rules/system.yaml | 1 + 9 files changed, 169 insertions(+), 119 deletions(-) delete mode 100644 OpenRA.Game/DomainIndex.cs create mode 100644 OpenRA.Mods.RA/World/DomainIndex.cs diff --git a/OpenRA.Game/DomainIndex.cs b/OpenRA.Game/DomainIndex.cs deleted file mode 100644 index 0207556e69..0000000000 --- a/OpenRA.Game/DomainIndex.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; - -using OpenRA.Effects; -using OpenRA.FileFormats; -using OpenRA.Graphics; -using OpenRA.Network; -using OpenRA.Orders; -using OpenRA.Support; -using OpenRA.Traits; -using XRandom = OpenRA.Thirdparty.Random; - -namespace OpenRA -{ - public class DomainIndex - { - Rectangle bounds; - int[,] domains; - - public DomainIndex(World world) - { - bounds = world.Map.Bounds; - domains = new int[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)]; - - BuildDomains(world); - } - - public int GetDomainOf(CPos p) - { - return domains[p.X, p.Y]; - } - - public bool IsCrossDomain(CPos p1, CPos p2) - { - return GetDomainOf(p1) != GetDomainOf(p2); - } - - public void SetDomain(CPos p, int domain) - { - domains[p.X, p.Y] = domain; - } - - void BuildDomains(World world) - { - Map map = world.Map; - - int i = 1; - HashSet unassigned = new HashSet(); - - // Fill up our set of yet-unassigned map cells - for (int x = map.Bounds.Left; x < bounds.Right; x += 1) - { - for (int y = bounds.Top; y < bounds.Bottom; y += 1) - { - unassigned.Add(new CPos(x, y)); - } - } - - while (unassigned.Count != 0) - { - var start = unassigned.First(); - unassigned.Remove(start); - - // Wander around looking for water transitions - bool inWater = WorldUtils.GetTerrainInfo(world, start).IsWater; - Queue toProcess = new Queue(); - HashSet seen = new HashSet(); - toProcess.Enqueue(start); - - do - { - CPos p = toProcess.Dequeue(); - if (seen.Contains(p)) continue; - seen.Add(p); - - TerrainTypeInfo cellInfo = WorldUtils.GetTerrainInfo(world, p); - bool isWater = cellInfo.IsWater; - - // Check if we're still in one contiguous domain - if (inWater == isWater) - { - SetDomain(p, i); - unassigned.Remove(p); - - // Visit our neighbors, if we haven't already - foreach (var d in CVec.directions) - { - CPos nextPos = p + d; - if (nextPos.X >= map.Bounds.Left && nextPos.Y >= map.Bounds.Top && - nextPos.X < map.Bounds.Right && nextPos.Y < map.Bounds.Bottom) - { - if (!seen.Contains(nextPos)) toProcess.Enqueue(nextPos); - } - } - } - } while (toProcess.Count != 0); - - i += 1; - } - - Log.Write("debug", "{0}: Found {1} domains", map.Title, i-1); - } - } -} diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 2c5c9cebbf..0aca544852 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -85,7 +85,6 @@ - diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index d1ea2004a0..7c2d6177a6 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -111,7 +111,6 @@ namespace OpenRA } } - public DomainIndex WorldDomains; internal World(Manifest manifest, Map map, OrderManager orderManager, bool isShellmap) { IsShellmap = isShellmap; @@ -122,9 +121,6 @@ namespace OpenRA TileSet = Rules.TileSets[Map.Tileset]; TileSet.LoadTiles(); - // Identify untraversable regions of the map for faster pathfinding, especially with AI - WorldDomains = new DomainIndex(this); - SharedRandom = new XRandom(orderManager.LobbyInfo.GlobalSettings.RandomSeed); WorldActor = CreateActor( "World", new TypeDictionary() ); diff --git a/OpenRA.Mods.RA/Move/PathFinder.cs b/OpenRA.Mods.RA/Move/PathFinder.cs index 38d22bfe47..92d7e15112 100755 --- a/OpenRA.Mods.RA/Move/PathFinder.cs +++ b/OpenRA.Mods.RA/Move/PathFinder.cs @@ -44,9 +44,6 @@ namespace OpenRA.Mods.RA.Move { using (new PerfSample("Pathfinder")) { - // If a water-land transition is required, bail early - if (world.WorldDomains.IsCrossDomain(from, target)) return new List(0); - var cached = CachedPaths.FirstOrDefault(p => p.from == from && p.to == target && p.actor == self); if (cached != null) { @@ -58,6 +55,15 @@ namespace OpenRA.Mods.RA.Move var mi = self.Info.Traits.Get(); + // If a water-land transition is required, bail early + var domainIndex = self.World.WorldActor.TraitOrDefault(); + if (domainIndex != null) + { + var passable = mi.GetMovementClass(world.TileSet); + if (!domainIndex.IsPassable(from, target, (uint)passable)) + return new List(0); + } + var pb = FindBidiPath( PathSearch.FromPoint(world, mi, self, target, from, true), PathSearch.FromPoint(world, mi, self, from, target, true).InReverse() @@ -87,12 +93,17 @@ namespace OpenRA.Mods.RA.Move // This assumes that the SubCell does not change during the path traversal var tilesInRange = world.FindTilesInCircle(targetCell, range.Range / 1024 + 1) .Where(t => (t.CenterPosition - target).LengthSquared <= rangeSquared - && mi.CanEnterCell(self.World, self, t, null, true, true) - && !world.WorldDomains.IsCrossDomain(src, t)); + && mi.CanEnterCell(self.World, self, t, null, true, true)); - if(tilesInRange.Count() == 0) + // 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(); + if (domainIndex != null) { - return new List(0); + var passable = mi.GetMovementClass(world.TileSet); + tilesInRange = new List(tilesInRange.Where(t => domainIndex.IsPassable(src, t, (uint)passable))); + if (tilesInRange.Count() == 0) + return new List(0); } var path = FindBidiPath( diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 6713cdc609..249cf6de26 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -457,6 +457,7 @@ + diff --git a/OpenRA.Mods.RA/World/DomainIndex.cs b/OpenRA.Mods.RA/World/DomainIndex.cs new file mode 100644 index 0000000000..9ce98e000c --- /dev/null +++ b/OpenRA.Mods.RA/World/DomainIndex.cs @@ -0,0 +1,146 @@ +#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.Effects; +using OpenRA.FileFormats; +using OpenRA.Graphics; +using OpenRA.Network; +using OpenRA.Orders; +using OpenRA.Support; +using OpenRA.Traits; +using OpenRA.Mods.RA.Move; +using XRandom = OpenRA.Thirdparty.Random; + +namespace OpenRA.Mods.RA +{ + // Identify untraversable regions of the map for faster pathfinding, especially with AI + class DomainIndexInfo : TraitInfo {} + + public class DomainIndex : IWorldLoaded + { + Dictionary domains; + + public void WorldLoaded(World world) + { + domains = new Dictionary(); + var movementClasses = new HashSet( + Rules.Info.Where(ai => ai.Value.Traits.Contains()) + .Select(ai => (uint)ai.Value.Traits.Get().GetMovementClass(world.TileSet))); + + foreach(var mc in movementClasses) domains[mc] = new MovementClassDomainIndex(world, mc); + } + + public bool IsPassable(CPos p1, CPos p2, uint movementClass) + { + return domains[movementClass].IsPassable(p1, p2); + } + } + + class MovementClassDomainIndex + { + Rectangle bounds; + uint movementClass; + int[,] domains; + + public MovementClassDomainIndex(World world, uint movementClass) + { + this.movementClass = movementClass; + bounds = world.Map.Bounds; + domains = new int[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)]; + BuildDomains(world); + } + + public int GetDomainOf(CPos p) + { + return domains[p.X, p.Y]; + } + + public bool IsPassable(CPos p1, CPos p2) + { + return domains[p1.X, p1.Y] == domains[p2.X, p2.Y]; + } + + public void SetDomain(CPos p, int domain) + { + domains[p.X, p.Y] = domain; + } + + + void BuildDomains(World world) + { + Map map = world.Map; + + int i = 1; + var unassigned = new HashSet(); + + // Fill up our set of yet-unassigned map cells + for (int x = map.Bounds.Left; x < bounds.Right; x += 1) + { + for (int y = bounds.Top; y < bounds.Bottom; y += 1) + { + unassigned.Add(new CPos(x, y)); + } + } + + while (unassigned.Count != 0) + { + var start = unassigned.First(); + unassigned.Remove(start); + + // Wander around looking for water transitions + string currentTileType = WorldUtils.GetTerrainType(world, start); + int terrainOffset = world.TileSet.Terrain.OrderBy(t => t.Key).ToList().FindIndex(x => x.Key == currentTileType); + bool currentPassable = (movementClass & (1 << terrainOffset)) > 0; + + var toProcess = new Queue(); + var seen = new HashSet(); + toProcess.Enqueue(start); + + do + { + CPos p = toProcess.Dequeue(); + if (seen.Contains(p)) continue; + seen.Add(p); + + string candidateTileType = WorldUtils.GetTerrainType(world, p); + int candidateTerrainOffset = world.TileSet.Terrain.OrderBy(t => t.Key).ToList().FindIndex(x => x.Key == candidateTileType); + bool candidatePassable = (movementClass & (1 << candidateTerrainOffset)) > 0; + + // Check if we're still in one contiguous domain + if (currentPassable == candidatePassable) + { + SetDomain(p, i); + unassigned.Remove(p); + + // Visit our neighbors, if we haven't already + foreach (var d in CVec.directions) + { + CPos nextPos = p + d; + if (nextPos.X >= map.Bounds.Left && nextPos.Y >= map.Bounds.Top && + nextPos.X < map.Bounds.Right && nextPos.Y < map.Bounds.Bottom) + { + if (!seen.Contains(nextPos)) toProcess.Enqueue(nextPos); + } + } + } + } while (toProcess.Count != 0); + + i += 1; + } + + Log.Write("debug", "{0}: Found {1} domains", map.Title, i-1); + } + } +} diff --git a/mods/cnc/rules/system.yaml b/mods/cnc/rules/system.yaml index 1445012741..1f2920bce5 100644 --- a/mods/cnc/rules/system.yaml +++ b/mods/cnc/rules/system.yaml @@ -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: diff --git a/mods/d2k/rules/system.yaml b/mods/d2k/rules/system.yaml index b6bbe67e14..79b5c49606 100644 --- a/mods/d2k/rules/system.yaml +++ b/mods/d2k/rules/system.yaml @@ -354,6 +354,7 @@ World: BibLayer: BibTypes: bib3x, bib2x BibWidths: 3, 2 + DomainIndex: ResourceLayer: ResourceClaimLayer: ResourceType@Spice: diff --git a/mods/ra/rules/system.yaml b/mods/ra/rules/system.yaml index 05ac3233c5..4953c644ec 100644 --- a/mods/ra/rules/system.yaml +++ b/mods/ra/rules/system.yaml @@ -604,6 +604,7 @@ World: Name: Soviet Race: soviet BibLayer: + DomainIndex: ResourceLayer: ResourceClaimLayer: ResourceType@ore: From 3fed98fcb10c18a3cccf8193ede0a94fe4273cb1 Mon Sep 17 00:00:00 2001 From: Andrew Aldridge Date: Thu, 4 Jul 2013 18:44:02 -0400 Subject: [PATCH 5/8] Add support for bridge construction Each movement class is given a crude transientDomain graph. Whenever a bridge state changes, the domain index rebuilds the relevant cells, setting its domain and creating a transient domain connection. This graph is searched as a fallback if the straightforward domain comparison is false. Behavior needs to be double-checked, and ideally destructing connections would be supported, but so far performance and behavior on allies-02 seems good. --- OpenRA.Mods.RA/Bridge.cs | 6 ++ OpenRA.Mods.RA/World/DomainIndex.cs | 132 +++++++++++++++++++++++----- 2 files changed, 114 insertions(+), 24 deletions(-) diff --git a/OpenRA.Mods.RA/Bridge.cs b/OpenRA.Mods.RA/Bridge.cs index b889b4e8c9..ea51b80d62 100644 --- a/OpenRA.Mods.RA/Bridge.cs +++ b/OpenRA.Mods.RA/Bridge.cs @@ -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(); + if (domainIndex != null) + domainIndex.UpdateCells(self.World, TileSprites[currentTemplate].Keys); + if (LongBridgeSegmentIsDead() && !killedUnits) { killedUnits = true; diff --git a/OpenRA.Mods.RA/World/DomainIndex.cs b/OpenRA.Mods.RA/World/DomainIndex.cs index 9ce98e000c..99d89d012c 100644 --- a/OpenRA.Mods.RA/World/DomainIndex.cs +++ b/OpenRA.Mods.RA/World/DomainIndex.cs @@ -13,15 +13,9 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; -using OpenRA.Effects; using OpenRA.FileFormats; -using OpenRA.Graphics; -using OpenRA.Network; -using OpenRA.Orders; -using OpenRA.Support; using OpenRA.Traits; using OpenRA.Mods.RA.Move; -using XRandom = OpenRA.Thirdparty.Random; namespace OpenRA.Mods.RA { @@ -30,53 +24,147 @@ namespace OpenRA.Mods.RA public class DomainIndex : IWorldLoaded { - Dictionary domains; + Dictionary domainIndexes; public void WorldLoaded(World world) { - domains = new Dictionary(); + domainIndexes = new Dictionary(); var movementClasses = new HashSet( Rules.Info.Where(ai => ai.Value.Traits.Contains()) .Select(ai => (uint)ai.Value.Traits.Get().GetMovementClass(world.TileSet))); - foreach(var mc in movementClasses) domains[mc] = new MovementClassDomainIndex(world, mc); + foreach (var mc in movementClasses) domainIndexes[mc] = new MovementClassDomainIndex(world, mc); } public bool IsPassable(CPos p1, CPos p2, uint movementClass) { - return domains[movementClass].IsPassable(p1, p2); + return domainIndexes[movementClass].IsPassable(p1, p2); + } + + /// Regenerate the domain index for a group of cells + public void UpdateCells(World world, IEnumerable cells) + { + var dirty = new HashSet(cells); + foreach (var index in domainIndexes) index.Value.UpdateCells(world, dirty); } } class MovementClassDomainIndex { Rectangle bounds; + uint movementClass; int[,] domains; + Dictionary> transientConnections; public MovementClassDomainIndex(World world, uint movementClass) { - this.movementClass = movementClass; bounds = world.Map.Bounds; + this.movementClass = movementClass; domains = new int[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)]; - BuildDomains(world); - } + transientConnections = new Dictionary>(); - public int GetDomainOf(CPos p) - { - return domains[p.X, p.Y]; + BuildDomains(world); } public bool IsPassable(CPos p1, CPos p2) { - return domains[p1.X, p1.Y] == domains[p2.X, p2.Y]; + 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 SetDomain(CPos p, int domain) + public void UpdateCells(World world, HashSet dirtyCells) + { + var neighborDomains = new List(); + + 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)); + + bool found = false; + foreach (var neighbor in neighbors) + { + if (!dirtyCells.Contains(neighbor)) + { + int neighborDomain = GetDomainOf(neighbor); + + bool 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(); + var toProcess = new Stack(); + toProcess.Push(d1); + + int i = 0; + while (toProcess.Count() > 0) + { + int 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(); + if (!transientConnections.ContainsKey(d2)) transientConnections[d2] = new HashSet(); + + transientConnections[d1].Add(d2); + transientConnections[d2].Add(d1); + } + + bool CanTraverseTile(World world, CPos p) + { + string currentTileType = WorldUtils.GetTerrainType(world, p); + int terrainOffset = world.TileSet.Terrain.OrderBy(t => t.Key).ToList().FindIndex(x => x.Key == currentTileType); + return (movementClass & (1 << terrainOffset)) > 0; + } void BuildDomains(World world) { @@ -100,9 +188,7 @@ namespace OpenRA.Mods.RA unassigned.Remove(start); // Wander around looking for water transitions - string currentTileType = WorldUtils.GetTerrainType(world, start); - int terrainOffset = world.TileSet.Terrain.OrderBy(t => t.Key).ToList().FindIndex(x => x.Key == currentTileType); - bool currentPassable = (movementClass & (1 << terrainOffset)) > 0; + bool currentPassable = CanTraverseTile(world, start); var toProcess = new Queue(); var seen = new HashSet(); @@ -114,9 +200,7 @@ namespace OpenRA.Mods.RA if (seen.Contains(p)) continue; seen.Add(p); - string candidateTileType = WorldUtils.GetTerrainType(world, p); - int candidateTerrainOffset = world.TileSet.Terrain.OrderBy(t => t.Key).ToList().FindIndex(x => x.Key == candidateTileType); - bool candidatePassable = (movementClass & (1 << candidateTerrainOffset)) > 0; + bool candidatePassable = CanTraverseTile(world, p); // Check if we're still in one contiguous domain if (currentPassable == candidatePassable) From 259e12130738770a528eca44f2557e7c74269eb5 Mon Sep 17 00:00:00 2001 From: Andrew Aldridge Date: Mon, 8 Jul 2013 23:34:34 -0400 Subject: [PATCH 6/8] Substantial performance improvement in DomainIndex * Get rid of HashSets * Cache CanTraverseTile()'s terrain offsets --- OpenRA.Mods.RA/World/DomainIndex.cs | 149 ++++++++++++++++------------ 1 file changed, 84 insertions(+), 65 deletions(-) diff --git a/OpenRA.Mods.RA/World/DomainIndex.cs b/OpenRA.Mods.RA/World/DomainIndex.cs index 99d89d012c..d23e11fdae 100644 --- a/OpenRA.Mods.RA/World/DomainIndex.cs +++ b/OpenRA.Mods.RA/World/DomainIndex.cs @@ -14,8 +14,9 @@ using System.Drawing; using System.Linq; using OpenRA.FileFormats; -using OpenRA.Traits; using OpenRA.Mods.RA.Move; +using OpenRA.Support; +using OpenRA.Traits; namespace OpenRA.Mods.RA { @@ -33,7 +34,8 @@ namespace OpenRA.Mods.RA Rules.Info.Where(ai => ai.Value.Traits.Contains()) .Select(ai => (uint)ai.Value.Traits.Get().GetMovementClass(world.TileSet))); - foreach (var mc in movementClasses) domainIndexes[mc] = new MovementClassDomainIndex(world, mc); + foreach (var mc in movementClasses) + domainIndexes[mc] = new MovementClassDomainIndex(world, mc); } public bool IsPassable(CPos p1, CPos p2, uint movementClass) @@ -45,7 +47,8 @@ namespace OpenRA.Mods.RA public void UpdateCells(World world, IEnumerable cells) { var dirty = new HashSet(cells); - foreach (var index in domainIndexes) index.Value.UpdateCells(world, dirty); + foreach (var index in domainIndexes) + index.Value.UpdateCells(world, dirty); } } @@ -57,6 +60,10 @@ namespace OpenRA.Mods.RA int[,] domains; Dictionary> transientConnections; + // Each terrain has an offset corresponding to its location in a + // movement class bitmask. This caches each offset. + Dictionary terrainOffsets; + public MovementClassDomainIndex(World world, uint movementClass) { bounds = world.Map.Bounds; @@ -64,12 +71,21 @@ namespace OpenRA.Mods.RA domains = new int[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)]; transientConnections = new Dictionary>(); + terrainOffsets = new Dictionary(); + 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; + 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. @@ -86,14 +102,14 @@ namespace OpenRA.Mods.RA var neighbors = CVec.directions.Select(d => d + cell) .Where(c => bounds.Contains(c.X, c.Y)); - bool found = false; + var found = false; foreach (var neighbor in neighbors) { if (!dirtyCells.Contains(neighbor)) { - int neighborDomain = GetDomainOf(neighbor); + var neighborDomain = GetDomainOf(neighbor); - bool match = CanTraverseTile(world, neighbor); + var match = CanTraverseTile(world, neighbor); if (match) neighborDomains.Add(neighborDomain); // Set ourselves to the first non-dirty neighbor we find. @@ -109,9 +125,7 @@ namespace OpenRA.Mods.RA foreach (var c1 in neighborDomains) { foreach (var c2 in neighborDomains) - { CreateConnection(c1, c2); - } } } @@ -132,15 +146,19 @@ namespace OpenRA.Mods.RA var toProcess = new Stack(); toProcess.Push(d1); - int i = 0; + var i = 0; while (toProcess.Count() > 0) { - int current = toProcess.Pop(); - if (!transientConnections.ContainsKey(current)) continue; + 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); + if (neighbor == d2) + return true; + if (!visited.Contains(neighbor)) + toProcess.Push(neighbor); } visited.Add(current); @@ -152,8 +170,10 @@ namespace OpenRA.Mods.RA void CreateConnection(int d1, int d2) { - if (!transientConnections.ContainsKey(d1)) transientConnections[d1] = new HashSet(); - if (!transientConnections.ContainsKey(d2)) transientConnections[d2] = new HashSet(); + if (!transientConnections.ContainsKey(d1)) + transientConnections[d1] = new HashSet(); + if (!transientConnections.ContainsKey(d2)) + transientConnections[d2] = new HashSet(); transientConnections[d1].Add(d2); transientConnections[d2].Add(d1); @@ -161,70 +181,69 @@ namespace OpenRA.Mods.RA bool CanTraverseTile(World world, CPos p) { - string currentTileType = WorldUtils.GetTerrainType(world, p); - int terrainOffset = world.TileSet.Terrain.OrderBy(t => t.Key).ToList().FindIndex(x => x.Key == currentTileType); + var currentTileType = WorldUtils.GetTerrainType(world, p); + var terrainOffset = terrainOffsets[currentTileType]; return (movementClass & (1 << terrainOffset)) > 0; } void BuildDomains(World world) { - Map map = world.Map; + var timer = new Stopwatch(); + var map = world.Map; - int i = 1; - var unassigned = new HashSet(); + var domain = 1; - // Fill up our set of yet-unassigned map cells - for (int x = map.Bounds.Left; x < bounds.Right; x += 1) + var visited = new bool[(bounds.Width + bounds.X), (bounds.Height + bounds.Y)]; + + var toProcess = new Queue(); + toProcess.Enqueue(new CPos(map.Bounds.Left, map.Bounds.Top)); + + // Flood-fill over each domain + while (toProcess.Count != 0) { - for (int y = bounds.Top; y < bounds.Bottom; y += 1) + 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(); + 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) { - unassigned.Add(new CPos(x, y)); - } - } + var n = domainQueue.Dequeue(); + if (visited[n.X, n.Y]) + continue; - while (unassigned.Count != 0) - { - var start = unassigned.First(); - unassigned.Remove(start); - - // Wander around looking for water transitions - bool currentPassable = CanTraverseTile(world, start); - - var toProcess = new Queue(); - var seen = new HashSet(); - toProcess.Enqueue(start); - - do - { - CPos p = toProcess.Dequeue(); - if (seen.Contains(p)) continue; - seen.Add(p); - - bool candidatePassable = CanTraverseTile(world, p); - - // Check if we're still in one contiguous domain - if (currentPassable == candidatePassable) + var candidatePassable = CanTraverseTile(world, n); + if (candidatePassable != currentPassable) { - SetDomain(p, i); - unassigned.Remove(p); - - // Visit our neighbors, if we haven't already - foreach (var d in CVec.directions) - { - CPos nextPos = p + d; - if (nextPos.X >= map.Bounds.Left && nextPos.Y >= map.Bounds.Top && - nextPos.X < map.Bounds.Right && nextPos.Y < map.Bounds.Bottom) - { - if (!seen.Contains(nextPos)) toProcess.Enqueue(nextPos); - } - } + toProcess.Enqueue(n); + continue; } - } while (toProcess.Count != 0); - i += 1; + 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", map.Title, i-1); + Log.Write("debug", "{0}: Found {1} domains. Took {2} s", map.Title, domain-1, timer.ElapsedTime()); } } } From 8c3769c99f0614d1880796aa4259f77fdc439780 Mon Sep 17 00:00:00 2001 From: i80and Date: Wed, 5 Jun 2013 23:11:33 -0400 Subject: [PATCH 7/8] Add self to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index de363158b7..34d60b0881 100644 --- a/AUTHORS +++ b/AUTHORS @@ -20,6 +20,7 @@ Also thanks to: * Akseli Virtanen (RAGEQUIT) * Andrew Perkins * Andrew Riedi + * Andrew Aldridge (i80and) * Andreas Beck (baxtor) * Barnaby Smith (mvi) * Bellator From 8287c5c8726ba473d4c1c9b1003bd1632dc8a364 Mon Sep 17 00:00:00 2001 From: Andrew Aldridge Date: Thu, 11 Jul 2013 19:44:49 -0400 Subject: [PATCH 8/8] Use static empty pathfinding path when possible --- OpenRA.Mods.RA/Move/PathFinder.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/OpenRA.Mods.RA/Move/PathFinder.cs b/OpenRA.Mods.RA/Move/PathFinder.cs index 92d7e15112..037f312cfb 100755 --- a/OpenRA.Mods.RA/Move/PathFinder.cs +++ b/OpenRA.Mods.RA/Move/PathFinder.cs @@ -25,6 +25,8 @@ namespace OpenRA.Mods.RA.Move public class PathFinder { + readonly static List emptyPath = new List(0); + readonly World world; public PathFinder(World world) { this.world = world; } @@ -50,7 +52,7 @@ 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(cached.result); + return emptyPath; } var mi = self.Info.Traits.Get(); @@ -61,7 +63,7 @@ namespace OpenRA.Mods.RA.Move { var passable = mi.GetMovementClass(world.TileSet); if (!domainIndex.IsPassable(from, target, (uint)passable)) - return new List(0); + return emptyPath; } var pb = FindBidiPath( @@ -103,7 +105,7 @@ namespace OpenRA.Mods.RA.Move var passable = mi.GetMovementClass(world.TileSet); tilesInRange = new List(tilesInRange.Where(t => domainIndex.IsPassable(src, t, (uint)passable))); if (tilesInRange.Count() == 0) - return new List(0); + return emptyPath; } var path = FindBidiPath( @@ -144,7 +146,7 @@ namespace OpenRA.Mods.RA.Move } // no path exists - return new List(0); + return emptyPath; } } @@ -209,7 +211,7 @@ namespace OpenRA.Mods.RA.Move return path; } - return new List(0); + return emptyPath; } }