From 09dc1db6517396a61718e67f10bbbd8d18b28dd6 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Fri, 3 Apr 2015 00:21:03 +0100 Subject: [PATCH] Refactored ScreenMap & improved perf of updates, removals and region lookups. Reduce code duplication by extracting a common class to deal with spatial partitioning of actors, and use some (cached) delegates to reduce duplication further without affecting performance too much. Speed up updates and removal of actors by caching their location so we only need to update or remove them from bins they are actually in (typically very few), compared to having to check every bin for removals which is much more work in comparison. Speed up checking for actors inside a region by checking if items are located entirely within the bin they are located in. If so, we don't need to add them to the hash-set for de-duplication purposes which is fairly expensive. --- OpenRA.Game/OpenRA.Game.csproj | 1 + .../Primitives/SpatiallyPartitioned.cs | 108 +++++++++++++ OpenRA.Game/Traits/World/ScreenMap.cs | 142 ++++++------------ 3 files changed, 154 insertions(+), 97 deletions(-) create mode 100644 OpenRA.Game/Primitives/SpatiallyPartitioned.cs diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 053f561dca..f3bfb5ba67 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -143,6 +143,7 @@ + diff --git a/OpenRA.Game/Primitives/SpatiallyPartitioned.cs b/OpenRA.Game/Primitives/SpatiallyPartitioned.cs new file mode 100644 index 0000000000..728c711ec4 --- /dev/null +++ b/OpenRA.Game/Primitives/SpatiallyPartitioned.cs @@ -0,0 +1,108 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace OpenRA.Primitives +{ + public sealed class SpatiallyPartitioned + { + readonly int rows, cols, binSize; + readonly Dictionary[] itemBoundsBins; + readonly Dictionary itemBounds = new Dictionary(); + readonly Action, T, Rectangle> addItem = (bin, actor, bounds) => bin.Add(actor, bounds); + readonly Action, T, Rectangle> removeItem = (bin, actor, bounds) => bin.Remove(actor); + + public SpatiallyPartitioned(int width, int height, int binSize) + { + this.binSize = binSize; + rows = height / binSize + 1; + cols = width / binSize + 1; + itemBoundsBins = Exts.MakeArray(rows * cols, _ => new Dictionary()); + } + + public void Add(T item, Rectangle bounds) + { + itemBounds.Add(item, bounds); + MutateBins(item, bounds, addItem); + } + + public void Update(T item, Rectangle bounds) + { + MutateBins(item, itemBounds[item], removeItem); + MutateBins(item, itemBounds[item] = bounds, addItem); + } + + public void Remove(T item) + { + MutateBins(item, itemBounds[item], removeItem); + itemBounds.Remove(item); + } + + Dictionary BinAt(int row, int col) + { + return itemBoundsBins[row * cols + col]; + } + + Rectangle BinBounds(int row, int col) + { + return new Rectangle(col * binSize, row * binSize, binSize, binSize); + } + + void MutateBins(T actor, Rectangle bounds, Action, T, Rectangle> action) + { + var top = Math.Max(0, bounds.Top / binSize); + var left = Math.Max(0, bounds.Left / binSize); + var bottom = Math.Min(rows - 1, bounds.Bottom / binSize); + var right = Math.Min(cols - 1, bounds.Right / binSize); + + for (var row = top; row <= bottom; row++) + for (var col = left; col <= right; col++) + action(BinAt(row, col), actor, bounds); + } + + public IEnumerable At(int2 location) + { + var col = (location.X / binSize).Clamp(0, cols - 1); + var row = (location.Y / binSize).Clamp(0, rows - 1); + foreach (var kvp in BinAt(row, col)) + if (kvp.Value.Contains(location)) + yield return kvp.Key; + } + + public IEnumerable InBox(Rectangle box) + { + var left = (box.Left / binSize).Clamp(0, cols - 1); + var right = (box.Right / binSize).Clamp(0, cols - 1); + var top = (box.Top / binSize).Clamp(0, rows - 1); + var bottom = (box.Bottom / binSize).Clamp(0, rows - 1); + + var items = new HashSet(); + for (var row = top; row <= bottom; row++) + for (var col = left; col <= right; col++) + { + var binBounds = BinBounds(row, col); + foreach (var kvp in BinAt(row, col)) + { + 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))) + yield return item; + } + } + } + } +} diff --git a/OpenRA.Game/Traits/World/ScreenMap.cs b/OpenRA.Game/Traits/World/ScreenMap.cs index 0a1cf9af7c..a4415e723e 100644 --- a/OpenRA.Game/Traits/World/ScreenMap.cs +++ b/OpenRA.Game/Traits/World/ScreenMap.cs @@ -27,99 +27,71 @@ namespace OpenRA.Traits public class ScreenMap : IWorldLoaded { - ScreenMapInfo info; + static readonly IEnumerable NoFrozenActors = new FrozenActor[0]; + readonly Func frozenActorIsValid = fa => fa.IsValid; + readonly Func actorIsInWorld = a => a.IsInWorld; + readonly Cache> partitionedFrozenActors; + readonly SpatiallyPartitioned partitionedActors; WorldRenderer worldRenderer; - Cache[]> frozen; - Dictionary[] actors; - int rows, cols; public ScreenMap(World world, ScreenMapInfo info) { - this.info = info; var ts = Game.ModData.Manifest.TileSize; - cols = world.Map.MapSize.X * ts.Width / info.BinSize + 1; - rows = world.Map.MapSize.Y * ts.Height / info.BinSize + 1; - - frozen = new Cache[]>(InitializeFrozenActors); - actors = new Dictionary[rows * cols]; - for (var j = 0; j < rows; j++) - for (var i = 0; i < cols; i++) - actors[j * cols + i] = new Dictionary(); + var width = world.Map.MapSize.X * ts.Width; + var height = world.Map.MapSize.Y * ts.Height; + partitionedFrozenActors = new Cache>( + _ => new SpatiallyPartitioned(width, height, info.BinSize)); + partitionedActors = new SpatiallyPartitioned(width, height, info.BinSize); } public void WorldLoaded(World w, WorldRenderer wr) { worldRenderer = wr; } - Dictionary[] InitializeFrozenActors(Player p) - { - var f = new Dictionary[rows * cols]; - for (var j = 0; j < rows; j++) - for (var i = 0; i < cols; i++) - f[j * cols + i] = new Dictionary(); - - return f; - } - - public void Add(Player viewer, FrozenActor fa) + Rectangle FrozenActorBounds(FrozenActor fa) { var pos = worldRenderer.ScreenPxPosition(fa.CenterPosition); var bounds = fa.Bounds; bounds.Offset(pos.X, pos.Y); - - var top = Math.Max(0, bounds.Top / info.BinSize); - var left = Math.Max(0, bounds.Left / info.BinSize); - var bottom = Math.Min(rows - 1, bounds.Bottom / info.BinSize); - var right = Math.Min(cols - 1, bounds.Right / info.BinSize); - - for (var j = top; j <= bottom; j++) - for (var i = left; i <= right; i++) - frozen[viewer][j * cols + i].Add(fa, bounds); + return bounds; } - public void Remove(Player viewer, FrozenActor fa) - { - foreach (var bin in frozen[viewer]) - bin.Remove(fa); - } - - public void Add(Actor a) + Rectangle ActorBounds(Actor a) { var pos = worldRenderer.ScreenPxPosition(a.CenterPosition); var bounds = a.Bounds; bounds.Offset(pos.X, pos.Y); - - var top = Math.Max(0, bounds.Top / info.BinSize); - var left = Math.Max(0, bounds.Left / info.BinSize); - var bottom = Math.Min(rows - 1, bounds.Bottom / info.BinSize); - var right = Math.Min(cols - 1, bounds.Right / info.BinSize); - - for (var j = top; j <= bottom; j++) - for (var i = left; i <= right; i++) - actors[j * cols + i].Add(a, bounds); + return bounds; } - public void Remove(Actor a) + public void Add(Player viewer, FrozenActor fa) { - foreach (var bin in actors) - bin.Remove(a); + partitionedFrozenActors[viewer].Add(fa, FrozenActorBounds(fa)); + } + + public void Remove(Player viewer, FrozenActor fa) + { + partitionedFrozenActors[viewer].Remove(fa); + } + + public void Add(Actor a) + { + partitionedActors.Add(a, ActorBounds(a)); } public void Update(Actor a) { - Remove(a); - Add(a); + partitionedActors.Update(a, ActorBounds(a)); + } + + public void Remove(Actor a) + { + partitionedActors.Remove(a); } - public static readonly IEnumerable NoFrozenActors = new FrozenActor[0].AsEnumerable(); public IEnumerable FrozenActorsAt(Player viewer, int2 worldPx) { if (viewer == null) return NoFrozenActors; - - var i = (worldPx.X / info.BinSize).Clamp(0, cols - 1); - var j = (worldPx.Y / info.BinSize).Clamp(0, rows - 1); - return frozen[viewer][j * cols + i] - .Where(kv => kv.Key.IsValid && kv.Value.Contains(worldPx)) - .Select(kv => kv.Key); + return partitionedFrozenActors[viewer].At(worldPx).Where(frozenActorIsValid); } public IEnumerable FrozenActorsAt(Player viewer, MouseInput mi) @@ -129,11 +101,7 @@ namespace OpenRA.Traits public IEnumerable ActorsAt(int2 worldPx) { - var i = (worldPx.X / info.BinSize).Clamp(0, cols - 1); - var j = (worldPx.Y / info.BinSize).Clamp(0, rows - 1); - return actors[j * cols + i] - .Where(kv => kv.Key.IsInWorld && kv.Value.Contains(worldPx)) - .Select(kv => kv.Key); + return partitionedActors.At(worldPx).Where(actorIsInWorld); } public IEnumerable ActorsAt(MouseInput mi) @@ -141,51 +109,31 @@ namespace OpenRA.Traits return ActorsAt(worldRenderer.Viewport.ViewToWorldPx(mi.Location)); } + static Rectangle RectWithCorners(int2 a, int2 b) + { + return Rectangle.FromLTRB(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Max(a.X, b.X), Math.Max(a.Y, b.Y)); + } + public IEnumerable ActorsInBox(int2 a, int2 b) { - return ActorsInBox(Rectangle.FromLTRB(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Max(a.X, b.X), Math.Max(a.Y, b.Y))); + return ActorsInBox(RectWithCorners(a, b)); } public IEnumerable ActorsInBox(Rectangle r) { - var left = (r.Left / info.BinSize).Clamp(0, cols - 1); - var right = (r.Right / info.BinSize).Clamp(0, cols - 1); - var top = (r.Top / info.BinSize).Clamp(0, rows - 1); - var bottom = (r.Bottom / info.BinSize).Clamp(0, rows - 1); - - var actorsChecked = new HashSet(); - for (var j = top; j <= bottom; j++) - for (var i = left; i <= right; i++) - foreach (var kvp in actors[j * cols + i]) - { - var actor = kvp.Key; - if (actor.IsInWorld && kvp.Value.IntersectsWith(r) && actorsChecked.Add(actor)) - yield return actor; - } + return partitionedActors.InBox(r).Where(actorIsInWorld); } public IEnumerable FrozenActorsInBox(Player p, int2 a, int2 b) { - return FrozenActorsInBox(p, Rectangle.FromLTRB(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Max(a.X, b.X), Math.Max(a.Y, b.Y))); + return FrozenActorsInBox(p, RectWithCorners(a, b)); } public IEnumerable FrozenActorsInBox(Player p, Rectangle r) { - var left = (r.Left / info.BinSize).Clamp(0, cols - 1); - var right = (r.Right / info.BinSize).Clamp(0, cols - 1); - var top = (r.Top / info.BinSize).Clamp(0, rows - 1); - var bottom = (r.Bottom / info.BinSize).Clamp(0, rows - 1); - var bins = frozen[p]; - - var actorsChecked = new HashSet(); - for (var j = top; j <= bottom; j++) - for (var i = left; i <= right; i++) - foreach (var kvp in bins[j * cols + i]) - { - var actor = kvp.Key; - if (actor.IsValid && kvp.Value.IntersectsWith(r) && actorsChecked.Add(actor)) - yield return actor; - } + if (p == null) + return NoFrozenActors; + return partitionedFrozenActors[p].InBox(r).Where(frozenActorIsValid); } } }