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