Fixes on blocking logic

This commit is contained in:
teinarss
2019-08-01 20:02:42 +02:00
committed by reaperrr
parent 98602cb1cb
commit 277906c657
9 changed files with 121 additions and 134 deletions

View File

@@ -76,8 +76,8 @@ namespace OpenRA
// Each player is identified with a unique bit in the set
// Cache masks for the player's index and ally/enemy player indices for performance.
public LongBitSet<PlayerBitMask> PlayerMask;
public LongBitSet<PlayerBitMask> AllyMask = default(LongBitSet<PlayerBitMask>);
public LongBitSet<PlayerBitMask> EnemyMask = default(LongBitSet<PlayerBitMask>);
public LongBitSet<PlayerBitMask> AlliedPlayersMask = default(LongBitSet<PlayerBitMask>);
public LongBitSet<PlayerBitMask> EnemyPlayersMask = default(LongBitSet<PlayerBitMask>);
public bool UnlockedRenderPlayer
{

View File

@@ -260,7 +260,7 @@ namespace OpenRA.Traits
WDist LargestActorRadius { get; }
WDist LargestBlockingActorRadius { get; }
event Action<IEnumerable<CPos>> CellsUpdated;
event Action<CPos> CellUpdated;
}
[RequireExplicitImplementation]

View File

@@ -43,7 +43,8 @@ namespace OpenRA
public readonly MersenneTwister SharedRandom;
public readonly MersenneTwister LocalRandom;
public readonly IModelCache ModelCache;
public LongBitSet<PlayerBitMask> AllPlayerMask = default(LongBitSet<PlayerBitMask>);
public LongBitSet<PlayerBitMask> AllPlayersMask = default(LongBitSet<PlayerBitMask>);
public readonly LongBitSet<PlayerBitMask> NoPlayersMask = default(LongBitSet<PlayerBitMask>);
public Player[] Players = new Player[0];
@@ -208,7 +209,7 @@ namespace OpenRA
foreach (var p in Players)
{
if (!p.Spectating)
AllPlayerMask = AllPlayerMask.Union(p.PlayerMask);
AllPlayersMask = AllPlayersMask.Union(p.PlayerMask);
foreach (var q in Players)
{
@@ -244,10 +245,10 @@ namespace OpenRA
{
case Stance.Enemy:
case Stance.Neutral:
p.EnemyMask = p.EnemyMask.Union(bitSet);
p.EnemyPlayersMask = p.EnemyPlayersMask.Union(bitSet);
break;
case Stance.Ally:
p.AllyMask = p.AllyMask.Union(bitSet);
p.AlliedPlayersMask = p.AlliedPlayersMask.Union(bitSet);
break;
}
}

View File

@@ -59,11 +59,13 @@ namespace OpenRA.Mods.Cnc.Traits
return info.CrushClasses.Overlaps(crushClasses);
}
bool ICrushable.TryCalculatePlayerBlocking(Actor self, BitSet<CrushClass> crushClasses, out LongBitSet<PlayerBitMask> blocking)
LongBitSet<PlayerBitMask> ICrushable.CrushableBy(Actor self, BitSet<CrushClass> crushClasses)
{
// Fall back to the slow path
blocking = default(LongBitSet<PlayerBitMask>);
return false;
if (!info.CrushClasses.Overlaps(crushClasses))
return self.World.NoPlayersMask;
// Friendly units should move around!
return info.BlockFriendly ? self.Owner.EnemyPlayersMask : self.World.AllPlayersMask;
}
}

View File

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

View File

