Added cache for cell cost and blocking
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
@@ -26,13 +27,34 @@ namespace OpenRA.Mods.Common.Traits
|
||||
All = TransientActors | BlockedByMovers
|
||||
}
|
||||
|
||||
public static class CellConditionsExts
|
||||
[Flags]
|
||||
public enum CellBlocking : byte
|
||||
{
|
||||
Empty = 0,
|
||||
HasActor = 1,
|
||||
FreeSubCell = 2,
|
||||
Crushable = 4
|
||||
}
|
||||
|
||||
public static class LocomoterExts
|
||||
{
|
||||
public static bool HasCellCondition(this CellConditions c, CellConditions cellCondition)
|
||||
{
|
||||
// PERF: Enum.HasFlag is slower and requires allocations.
|
||||
return (c & cellCondition) == cellCondition;
|
||||
}
|
||||
|
||||
public static bool HasCellBlocking(this CellBlocking c, CellBlocking cellBlocking)
|
||||
{
|
||||
// PERF: Enum.HasFlag is slower and requires allocations.
|
||||
return (c & cellBlocking) == cellBlocking;
|
||||
}
|
||||
|
||||
public static bool HasMovementType(this MovementType m, MovementType movementType)
|
||||
{
|
||||
// PERF: Enum.HasFlag is slower and requires allocations.
|
||||
return (m & movementType) == movementType;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CustomMovementLayerType
|
||||
@@ -78,9 +100,9 @@ namespace OpenRA.Mods.Common.Traits
|
||||
if (speed > 0)
|
||||
{
|
||||
var nodesDict = t.Value.ToDictionary();
|
||||
var cost = nodesDict.ContainsKey("PathingCost")
|
||||
var cost = (nodesDict.ContainsKey("PathingCost")
|
||||
? FieldLoader.GetValue<short>("cost", nodesDict["PathingCost"].Value)
|
||||
: 10000 / speed;
|
||||
: 10000 / speed);
|
||||
ret.Add(t.Key, new TerrainInfo(speed, (short)cost));
|
||||
}
|
||||
}
|
||||
@@ -146,25 +168,6 @@ namespace OpenRA.Mods.Common.Traits
|
||||
TilesetMovementClass = new Cache<TileSet, int>(CalculateTilesetMovementClass);
|
||||
}
|
||||
|
||||
public int MovementCostForCell(World world, CPos cell)
|
||||
{
|
||||
return MovementCostForCell(world, TilesetTerrainInfo[world.Map.Rules.TileSet], cell);
|
||||
}
|
||||
|
||||
int MovementCostForCell(World world, TerrainInfo[] terrainInfos, CPos cell)
|
||||
{
|
||||
if (!world.Map.Contains(cell))
|
||||
return int.MaxValue;
|
||||
|
||||
var index = cell.Layer == 0 ? world.Map.GetTerrainIndex(cell) :
|
||||
world.GetCustomMovementLayers()[cell.Layer].GetTerrainIndex(cell);
|
||||
|
||||
if (index == byte.MaxValue)
|
||||
return int.MaxValue;
|
||||
|
||||
return terrainInfos[index].Cost;
|
||||
}
|
||||
|
||||
public int CalculateTilesetMovementClass(TileSet tileset)
|
||||
{
|
||||
// collect our ability to cross *all* terraintypes, in a bitvector
|
||||
@@ -190,80 +193,143 @@ namespace OpenRA.Mods.Common.Traits
|
||||
return new WorldMovementInfo(world, this);
|
||||
}
|
||||
|
||||
public int MovementCostToEnterCell(WorldMovementInfo worldMovementInfo, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All)
|
||||
public virtual bool DisableDomainPassabilityCheck { get { return false; } }
|
||||
|
||||
public virtual object Create(ActorInitializer init) { return new Locomotor(init.Self, this); }
|
||||
}
|
||||
|
||||
public class Locomotor : IWorldLoaded
|
||||
{
|
||||
struct CellCache
|
||||
{
|
||||
var cost = MovementCostForCell(worldMovementInfo.World, worldMovementInfo.TerrainInfos, cell);
|
||||
if (cost == int.MaxValue || !CanMoveFreelyInto(worldMovementInfo.World, self, cell, ignoreActor, check))
|
||||
return int.MaxValue;
|
||||
return cost;
|
||||
public readonly CellBlocking CellBlocking;
|
||||
public readonly short Cost;
|
||||
public readonly LongBitSet<PlayerBitMask> Blocking;
|
||||
|
||||
public CellCache(short cost, LongBitSet<PlayerBitMask> blocking, CellBlocking cellBlocking)
|
||||
{
|
||||
Cost = cost;
|
||||
Blocking = blocking;
|
||||
CellBlocking = cellBlocking;
|
||||
}
|
||||
|
||||
public CellCache WithCost(short cost)
|
||||
{
|
||||
return new CellCache(cost, Blocking, CellBlocking);
|
||||
}
|
||||
|
||||
public CellCache WithBlocking(LongBitSet<PlayerBitMask> blocking, CellBlocking cellBlocking)
|
||||
{
|
||||
return new CellCache(Cost, blocking, cellBlocking);
|
||||
}
|
||||
}
|
||||
|
||||
public SubCell GetAvailableSubCell(
|
||||
World world, Actor self, CPos cell, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, CellConditions check = CellConditions.All)
|
||||
public readonly LocomotorInfo Info;
|
||||
CellLayer<CellCache> pathabilityCache;
|
||||
LongBitSet<PlayerBitMask> allPlayerMask = default(LongBitSet<PlayerBitMask>);
|
||||
|
||||
LocomotorInfo.TerrainInfo[] terrainInfos;
|
||||
World world;
|
||||
readonly HashSet<CPos> updatedTerrainCells = new HashSet<CPos>();
|
||||
|
||||
IActorMap actorMap;
|
||||
bool sharesCell;
|
||||
|
||||
public Locomotor(Actor self, LocomotorInfo info)
|
||||
{
|
||||
if (MovementCostForCell(world, cell) == int.MaxValue)
|
||||
Info = info;
|
||||
sharesCell = info.SharesCell;
|
||||
}
|
||||
|
||||
public int MovementCostForCell(CPos cell)
|
||||
{
|
||||
if (!world.Map.Contains(cell))
|
||||
return int.MaxValue;
|
||||
|
||||
return pathabilityCache[cell].Cost;
|
||||
}
|
||||
|
||||
public int MovementCostToEnterCell(Actor actor, CPos destNode, Actor ignoreActor, CellConditions check)
|
||||
{
|
||||
if (!world.Map.Contains(destNode))
|
||||
return int.MaxValue;
|
||||
|
||||
var cellCache = pathabilityCache[destNode];
|
||||
|
||||
if (cellCache.Cost == short.MaxValue ||
|
||||
!CanMoveFreelyInto(actor, ignoreActor, destNode, check, cellCache))
|
||||
return int.MaxValue;
|
||||
|
||||
return cellCache.Cost;
|
||||
}
|
||||
|
||||
// Determines whether the actor is blocked by other Actors
|
||||
public bool CanMoveFreelyInto(Actor actor, CPos cell, Actor ignoreActor, CellConditions check)
|
||||
{
|
||||
return CanMoveFreelyInto(actor, ignoreActor, cell, check, pathabilityCache[cell]);
|
||||
}
|
||||
|
||||
bool CanMoveFreelyInto(Actor actor, Actor ignoreActor, CPos cell, CellConditions check, CellCache cellCache)
|
||||
{
|
||||
var cellBlocking = cellCache.CellBlocking;
|
||||
var blocking = cellCache.Blocking;
|
||||
|
||||
if (!check.HasCellCondition(CellConditions.TransientActors))
|
||||
return true;
|
||||
|
||||
// If actor is null we're just checking what would happen theoretically.
|
||||
// In such a scenario - we'll just assume any other actor in the cell will block us by default.
|
||||
// If we have a real actor, we can then perform the extra checks that allow us to avoid being blocked.
|
||||
if (actor == null)
|
||||
return true;
|
||||
|
||||
// No actor in cell
|
||||
if (cellBlocking == CellBlocking.Empty)
|
||||
return true;
|
||||
|
||||
// We are blocked
|
||||
if (ignoreActor == null && blocking.Overlaps(actor.Owner.PlayerMask))
|
||||
return false;
|
||||
|
||||
if (cellBlocking.HasCellBlocking(CellBlocking.Crushable))
|
||||
return true;
|
||||
|
||||
if (sharesCell && cellBlocking.HasCellBlocking(CellBlocking.FreeSubCell))
|
||||
return true;
|
||||
|
||||
foreach (var otherActor in world.ActorMap.GetActorsAt(cell))
|
||||
if (IsBlockedBy(actor, otherActor, ignoreActor, check))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public SubCell GetAvailableSubCell(Actor self, CPos cell, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, CellConditions check = CellConditions.All)
|
||||
{
|
||||
if (MovementCostForCell(cell) == int.MaxValue)
|
||||
return SubCell.Invalid;
|
||||
|
||||
if (check.HasCellCondition(CellConditions.TransientActors))
|
||||
{
|
||||
Func<Actor, bool> checkTransient = otherActor => IsBlockedBy(self, otherActor, ignoreActor, check);
|
||||
|
||||
if (!SharesCell)
|
||||
if (!sharesCell)
|
||||
return world.ActorMap.AnyActorsAt(cell, SubCell.FullCell, checkTransient) ? SubCell.Invalid : SubCell.FullCell;
|
||||
|
||||
return world.ActorMap.FreeSubCell(cell, preferredSubCell, checkTransient);
|
||||
}
|
||||
|
||||
if (!SharesCell)
|
||||
if (!sharesCell)
|
||||
return world.ActorMap.AnyActorsAt(cell, SubCell.FullCell) ? SubCell.Invalid : SubCell.FullCell;
|
||||
|
||||
return world.ActorMap.FreeSubCell(cell, preferredSubCell);
|
||||
}
|
||||
|
||||
static bool IsMovingInMyDirection(Actor self, Actor other)
|
||||
{
|
||||
var otherMobile = other.TraitOrDefault<Mobile>();
|
||||
if (otherMobile == null || !otherMobile.CurrentMovementTypes.HasFlag(MovementType.Horizontal))
|
||||
return false;
|
||||
|
||||
var selfMobile = self.TraitOrDefault<Mobile>();
|
||||
if (selfMobile == null)
|
||||
return false;
|
||||
|
||||
// Moving in the same direction if the facing delta is between +/- 90 degrees
|
||||
var delta = Util.NormalizeFacing(otherMobile.Facing - selfMobile.Facing);
|
||||
return delta < 64 || delta > 192;
|
||||
}
|
||||
|
||||
// Determines whether the actor is blocked by other Actors
|
||||
public bool CanMoveFreelyInto(World world, Actor self, CPos cell, Actor ignoreActor, CellConditions check)
|
||||
{
|
||||
if (!check.HasCellCondition(CellConditions.TransientActors))
|
||||
return true;
|
||||
|
||||
if (SharesCell && world.ActorMap.HasFreeSubCell(cell))
|
||||
return true;
|
||||
|
||||
// PERF: Avoid LINQ.
|
||||
foreach (var otherActor in world.ActorMap.GetActorsAt(cell))
|
||||
if (IsBlockedBy(self, otherActor, ignoreActor, check))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsBlockedBy(Actor self, Actor otherActor, Actor ignoreActor, CellConditions check)
|
||||
{
|
||||
// We are not blocked by the actor we are ignoring.
|
||||
if (otherActor == ignoreActor)
|
||||
return false;
|
||||
|
||||
// If self is null, we don't have a real actor - we're just checking what would happen theoretically.
|
||||
// In such a scenario - we'll just assume any other actor in the cell will block us by default.
|
||||
// If we have a real actor, we can then perform the extra checks that allow us to avoid being blocked.
|
||||
if (self == null)
|
||||
return true;
|
||||
|
||||
// If the check allows: we are not blocked by allied units moving in our direction.
|
||||
if (!check.HasCellCondition(CellConditions.BlockedByMovers) &&
|
||||
self.Owner.Stances[otherActor.Owner] == Stance.Ally &&
|
||||
@@ -280,31 +346,134 @@ namespace OpenRA.Mods.Common.Traits
|
||||
}
|
||||
|
||||
// If we cannot crush the other actor in our way, we are blocked.
|
||||
if (Crushes.IsEmpty)
|
||||
if (Info.Crushes.IsEmpty)
|
||||
return true;
|
||||
|
||||
// If the other actor in our way cannot be crushed, we are blocked.
|
||||
// PERF: Avoid LINQ.
|
||||
var crushables = otherActor.TraitsImplementing<ICrushable>();
|
||||
foreach (var crushable in crushables)
|
||||
if (crushable.CrushableBy(otherActor, self, Crushes))
|
||||
if (crushable.CrushableBy(otherActor, self, Info.Crushes))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual bool DisableDomainPassabilityCheck { get { return false; } }
|
||||
|
||||
public virtual object Create(ActorInitializer init) { return new Locomotor(init.Self, this); }
|
||||
}
|
||||
|
||||
public class Locomotor
|
||||
{
|
||||
public readonly LocomotorInfo Info;
|
||||
|
||||
public Locomotor(Actor self, LocomotorInfo info)
|
||||
static bool IsMovingInMyDirection(Actor self, Actor other)
|
||||
{
|
||||
Info = info;
|
||||
// PERF: Because we can be sure that OccupiesSpace is Mobile here we can save some performance by avoiding querying for the trait.
|
||||
var otherMobile = other.OccupiesSpace as Mobile;
|
||||
if (otherMobile == null || !otherMobile.CurrentMovementTypes.HasMovementType(MovementType.Horizontal))
|
||||
return false;
|
||||
|
||||
// PERF: Same here.
|
||||
var selfMobile = self.OccupiesSpace as Mobile;
|
||||
if (selfMobile == null)
|
||||
return false;
|
||||
|
||||
// Moving in the same direction if the facing delta is between +/- 90 degrees
|
||||
var delta = Util.NormalizeFacing(otherMobile.Facing - selfMobile.Facing);
|
||||
return delta < 64 || delta > 192;
|
||||
}
|
||||
|
||||
public void WorldLoaded(World w, WorldRenderer wr)
|
||||
{
|
||||
world = w;
|
||||
var map = w.Map;
|
||||
actorMap = w.ActorMap;
|
||||
actorMap.CellsUpdated += CellsUpdated;
|
||||
pathabilityCache = new CellLayer<CellCache>(map);
|
||||
|
||||
terrainInfos = Info.TilesetTerrainInfo[map.Rules.TileSet];
|
||||
|
||||
allPlayerMask = world.AllPlayerMask;
|
||||
|
||||
foreach (var cell in map.AllCells)
|
||||
UpdateCellCost(cell);
|
||||
|
||||
map.CustomTerrain.CellEntryChanged += MapCellEntryChanged;
|
||||
map.Tiles.CellEntryChanged += MapCellEntryChanged;
|
||||
}
|
||||
|
||||
void CellsUpdated(IEnumerable<CPos> cells)
|
||||
{
|
||||
foreach (var cell in cells)
|
||||
UpdateCellBlocking(cell);
|
||||
|
||||
foreach (var cell in updatedTerrainCells)
|
||||
UpdateCellCost(cell);
|
||||
|
||||
updatedTerrainCells.Clear();
|
||||
}
|
||||
|
||||
void UpdateCellCost(CPos cell)
|
||||
{
|
||||
var index = cell.Layer == 0
|
||||
? world.Map.GetTerrainIndex(cell)
|
||||
: world.GetCustomMovementLayers()[cell.Layer].GetTerrainIndex(cell);
|
||||
|
||||
var cost = short.MaxValue;
|
||||
|
||||
if (index != byte.MaxValue)
|
||||
cost = terrainInfos[index].Cost;
|
||||
|
||||
var current = pathabilityCache[cell];
|
||||
pathabilityCache[cell] = current.WithCost(cost);
|
||||
}
|
||||
|
||||
void UpdateCellBlocking(CPos cell)
|
||||
{
|
||||
var current = pathabilityCache[cell];
|
||||
|
||||
var actors = actorMap.GetActorsAt(cell);
|
||||
|
||||
if (!actors.Any())
|
||||
{
|
||||
pathabilityCache[cell] = current.WithBlocking(default(LongBitSet<PlayerBitMask>), CellBlocking.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
var cellBlocking = CellBlocking.HasActor;
|
||||
if (sharesCell && actorMap.HasFreeSubCell(cell))
|
||||
{
|
||||
pathabilityCache[cell] = current.WithBlocking(default(LongBitSet<PlayerBitMask>), cellBlocking | CellBlocking.FreeSubCell);
|
||||
return;
|
||||
}
|
||||
|
||||
var cellBits = default(LongBitSet<PlayerBitMask>);
|
||||
|
||||
foreach (var actor in actors)
|
||||
{
|
||||
var actorBits = default(LongBitSet<PlayerBitMask>);
|
||||
var crushables = actor.TraitsImplementing<ICrushable>();
|
||||
var mobile = actor.OccupiesSpace as Mobile;
|
||||
var isMoving = mobile != null && mobile.CurrentMovementTypes.HasFlag(MovementType.Horizontal);
|
||||
|
||||
if (crushables.Any())
|
||||
{
|
||||
LongBitSet<PlayerBitMask> crushingBits;
|
||||
foreach (var crushable in crushables)
|
||||
if (crushable.TryCalculatePlayerBlocking(actor, Info.Crushes, out crushingBits))
|
||||
{
|
||||
actorBits = actorBits.Union(crushingBits);
|
||||
cellBlocking |= CellBlocking.Crushable;
|
||||
}
|
||||
|
||||
if (isMoving)
|
||||
actorBits = actorBits.Except(actor.Owner.AllyMask);
|
||||
}
|
||||
else
|
||||
actorBits = isMoving ? actor.Owner.EnemyMask : allPlayerMask;
|
||||
|
||||
cellBits = cellBits.Union(actorBits);
|
||||
}
|
||||
|
||||
pathabilityCache[cell] = current.WithBlocking(cellBits, cellBlocking);
|
||||
}
|
||||
|
||||
void MapCellEntryChanged(CPos cell)
|
||||
{
|
||||
updatedTerrainCells.Add(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user