Added cache for cell cost and blocking

This commit is contained in:
teinarss
2019-07-07 19:57:13 +02:00
committed by reaperrr
parent fb1af81280
commit cc84daacea
9 changed files with 305 additions and 102 deletions

View File

@@ -58,6 +58,13 @@ namespace OpenRA.Mods.Cnc.Traits
return info.CrushClasses.Overlaps(crushClasses);
}
bool ICrushable.TryCalculatePlayerBlocking(Actor self, BitSet<CrushClass> crushClasses, out LongBitSet<PlayerBitMask> blocking)
{
// Fall back to the slow path
blocking = default(LongBitSet<PlayerBitMask>);
return false;
}
}
[Desc("Tag trait for stuff that should not trigger mines.")]

View File

@@ -172,7 +172,8 @@ namespace OpenRA.Mods.Common.Pathfinder
int GetCostToNode(CPos destNode, CVec direction)
{
var movementCost = locomotorInfo.MovementCostToEnterCell(worldMovementInfo, Actor, destNode, IgnoreActor, checkConditions);
var movementCost = locomotor.MovementCostToEnterCell(Actor, destNode, IgnoreActor, checkConditions);
if (movementCost != int.MaxValue && !(CustomBlock != null && CustomBlock(destNode)))
return CalculateCellCost(destNode, direction, movementCost);

View File

@@ -57,6 +57,7 @@ namespace OpenRA.Mods.Common.Traits
{
readonly Actor self;
Transforms[] transforms;
Locomotor locomotor;
public TransformsIntoMobile(ActorInitializer init, TransformsIntoMobileInfo info)
: base(info)
@@ -67,6 +68,8 @@ namespace OpenRA.Mods.Common.Traits
protected override void Created(Actor self)
{
transforms = self.TraitsImplementing<Transforms>().ToArray();
locomotor = self.World.WorldActor.TraitsImplementing<Locomotor>()
.Single(l => l.Info.Name == Info.Locomotor);
base.Created(self);
}
@@ -182,21 +185,20 @@ namespace OpenRA.Mods.Common.Traits
cursor = self.World.Map.Contains(location) ?
(self.World.Map.GetTerrainInfo(location).CustomCursor ?? mobile.Info.Cursor) : mobile.Info.BlockedCursor;
var locomotor = mobile.Info.LocomotorInfo;
if (!(self.CurrentActivity is Transform || mobile.transforms.Any(t => !t.IsTraitDisabled && !t.IsTraitPaused))
|| (!explored && !locomotor.MoveIntoShroud)
|| (explored && !CanEnterCell(self.World, self, location)))
|| (!explored && !mobile.locomotor.Info.MoveIntoShroud)
|| (explored && !CanEnterCell(self, location)))
cursor = mobile.Info.BlockedCursor;
return true;
}
bool CanEnterCell(World world, Actor self, CPos cell)
bool CanEnterCell(Actor self, CPos cell)
{
if (mobile.Info.LocomotorInfo.MovementCostForCell(world, cell) == int.MaxValue)
if (mobile.locomotor.MovementCostForCell(cell) == int.MaxValue)
return false;
return mobile.Info.LocomotorInfo.CanMoveFreelyInto(world, self, cell, null, CellConditions.BlockedByMovers);
return mobile.locomotor.CanMoveFreelyInto(self, cell, null, CellConditions.BlockedByMovers);
}
}
}

View File

@@ -225,6 +225,16 @@ namespace OpenRA.Mods.Common.Traits
return self.IsAtGroundLevel() && crushClasses.Contains(info.CrushClass);
}
bool ICrushable.TryCalculatePlayerBlocking(Actor self, BitSet<CrushClass> crushClasses, out LongBitSet<PlayerBitMask> blocking)
{
if (self.IsAtGroundLevel() && crushClasses.Contains(info.CrushClass))
blocking = default(LongBitSet<PlayerBitMask>);
else
blocking = self.World.AllPlayerMask;
return true;
}
void INotifyAddedToWorld.AddedToWorld(Actor self)
{
self.World.AddToMaps(self, this);

View File

@@ -79,5 +79,15 @@ namespace OpenRA.Mods.Common.Traits
return Info.CrushClasses.Overlaps(crushClasses);
}
bool ICrushable.TryCalculatePlayerBlocking(Actor self, BitSet<CrushClass> crushClasses, out LongBitSet<PlayerBitMask> blocking)
{
if (IsTraitDisabled || !self.IsAtGroundLevel() || !Info.CrushClasses.Overlaps(crushClasses))
blocking = self.World.AllPlayerMask;
else
blocking = Info.CrushedByFriendlies ? default(LongBitSet<PlayerBitMask>) : self.Owner.AllyMask;
return true;
}
}
}