@@ -65,6 +65,14 @@ namespace OpenRA.Mods.Common.Traits
return CrushableInner(crushClasses, crusher.Owner);
}
LongBitSet<PlayerBitMask> ICrushable.CrushableBy(Actor self, BitSet<CrushClass> crushClasses)
{
if (IsTraitDisabled || !self.IsAtGroundLevel() || !Info.CrushClasses.Overlaps(crushClasses))
return self.World.NoPlayersMask;
return Info.CrushedByFriendlies ? self.World.AllPlayersMask : self.Owner.EnemyPlayersMask;
}
bool CrushableInner(BitSet<CrushClass> crushClasses, Player crushOwner)
{
if (IsTraitDisabled)
@@ -79,15 +87,5 @@ 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

@@ -173,7 +173,7 @@ namespace OpenRA.Mods.Common.Traits
readonly CellLayer<InfluenceNode> influence;
readonly Dictionary<int, CellLayer<InfluenceNode>> customInfluence = new Dictionary<int, CellLayer<InfluenceNode>>();
public readonly Dictionary<int, ICustomMovementLayer> CustomMovementLayers = new Dictionary<int, ICustomMovementLayer>();
public event Action<IEnumerable<CPos>> CellsUpdated;
public event Action<CPos> CellUpdated;
readonly Bin[] bins;
readonly int rows, cols;
@@ -182,7 +182,6 @@ namespace OpenRA.Mods.Common.Traits
readonly HashSet<Actor> addActorPosition = new HashSet<Actor>();
readonly HashSet<Actor> removeActorPosition = new HashSet<Actor>();
readonly Predicate<Actor> actorShouldBeRemoved;
readonly HashSet<CPos> updatedCells = new HashSet<CPos>();
public WDist LargestActorRadius { get; private set; }
public WDist LargestBlockingActorRadius { get; private set; }
@@ -372,7 +371,8 @@ namespace OpenRA.Mods.Common.Traits
foreach (var t in triggers)
t.Dirty = true;
updatedCells.Add(c.First);
if (CellUpdated != null)
CellUpdated(c.First);
}
}
@@ -394,7 +394,8 @@ namespace OpenRA.Mods.Common.Traits
foreach (var t in triggers)
t.Dirty = true;
updatedCells.Add(c.First);
if (CellUpdated != null)
CellUpdated(c.First);
}
}
@@ -442,15 +443,6 @@ namespace OpenRA.Mods.Common.Traits
foreach (var t in proximityTriggers)
t.Value.Tick(this);
self.World.AddFrameEndTask(s =>
{
if (CellsUpdated != null)
{
CellsUpdated(updatedCells);
updatedCells.Clear();
}
});
}
public int AddCellTrigger(CPos[] cells, Action<Actor> onEntry, Action<Actor> onExit)

View File

