diff --git a/OpenRA.Mods.Cnc/Traits/Mine.cs b/OpenRA.Mods.Cnc/Traits/Mine.cs index 918269f112..b0b16c8546 100644 --- a/OpenRA.Mods.Cnc/Traits/Mine.cs +++ b/OpenRA.Mods.Cnc/Traits/Mine.cs @@ -58,6 +58,13 @@ namespace OpenRA.Mods.Cnc.Traits return info.CrushClasses.Overlaps(crushClasses); } + + bool ICrushable.TryCalculatePlayerBlocking(Actor self, BitSet crushClasses, out LongBitSet blocking) + { + // Fall back to the slow path + blocking = default(LongBitSet); + return false; + } } [Desc("Tag trait for stuff that should not trigger mines.")] diff --git a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs index 67fea85b87..9530db5f49 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs @@ -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); diff --git a/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoMobile.cs b/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoMobile.cs index 14fe590364..d3217bec27 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoMobile.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/TransformsIntoMobile.cs @@ -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().ToArray(); + locomotor = self.World.WorldActor.TraitsImplementing() + .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); } } } diff --git a/OpenRA.Mods.Common/Traits/Crates/Crate.cs b/OpenRA.Mods.Common/Traits/Crates/Crate.cs index 53c64efb61..aa29c880a8 100644 --- a/OpenRA.Mods.Common/Traits/Crates/Crate.cs +++ b/OpenRA.Mods.Common/Traits/Crates/Crate.cs @@ -225,6 +225,16 @@ namespace OpenRA.Mods.Common.Traits return self.IsAtGroundLevel() && crushClasses.Contains(info.CrushClass); } + bool ICrushable.TryCalculatePlayerBlocking(Actor self, BitSet crushClasses, out LongBitSet blocking) + { + if (self.IsAtGroundLevel() && crushClasses.Contains(info.CrushClass)) + blocking = default(LongBitSet); + else + blocking = self.World.AllPlayerMask; + + return true; + } + void INotifyAddedToWorld.AddedToWorld(Actor self) { self.World.AddToMaps(self, this); diff --git a/OpenRA.Mods.Common/Traits/Crushable.cs b/OpenRA.Mods.Common/Traits/Crushable.cs index d592599fb5..bf1c7d5680 100644 --- a/OpenRA.Mods.Common/Traits/Crushable.cs +++ b/OpenRA.Mods.Common/Traits/Crushable.cs @@ -79,5 +79,15 @@ namespace OpenRA.Mods.Common.Traits return Info.CrushClasses.Overlaps(crushClasses); } + + bool ICrushable.TryCalculatePlayerBlocking(Actor self, BitSet crushClasses, out LongBitSet blocking) + { + if (IsTraitDisabled || !self.IsAtGroundLevel() || !Info.CrushClasses.Overlaps(crushClasses)) + blocking = self.World.AllPlayerMask; + else + blocking = Info.CrushedByFriendlies ? default(LongBitSet) : self.Owner.AllyMask; + + return true; + } } } diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index e780ff1a05..dd895caf29 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -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() + .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 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; diff --git a/OpenRA.Mods.Common/Traits/World/Locomotor.cs b/OpenRA.Mods.Common/Traits/World/Locomotor.cs index 6664e6f4a7..7e3ab6958d 100644 --- a/OpenRA.Mods.Common/Traits/World/Locomotor.cs +++ b/OpenRA.Mods.Common/Traits/World/Locomotor.cs @@ -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("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(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 Blocking; + + public CellCache(short cost, LongBitSet blocking, CellBlocking cellBlocking) + { + Cost = cost; + Blocking = blocking; + CellBlocking = cellBlocking; + } + + public CellCache WithCost(short cost) + { + return new CellCache(cost, Blocking, CellBlocking); + } + + public CellCache WithBlocking(LongBitSet 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 pathabilityCache; + LongBitSet allPlayerMask = default(LongBitSet); + + LocomotorInfo.TerrainInfo[] terrainInfos; + World world; + readonly HashSet updatedTerrainCells = new HashSet(); + + 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 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(); - if (otherMobile == null || !otherMobile.CurrentMovementTypes.HasFlag(MovementType.Horizontal)) - return false; - - var selfMobile = self.TraitOrDefault(); - 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(); 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(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 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), CellBlocking.Empty); + return; + } + + var cellBlocking = CellBlocking.HasActor; + if (sharesCell && actorMap.HasFreeSubCell(cell)) + { + pathabilityCache[cell] = current.WithBlocking(default(LongBitSet), cellBlocking | CellBlocking.FreeSubCell); + return; + } + + var cellBits = default(LongBitSet); + + foreach (var actor in actors) + { + var actorBits = default(LongBitSet); + var crushables = actor.TraitsImplementing(); + var mobile = actor.OccupiesSpace as Mobile; + var isMoving = mobile != null && mobile.CurrentMovementTypes.HasFlag(MovementType.Horizontal); + + if (crushables.Any()) + { + LongBitSet 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); } } } diff --git a/OpenRA.Mods.Common/Traits/World/PathFinder.cs b/OpenRA.Mods.Common/Traits/World/PathFinder.cs index c3be1dbb85..92b2c538d8 100644 --- a/OpenRA.Mods.Common/Traits/World/PathFinder.cs +++ b/OpenRA.Mods.Common/Traits/World/PathFinder.cs @@ -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 { target }; List pb; diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index a1a2ce69b5..e5884dc10a 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -90,6 +90,7 @@ namespace OpenRA.Mods.Common.Traits public interface ICrushable { bool CrushableBy(Actor self, Actor crusher, BitSet crushClasses); + bool TryCalculatePlayerBlocking(Actor self, BitSet crushClasses, out LongBitSet blocking); } [RequireExplicitImplementation]