View File

@@ -76,13 +76,21 @@ namespace OpenRA.Mods.Common.Traits
public int GetInitialFacing() { return InitialFacing; }
// initialized and used by CanEnterCell
Locomotor locomotor;
public bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, bool checkTransientActors = true)
{
if (LocomotorInfo.MovementCostForCell(world, cell) == int.MaxValue)
// PERF: Avoid repeated trait queries on the hot path
if (locomotor == null)
locomotor = world.WorldActor.TraitsImplementing<Locomotor>()
.SingleOrDefault(l => l.Info.Name == Locomotor);
if (locomotor.MovementCostForCell(cell) == int.MaxValue)
return false;
var check = checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers;
return LocomotorInfo.CanMoveFreelyInto(world, self, cell, ignoreActor, check);
return locomotor.CanMoveFreelyInto(self, cell, ignoreActor, check);
}
public IReadOnlyDictionary<CPos, SubCell> OccupiedCells(ActorInfo info, CPos location, SubCell subCell = SubCell.Any)
@@ -425,12 +433,12 @@ namespace OpenRA.Mods.Common.Traits
public SubCell GetAvailableSubCell(CPos a, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, bool checkTransientActors = true)
{
var cellConditions = checkTransientActors ? CellConditions.All : CellConditions.None;
return Info.LocomotorInfo.GetAvailableSubCell(self.World, self, a, preferredSubCell, ignoreActor, cellConditions);
return Locomotor.GetAvailableSubCell(self, a, preferredSubCell, ignoreActor, cellConditions);
}
public bool CanExistInCell(CPos cell)
{
return Info.LocomotorInfo.MovementCostForCell(self.World, cell) != int.MaxValue;
return Locomotor.MovementCostForCell(cell) != int.MaxValue;
}
public bool CanEnterCell(CPos cell, Actor ignoreActor = null, bool checkTransientActors = true)
@@ -664,11 +672,6 @@ namespace OpenRA.Mods.Common.Traits
return target;
}
public bool CanMoveFreelyInto(CPos cell, Actor ignoreActor = null, bool checkTransientActors = true)
{
return Info.LocomotorInfo.CanMoveFreelyInto(self.World, self, cell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers);
}
public void EnteringCell(Actor self)
{
// Only make actor crush if it is on the ground
@@ -875,7 +878,7 @@ namespace OpenRA.Mods.Common.Traits
if (mobile.IsTraitPaused
|| (!explored && !locomotorInfo.MoveIntoShroud)
|| (explored && locomotorInfo.MovementCostForCell(self.World, location) == int.MaxValue))
|| (explored && mobile.Locomotor.MovementCostForCell(location) == int.MaxValue))
cursor = mobile.Info.BlockedCursor;
return true;

View File

@@ -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);
}
}
}

View File

@@ -78,7 +78,7 @@ namespace OpenRA.Mods.Common.Traits
return EmptyPath;
var distance = source - target;
if (source.Layer == target.Layer && distance.LengthSquared < 3 && li.CanMoveFreelyInto(world, self, target, null, CellConditions.All))
if (source.Layer == target.Layer && distance.LengthSquared < 3 && locomotor.CanMoveFreelyInto(self, target, null, CellConditions.All))
return new List<CPos> { target };
List<CPos> pb;

View File

@@ -90,6 +90,7 @@ namespace OpenRA.Mods.Common.Traits
public interface ICrushable
{
bool CrushableBy(Actor self, Actor crusher, BitSet<CrushClass> crushClasses);
bool TryCalculatePlayerBlocking(Actor self, BitSet<CrushClass> crushClasses, out LongBitSet<PlayerBitMask> blocking);
}
[RequireExplicitImplementation]