diff --git a/OpenRA.Game/Primitives/SpatiallyPartitioned.cs b/OpenRA.Game/Primitives/SpatiallyPartitioned.cs index 728c711ec4..6bc9833300 100644 --- a/OpenRA.Game/Primitives/SpatiallyPartitioned.cs +++ b/OpenRA.Game/Primitives/SpatiallyPartitioned.cs @@ -86,7 +86,11 @@ namespace OpenRA.Primitives var top = (box.Top / binSize).Clamp(0, rows - 1); var bottom = (box.Bottom / binSize).Clamp(0, rows - 1); - var items = new HashSet(); + // We want to return any items intersecting the box. + // If the box covers multiple bins, we must handle items that are contained in multiple bins and avoid + // returning them more than once. We shall use a set to track these. + // PERF: If we are only looking inside one bin, we can avoid the cost of performing this tracking. + var items = top == bottom && left == right ? null : new HashSet(); for (var row = top; row <= bottom; row++) for (var col = left; col <= right; col++) { @@ -96,10 +100,12 @@ namespace OpenRA.Primitives var item = kvp.Key; var bounds = kvp.Value; - // Return items that intersect the box. We also want to avoid returning the same item many times. - // If the item is contained wholly within this bin, we're good as we know it won't show up in any others. - // Otherwise it may appear in another bin. We use a set of seen items to avoid yielding it again. - if (bounds.IntersectsWith(box) && (binBounds.Contains(bounds) || items.Add(item))) + // If the item is in the bin, we must check it intersects the box before returning it. + // We shall track it in the set of items seen so far to avoid returning it again if it appears + // in another bin. + // PERF: If the item is wholly contained within the bin, we can avoid the cost of tracking it. + if (bounds.IntersectsWith(box) && + (items == null || binBounds.Contains(bounds) || items.Add(item))) yield return item; } } diff --git a/OpenRA.Game/Traits/Player/FrozenActorLayer.cs b/OpenRA.Game/Traits/Player/FrozenActorLayer.cs index 2522a454fe..e7aed4e376 100644 --- a/OpenRA.Game/Traits/Player/FrozenActorLayer.cs +++ b/OpenRA.Game/Traits/Player/FrozenActorLayer.cs @@ -13,13 +13,17 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; using OpenRA.Graphics; +using OpenRA.Primitives; namespace OpenRA.Traits { [Desc("Required for FrozenUnderFog to work. Attach this to the player actor.")] - public class FrozenActorLayerInfo : ITraitInfo + public class FrozenActorLayerInfo : Requires, ITraitInfo { - public object Create(ActorInitializer init) { return new FrozenActorLayer(init.Self); } + [Desc("Size of partition bins (cells)")] + public readonly int BinSize = 10; + + public object Create(ActorInitializer init) { return new FrozenActorLayer(init.Self, this); } } public class FrozenActor @@ -74,13 +78,11 @@ namespace OpenRA.Traits public void Tick() { - UpdateVisibility(); - if (flashTicks > 0) flashTicks--; } - void UpdateVisibility() + public void UpdateVisibility() { var wasVisible = Visible; Shrouded = true; @@ -146,54 +148,127 @@ namespace OpenRA.Traits [Sync] public int VisibilityHash; [Sync] public int FrozenHash; + readonly int binSize; readonly World world; readonly Player owner; - readonly Dictionary frozen; + readonly Dictionary frozenActorsById; + readonly SpatiallyPartitioned partitionedFrozenActorIds; + readonly bool[] dirtyBins; + readonly HashSet dirtyFrozenActorIds = new HashSet(); - public FrozenActorLayer(Actor self) + public FrozenActorLayer(Actor self, FrozenActorLayerInfo info) { + binSize = info.BinSize; world = self.World; owner = self.Owner; - frozen = new Dictionary(); + frozenActorsById = new Dictionary(); + + // PERF: Partition the map into a series of coarse-grained bins and track changes in the shroud against + // bin - marking that bin dirty if it changes. This is fairly cheap to track and allows us to perform the + // expensive visibility update for frozen actors in these regions. + partitionedFrozenActorIds = new SpatiallyPartitioned( + world.Map.MapSize.X, world.Map.MapSize.Y, binSize); + var maxX = world.Map.MapSize.X / binSize + 1; + var maxY = world.Map.MapSize.Y / binSize + 1; + dirtyBins = new bool[maxX * maxY]; + self.Trait().CellsChanged += cells => + { + foreach (var cell in cells) + { + var x = cell.U / binSize; + var y = cell.V / binSize; + dirtyBins[y * maxX + x] = true; + } + }; } public void Add(FrozenActor fa) { - frozen.Add(fa.ID, fa); + frozenActorsById.Add(fa.ID, fa); world.ScreenMap.Add(owner, fa); + partitionedFrozenActorIds.Add(fa.ID, FootprintBounds(fa)); + } + + Rectangle FootprintBounds(FrozenActor fa) + { + var p1 = fa.Footprint[0]; + var minU = p1.U; + var maxU = p1.U; + var minV = p1.V; + var maxV = p1.V; + foreach (var p in fa.Footprint) + { + if (minU > p.U) + minU = p.U; + else if (maxU < p.U) + maxU = p.U; + + if (minV > p.V) + minV = p.V; + else if (maxV < p.V) + maxV = p.V; + } + + return Rectangle.FromLTRB(minU, minV, maxU, maxV); } public void Tick(Actor self) { - var remove = new List(); + UpdateDirtyFrozenActorsFromDirtyBins(); + + var idsToRemove = new List(); VisibilityHash = 0; FrozenHash = 0; - // TODO: Track shroud updates using Shroud.CellsChanged - // and then only tick FrozenActors that might have changed - foreach (var kvp in frozen) + foreach (var kvp in frozenActorsById) { - var hash = (int)kvp.Key; + var id = kvp.Key; + var hash = (int)id; FrozenHash += hash; var frozenActor = kvp.Value; frozenActor.Tick(); + if (dirtyFrozenActorIds.Contains(id)) + frozenActor.UpdateVisibility(); if (frozenActor.ShouldBeRemoved(owner)) - remove.Add(kvp.Key); + idsToRemove.Add(id); else if (frozenActor.Visible) VisibilityHash += hash; else if (frozenActor.Actor == null) - remove.Add(kvp.Key); + idsToRemove.Add(id); } - foreach (var actorID in remove) + dirtyFrozenActorIds.Clear(); + + foreach (var id in idsToRemove) { - world.ScreenMap.Remove(owner, frozen[actorID]); - frozen.Remove(actorID); + partitionedFrozenActorIds.Remove(id); + world.ScreenMap.Remove(owner, frozenActorsById[id]); + frozenActorsById.Remove(id); } } + void UpdateDirtyFrozenActorsFromDirtyBins() + { + // Check which bins on the map were dirtied due to changes in the shroud and gather the frozen actors whose + // footprint overlap with these bins. + var maxX = world.Map.MapSize.X / binSize + 1; + var maxY = world.Map.MapSize.Y / binSize + 1; + for (var y = 0; y < maxY; y++) + { + for (var x = 0; x < maxX; x++) + { + if (!dirtyBins[y * maxX + x]) + continue; + var box = new Rectangle(x * binSize, y * binSize, binSize, binSize); + dirtyFrozenActorIds.UnionWith(partitionedFrozenActorIds.InBox(box)); + } + } + + Array.Clear(dirtyBins, 0, dirtyBins.Length); + } + public virtual IEnumerable Render(Actor self, WorldRenderer wr) { return world.ScreenMap.FrozenActorsInBox(owner, wr.Viewport.TopLeft, wr.Viewport.BottomRight) @@ -203,11 +278,11 @@ namespace OpenRA.Traits public FrozenActor FromID(uint id) { - FrozenActor ret; - if (!frozen.TryGetValue(id, out ret)) + FrozenActor fa; + if (!frozenActorsById.TryGetValue(id, out fa)) return null; - return ret; + return fa; } } }