@@ -29,12 +29,13 @@ namespace OpenRA.Mods.Common.Traits
}
[Flags]
public enum CellBlocking : byte
public enum CellFlag : byte
{
Empty = 0,
HasFreeSpace = 0,
HasActor = 1,
FreeSubCell = 2,
Crushable = 4
HasMovingActor = 2,
HasCrushableActor = 4,
HasTemporaryBlocker = 8
}
public static class LocomoterExts
@@ -45,10 +46,10 @@ namespace OpenRA.Mods.Common.Traits
return (c & cellCondition) == cellCondition;
}
public static bool HasCellBlocking(this CellBlocking c, CellBlocking cellBlocking)
public static bool HasCellFlag(this CellFlag c, CellFlag cellFlag)
{
// PERF: Enum.HasFlag is slower and requires allocations.
return (c & cellBlocking) == cellBlocking;
return (c & cellFlag) == cellFlag;
}
public static bool HasMovementType(this MovementType m, MovementType movementType)
@@ -203,35 +204,25 @@ namespace OpenRA.Mods.Common.Traits
{
struct CellCache
{
public readonly CellBlocking CellBlocking;
public readonly short Cost;
public readonly LongBitSet<PlayerBitMask> Blocking;
public readonly LongBitSet<PlayerBitMask> Crushable;
public readonly CellFlag CellFlag;
public CellCache(short cost, LongBitSet<PlayerBitMask> blocking, CellBlocking cellBlocking)
public CellCache(LongBitSet<PlayerBitMask> blocking, CellFlag cellFlag, LongBitSet<PlayerBitMask> crushable = default(LongBitSet<PlayerBitMask>))
{
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);
Crushable = crushable;
CellFlag = cellFlag;
}
}
public readonly LocomotorInfo Info;
CellLayer<CellCache> pathabilityCache;
LongBitSet<PlayerBitMask> allPlayerMask = default(LongBitSet<PlayerBitMask>);
CellLayer<short> cellsCost;
CellLayer<CellCache> blockingCache;
LocomotorInfo.TerrainInfo[] terrainInfos;
World world;
readonly HashSet<CPos> updatedTerrainCells = new HashSet<CPos>();
readonly HashSet<CPos> dirtyCells = new HashSet<CPos>();
IActorMap actorMap;
bool sharesCell;
@@ -247,7 +238,7 @@ namespace OpenRA.Mods.Common.Traits
if (!world.Map.Contains(cell))
return short.MaxValue;
return pathabilityCache[cell].Cost;
return cellsCost[cell];
}
public short MovementCostToEnterCell(Actor actor, CPos destNode, Actor ignoreActor, CellConditions check)
@@ -255,51 +246,51 @@ namespace OpenRA.Mods.Common.Traits
if (!world.Map.Contains(destNode))
return short.MaxValue;
var cellCache = pathabilityCache[destNode];
var cellCost = cellsCost[destNode];
if (cellCache.Cost == short.MaxValue ||
!CanMoveFreelyInto(actor, ignoreActor, destNode, check, cellCache))
if (cellCost == short.MaxValue ||
!CanMoveFreelyInto(actor, destNode, ignoreActor, check))
return short.MaxValue;
return cellCache.Cost;
return cellCost;
}
// 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;
var cellCache = GetCache(cell);
var cellFlag = cellCache.CellFlag;
if (!check.HasCellCondition(CellConditions.TransientActors))
return true;
// No actor in the cell or free SubCell.
if (cellFlag == CellFlag.HasFreeSpace)
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))
// All actors that may be in the cell can be crushed.
if (cellCache.Crushable.Overlaps(actor.Owner.PlayerMask))
return true;
if (sharesCell && cellBlocking.HasCellBlocking(CellBlocking.FreeSubCell))
return true;
// Cache doesn't account for ignored actors - these must use the slow path
if (ignoreActor == null)
{
// We are blocked by another actor in the cell.
if (cellCache.Blocking.Overlaps(actor.Owner.PlayerMask))
return false;
if (check == CellConditions.BlockedByMovers && cellFlag < CellFlag.HasCrushableActor)
return false;
}
foreach (var otherActor in world.ActorMap.GetActorsAt(cell))
if (IsBlockedBy(actor, otherActor, ignoreActor, check))
if (IsBlockedBy(actor, otherActor, ignoreActor, check, cellFlag))
return false;
return true;
@@ -307,12 +298,13 @@ namespace OpenRA.Mods.Common.Traits
public SubCell GetAvailableSubCell(Actor self, CPos cell, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, CellConditions check = CellConditions.All)
{
if (MovementCostForCell(cell) == short.MaxValue)
var cost = cellsCost[cell];
if (cost == short.MaxValue)
return SubCell.Invalid;
if (check.HasCellCondition(CellConditions.TransientActors))
{
Func<Actor, bool> checkTransient = otherActor => IsBlockedBy(self, otherActor, ignoreActor, check);
Func<Actor, bool> checkTransient = otherActor => IsBlockedBy(self, otherActor, ignoreActor, check, blockingCache[cell].CellFlag);
if (!sharesCell)
return world.ActorMap.AnyActorsAt(cell, SubCell.FullCell, checkTransient) ? SubCell.Invalid : SubCell.FullCell;
@@ -326,19 +318,18 @@ namespace OpenRA.Mods.Common.Traits
return world.ActorMap.FreeSubCell(cell, preferredSubCell);
}
bool IsBlockedBy(Actor self, Actor otherActor, Actor ignoreActor, CellConditions check)
bool IsBlockedBy(Actor self, Actor otherActor, Actor ignoreActor, CellConditions check, CellFlag cellFlag)
{
if (otherActor == ignoreActor)
return false;
// If the check allows: we are not blocked by allied units moving in our direction.
if (!check.HasCellCondition(CellConditions.BlockedByMovers) &&
if (!check.HasCellCondition(CellConditions.BlockedByMovers) && cellFlag.HasCellFlag(CellFlag.HasMovingActor) &&
self.Owner.Stances[otherActor.Owner] == Stance.Ally &&
IsMovingInMyDirection(self, otherActor))
return false;
// PERF: Only perform ITemporaryBlocker trait look-up if mod/map rules contain any actors that are temporary blockers
if (self.World.RulesContainTemporaryBlocker)
if (cellFlag.HasCellFlag(CellFlag.HasTemporaryBlocker))
{
// If there is a temporary blocker in our path, but we can remove it, we are not blocked.
var temporaryBlocker = otherActor.TraitOrDefault<ITemporaryBlocker>();
@@ -346,6 +337,9 @@ namespace OpenRA.Mods.Common.Traits
return false;
}
if (!cellFlag.HasCellFlag(CellFlag.HasCrushableActor))
return true;
// If we cannot crush the other actor in our way, we are blocked.
if (Info.Crushes.IsEmpty)
return true;
@@ -382,29 +376,34 @@ namespace OpenRA.Mods.Common.Traits
world = w;
var map = w.Map;
actorMap = w.ActorMap;
actorMap.CellsUpdated += CellsUpdated;
pathabilityCache = new CellLayer<CellCache>(map);
actorMap.CellUpdated += CellUpdated;
blockingCache = new CellLayer<CellCache>(map);
cellsCost = new CellLayer<short>(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;
map.CustomTerrain.CellEntryChanged += UpdateCellCost;
map.Tiles.CellEntryChanged += UpdateCellCost;
}
void CellsUpdated(IEnumerable<CPos> cells)
CellCache GetCache(CPos cell)
{
foreach (var cell in cells)
if (dirtyCells.Contains(cell))
{
UpdateCellBlocking(cell);
dirtyCells.Remove(cell);
}
foreach (var cell in updatedTerrainCells)
UpdateCellCost(cell);
return blockingCache[cell];
}
updatedTerrainCells.Clear();
void CellUpdated(CPos cell)
{
dirtyCells.Add(cell);
}
void UpdateCellCost(CPos cell)
@@ -418,66 +417,66 @@ namespace OpenRA.Mods.Common.Traits
if (index != byte.MaxValue)
cost = terrainInfos[index].Cost;
var current = pathabilityCache[cell];
pathabilityCache[cell] = current.WithCost(cost);
cellsCost[cell] = cost;
}
void UpdateCellBlocking(CPos cell)
{
using (new PerfSample("locomotor_cache"))
{
var current = pathabilityCache[cell];
var actors = actorMap.GetActorsAt(cell);
if (!actors.Any())
{
pathabilityCache[cell] = current.WithBlocking(default(LongBitSet<PlayerBitMask>), CellBlocking.Empty);
blockingCache[cell] = new CellCache(default(LongBitSet<PlayerBitMask>), CellFlag.HasFreeSpace);
return;
}
var cellBlocking = CellBlocking.HasActor;
if (sharesCell && actorMap.HasFreeSubCell(cell))
{
pathabilityCache[cell] = current.WithBlocking(default(LongBitSet<PlayerBitMask>), cellBlocking | CellBlocking.FreeSubCell);
blockingCache[cell] = new CellCache(default(LongBitSet<PlayerBitMask>), CellFlag.HasFreeSpace);
return;
}
var cellBits = default(LongBitSet<PlayerBitMask>);
var cellFlag = CellFlag.HasActor;
var cellBlockedPlayers = default(LongBitSet<PlayerBitMask>);
var cellCrushablePlayers = world.AllPlayersMask;
foreach (var actor in actors)
{
var actorBits = default(LongBitSet<PlayerBitMask>);
var actorBlocksPlayers = world.AllPlayersMask;
var crushables = actor.TraitsImplementing<ICrushable>();
var mobile = actor.OccupiesSpace as Mobile;
var isMoving = mobile != null && mobile.CurrentMovementTypes.HasFlag(MovementType.Horizontal);
var isMoving = mobile != null && mobile.CurrentMovementTypes.HasMovementType(MovementType.Horizontal);
if (crushables.Any())
{
LongBitSet<PlayerBitMask> crushingBits;
cellFlag |= CellFlag.HasCrushableActor;
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);
cellCrushablePlayers = cellCrushablePlayers.Intersect(crushable.CrushableBy(actor, Info.Crushes));
}
else
actorBits = isMoving ? actor.Owner.EnemyMask : allPlayerMask;
cellCrushablePlayers = world.NoPlayersMask;
cellBits = cellBits.Union(actorBits);
if (isMoving)
{
actorBlocksPlayers = actorBlocksPlayers.Except(actor.Owner.AlliedPlayersMask);
cellFlag |= CellFlag.HasMovingActor;
}
// PERF: Only perform ITemporaryBlocker trait look-up if mod/map rules contain any actors that are temporary blockers
if (world.RulesContainTemporaryBlocker)
{
// If there is a temporary blocker in this cell.
if (actor.TraitOrDefault<ITemporaryBlocker>() != null)
cellFlag |= CellFlag.HasTemporaryBlocker;
}
cellBlockedPlayers = cellBlockedPlayers.Union(actorBlocksPlayers);
}
pathabilityCache[cell] = current.WithBlocking(cellBits, cellBlocking);
blockingCache[cell] = new CellCache(cellBlockedPlayers, cellFlag, cellCrushablePlayers);
}
}
void MapCellEntryChanged(CPos cell)
{
updatedTerrainCells.Add(cell);
}
}
}

View File

@@ -90,7 +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);
LongBitSet<PlayerBitMask> CrushableBy(Actor self, BitSet<CrushClass> crushClasses);
}
[RequireExplicitImplementation]