Replace DomainIndex internals with a lookup from HierarchicalPathFinder instead
Teach HierarchicalPathFinder to keep a cache of domain indices, refreshing them only on demand and when invalidated by terrain changes. This provides an accurate and quick determination for checking if paths exist between given locations. By exposing PathExistsForLocomotor on the IPathFinder interface, we can remove the DomainIndex trait entirely.
This commit is contained in:
committed by
Matthias Mailänder
parent
5a8f91aa21
commit
aef65d353d
@@ -28,12 +28,11 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads
|
||||
// Navy squad AI can exploit enemy naval production to find path, if any.
|
||||
// (Way better than finding a nearest target which is likely to be on Ground)
|
||||
// You might be tempted to move these lookups into Activate() but that causes null reference exception.
|
||||
var domainIndex = first.World.WorldActor.Trait<DomainIndex>();
|
||||
var locomotor = first.Trait<Mobile>().Locomotor;
|
||||
var mobile = first.Trait<Mobile>();
|
||||
|
||||
var navalProductions = owner.World.ActorsHavingTrait<Building>().Where(a
|
||||
=> owner.SquadManager.Info.NavalProductionTypes.Contains(a.Info.Name)
|
||||
&& domainIndex.IsPassable(first.Location, a.Location, locomotor)
|
||||
&& mobile.PathFinder.PathExistsForLocomotor(mobile.Locomotor, first.Location, a.Location)
|
||||
&& a.AppearsHostileTo(first));
|
||||
|
||||
if (navalProductions.Any())
|
||||
|
||||
@@ -323,11 +323,6 @@ namespace OpenRA.Mods.Common.Traits
|
||||
radarSignature[i++] = (c, tileInfo.GetColor(self.World.LocalRandom));
|
||||
}
|
||||
|
||||
// If this bridge repair operation connects two pathfinding domains,
|
||||
// update the domain index.
|
||||
var domainIndex = self.World.WorldActor.Trait<DomainIndex>();
|
||||
domainIndex.UpdateCells(self.World, footprint.Keys);
|
||||
|
||||
if (LongBridgeSegmentIsDead() && !killedUnits)
|
||||
{
|
||||
killedUnits = true;
|
||||
|
||||
@@ -70,8 +70,6 @@ namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
foreach (var cell in cells)
|
||||
self.World.Map.CustomTerrain[cell] = terrainIndex;
|
||||
|
||||
self.World.WorldActor.Trait<DomainIndex>().UpdateCells(self.World, cells);
|
||||
}
|
||||
|
||||
void INotifyAddedToWorld.AddedToWorld(Actor self)
|
||||
|
||||
@@ -25,13 +25,13 @@ namespace OpenRA.Mods.Common.Traits
|
||||
class ProductionFromMapEdge : Production
|
||||
{
|
||||
readonly CPos? spawnLocation;
|
||||
readonly DomainIndex domainIndex;
|
||||
readonly IPathFinder pathFinder;
|
||||
RallyPoint rp;
|
||||
|
||||
public ProductionFromMapEdge(ActorInitializer init, ProductionInfo info)
|
||||
: base(init, info)
|
||||
{
|
||||
domainIndex = init.Self.World.WorldActor.Trait<DomainIndex>();
|
||||
pathFinder = init.Self.World.WorldActor.Trait<IPathFinder>();
|
||||
|
||||
var spawnLocationInit = init.GetOrDefault<ProductionSpawnLocationInit>(info);
|
||||
if (spawnLocationInit != null)
|
||||
@@ -65,7 +65,7 @@ namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
var locomotor = self.World.WorldActor.TraitsImplementing<Locomotor>().First(l => l.Info.Name == mobileInfo.Locomotor);
|
||||
location = self.World.Map.ChooseClosestMatchingEdgeCell(self.Location,
|
||||
c => mobileInfo.CanEnterCell(self.World, null, c) && domainIndex.IsPassable(c, destinations[0], locomotor));
|
||||
c => mobileInfo.CanEnterCell(self.World, null, c) && pathFinder.PathExistsForLocomotor(locomotor, c, destinations[0]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2022 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, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[TraitLocation(SystemActors.World)]
|
||||
[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 locomotors = world.WorldActor.TraitsImplementing<Locomotor>().Where(l => !string.IsNullOrEmpty(l.Info.Name));
|
||||
var movementClasses = locomotors.Select(t => t.MovementClass).Distinct();
|
||||
|
||||
foreach (var mc in movementClasses)
|
||||
domainIndexes[mc] = new MovementClassDomainIndex(world, mc);
|
||||
}
|
||||
|
||||
public bool IsPassable(CPos p1, CPos p2, Locomotor locomotor)
|
||||
{
|
||||
// HACK: Work around units in other movement layers from being blocked
|
||||
// when the point in the main layer is not pathable
|
||||
if (p1.Layer != 0 || p2.Layer != 0)
|
||||
return true;
|
||||
|
||||
if (locomotor.Info.DisableDomainPassabilityCheck)
|
||||
return true;
|
||||
|
||||
return domainIndexes[locomotor.MovementClass].IsPassable(p1, p2);
|
||||
}
|
||||
|
||||
/// <summary>Regenerate the domain index for a group of cells.</summary>
|
||||
public void UpdateCells(World world, IEnumerable<CPos> cells)
|
||||
{
|
||||
var dirty = cells.ToHashSet();
|
||||
foreach (var index in domainIndexes)
|
||||
index.Value.UpdateCells(world, dirty);
|
||||
}
|
||||
|
||||
public void AddFixedConnection(IEnumerable<CPos> cells)
|
||||
{
|
||||
foreach (var index in domainIndexes)
|
||||
index.Value.AddFixedConnection(cells);
|
||||
}
|
||||
}
|
||||
|
||||
class MovementClassDomainIndex
|
||||
{
|
||||
readonly Map map;
|
||||
readonly uint movementClass;
|
||||
readonly CellLayer<ushort> domains;
|
||||
readonly Dictionary<ushort, HashSet<ushort>> transientConnections;
|
||||
|
||||
public MovementClassDomainIndex(World world, uint movementClass)
|
||||
{
|
||||
map = world.Map;
|
||||
this.movementClass = movementClass;
|
||||
domains = new CellLayer<ushort>(world.Map);
|
||||
transientConnections = new Dictionary<ushort, HashSet<ushort>>();
|
||||
|
||||
using (new PerfTimer($"BuildDomains: {world.Map.Title} for movement class {movementClass}"))
|
||||
BuildDomains(world);
|
||||
}
|
||||
|
||||
public bool IsPassable(CPos p1, CPos p2)
|
||||
{
|
||||
if (!domains.Contains(p1) || !domains.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<ushort>();
|
||||
|
||||
foreach (var cell in dirtyCells)
|
||||
{
|
||||
// Select all neighbors inside the map boundaries
|
||||
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);
|
||||
}
|
||||
|
||||
public void AddFixedConnection(IEnumerable<CPos> cells)
|
||||
{
|
||||
// HACK: this is a temporary workaround to add a permanent connection between the domains of the listed cells.
|
||||
// This is sufficient for fixed point-to-point tunnels, but not for dynamically updating custom layers
|
||||
// such as destroyable elevated bridges.
|
||||
// To support those the domain index will need to learn about custom movement layers, but that then requires
|
||||
// a complete refactor of the domain code to deal with MobileInfo or better a shared pathfinder class type.
|
||||
var cellDomains = cells.Select(c => domains[c]).ToHashSet();
|
||||
foreach (var c1 in cellDomains)
|
||||
foreach (var c2 in cellDomains.Where(c => c != c1))
|
||||
CreateConnection(c1, c2);
|
||||
}
|
||||
|
||||
bool HasConnection(ushort d1, ushort d2)
|
||||
{
|
||||
// Search our connections graph for a possible route
|
||||
var visited = new HashSet<ushort>();
|
||||
var toProcess = new Stack<ushort>();
|
||||
toProcess.Push(d1);
|
||||
|
||||
while (toProcess.Count > 0)
|
||||
{
|
||||
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(ushort d1, ushort d2)
|
||||
{
|
||||
if (!transientConnections.ContainsKey(d1))
|
||||
transientConnections[d1] = new HashSet<ushort>();
|
||||
if (!transientConnections.ContainsKey(d2))
|
||||
transientConnections[d2] = new HashSet<ushort>();
|
||||
|
||||
transientConnections[d1].Add(d2);
|
||||
transientConnections[d2].Add(d1);
|
||||
}
|
||||
|
||||
bool CanTraverseTile(World world, CPos p)
|
||||
{
|
||||
if (!map.Contains(p))
|
||||
return false;
|
||||
|
||||
var terrainOffset = world.Map.GetTerrainIndex(p);
|
||||
return (movementClass & (1 << terrainOffset)) > 0;
|
||||
}
|
||||
|
||||
void BuildDomains(World world)
|
||||
{
|
||||
ushort domain = 1;
|
||||
|
||||
var visited = new CellLayer<bool>(map);
|
||||
|
||||
var toProcess = new Queue<CPos>();
|
||||
toProcess.Enqueue(MPos.Zero.ToCPos(map));
|
||||
|
||||
// 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;
|
||||
|
||||
// PERF: Avoid LINQ.
|
||||
foreach (var direction in CVec.Directions)
|
||||
{
|
||||
// Don't crawl off the map, or add already-visited cells.
|
||||
var neighbor = direction + n;
|
||||
if (visited.Contains(neighbor) && !visited[neighbor])
|
||||
domainQueue.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
domain += 1;
|
||||
}
|
||||
|
||||
Log.Write("debug", "Found {0} domains for movement class {1} on map {2}.", domain - 1, movementClass, map.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ using OpenRA.Traits;
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[TraitLocation(SystemActors.World)]
|
||||
public class ElevatedBridgeLayerInfo : TraitInfo, Requires<DomainIndexInfo>, ILobbyCustomRulesIgnore, ICustomMovementLayerInfo
|
||||
public class ElevatedBridgeLayerInfo : TraitInfo, ILobbyCustomRulesIgnore, ICustomMovementLayerInfo
|
||||
{
|
||||
[Desc("Terrain type used by cells outside any elevated bridge footprint.")]
|
||||
public readonly string ImpassableTerrainType = "Impassable";
|
||||
@@ -44,7 +44,6 @@ namespace OpenRA.Mods.Common.Traits
|
||||
|
||||
public void WorldLoaded(World world, WorldRenderer wr)
|
||||
{
|
||||
var domainIndex = world.WorldActor.Trait<DomainIndex>();
|
||||
var cellHeight = world.Map.CellHeightStep.Length;
|
||||
foreach (var tti in world.WorldActor.Info.TraitInfos<ElevatedBridgePlaceholderInfo>())
|
||||
{
|
||||
@@ -61,7 +60,6 @@ namespace OpenRA.Mods.Common.Traits
|
||||
}
|
||||
|
||||
var end = tti.EndCells();
|
||||
domainIndex.AddFixedConnection(end);
|
||||
foreach (var c in end)
|
||||
{
|
||||
// Need to explicitly set both default and tunnel layers, otherwise the .Contains check will fail
|
||||
|
||||
@@ -88,10 +88,10 @@ namespace OpenRA.Mods.Common.Traits
|
||||
: new[] { Locomotor };
|
||||
foreach (var locomotor in locomotors)
|
||||
{
|
||||
var abstractGraph = pathFinder.GetOverlayDataForLocomotor(locomotor);
|
||||
var (abstractGraph, abstractDomains) = pathFinder.GetOverlayDataForLocomotor(locomotor);
|
||||
|
||||
// Locomotor doesn't allow movement, nothing to display.
|
||||
if (abstractGraph == null)
|
||||
if (abstractGraph == null || abstractDomains == null)
|
||||
continue;
|
||||
|
||||
foreach (var connectionsFromOneNode in abstractGraph)
|
||||
@@ -129,6 +129,19 @@ namespace OpenRA.Mods.Common.Traits
|
||||
yield return new TextAnnotationRenderable(font, centerPos, 0, lineColor, cost.Cost.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var domainForCell in abstractDomains)
|
||||
{
|
||||
var nodeCell = domainForCell.Key;
|
||||
var srcUv = (PPos)nodeCell.ToMPos(self.World.Map);
|
||||
if (!visibleRegion.Contains(srcUv))
|
||||
continue;
|
||||
|
||||
// Show the abstract cell and its domain index.
|
||||
var nodePos = self.World.Map.CenterOfSubCell(nodeCell, SubCell.FullCell);
|
||||
yield return new TextAnnotationRenderable(
|
||||
font, nodePos, 0, info.AbstractNodeColor, $"{domainForCell.Value}: {nodeCell}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ namespace OpenRA.Mods.Common.Traits
|
||||
world = self.World;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<CPos, List<GraphConnection>> GetOverlayDataForLocomotor(Locomotor locomotor)
|
||||
public (
|
||||
IReadOnlyDictionary<CPos, List<GraphConnection>> AbstractGraph,
|
||||
IReadOnlyDictionary<CPos, uint> AbstractDomains) GetOverlayDataForLocomotor(Locomotor locomotor)
|
||||
{
|
||||
return hierarchicalPathFindersByLocomotor[locomotor].GetOverlayData();
|
||||
}
|
||||
@@ -133,6 +135,16 @@ namespace OpenRA.Mods.Common.Traits
|
||||
return search.FindPath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a path exists between source and target.
|
||||
/// Only terrain is taken into account, i.e. as if <see cref="BlockedByActor.None"/> was given.
|
||||
/// This would apply for any actor using the given <see cref="Locomotor"/>.
|
||||
/// </summary>
|
||||
public bool PathExistsForLocomotor(Locomotor locomotor, CPos source, CPos target)
|
||||
{
|
||||
return hierarchicalPathFindersByLocomotor[locomotor].PathExists(source, target);
|
||||
}
|
||||
|
||||
static Locomotor GetActorLocomotor(Actor self)
|
||||
{
|
||||
// PERF: This PathFinder trait requires the use of Mobile, so we can be sure that is in use.
|
||||
|
||||
@@ -17,7 +17,7 @@ using OpenRA.Traits;
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[TraitLocation(SystemActors.World)]
|
||||
public class TerrainTunnelLayerInfo : TraitInfo, Requires<DomainIndexInfo>, ILobbyCustomRulesIgnore, ICustomMovementLayerInfo
|
||||
public class TerrainTunnelLayerInfo : TraitInfo, ILobbyCustomRulesIgnore, ICustomMovementLayerInfo
|
||||
{
|
||||
[Desc("Terrain type used by cells outside any tunnel footprint.")]
|
||||
public readonly string ImpassableTerrainType = "Impassable";
|
||||
@@ -43,7 +43,6 @@ namespace OpenRA.Mods.Common.Traits
|
||||
|
||||
public void WorldLoaded(World world, WorldRenderer wr)
|
||||
{
|
||||
var domainIndex = world.WorldActor.Trait<DomainIndex>();
|
||||
var cellHeight = world.Map.CellHeightStep.Length;
|
||||
foreach (var tti in world.WorldActor.Info.TraitInfos<TerrainTunnelInfo>())
|
||||
{
|
||||
@@ -60,7 +59,6 @@ namespace OpenRA.Mods.Common.Traits
|
||||
}
|
||||
|
||||
var portal = tti.PortalCells();
|
||||
domainIndex.AddFixedConnection(portal);
|
||||
foreach (var c in portal)
|
||||
{
|
||||
// Need to explicitly set both default and tunnel layers, otherwise the .Contains check will fail
|
||||
|
||||
Reference in New Issue
Block a user