diff --git a/OpenRA.Game/Graphics/Minimap.cs b/OpenRA.Game/Graphics/Minimap.cs index c7aba5fa3b..3db0bd3117 100644 --- a/OpenRA.Game/Graphics/Minimap.cs +++ b/OpenRA.Game/Graphics/Minimap.cs @@ -180,12 +180,14 @@ namespace OpenRA.Graphics { var colors = (int*)bitmapData.Scan0; var stride = bitmapData.Stride / 4; + var shroudObscured = world.ShroudObscuresTest(map.Cells); + var fogObscured = world.FogObscuresTest(map.Cells); foreach (var cell in map.Cells) { var uv = Map.CellToMap(map.TileShape, cell) - offset; - if (world.ShroudObscures(cell)) + if (shroudObscured(cell)) colors[uv.Y * stride + uv.X] = shroud; - else if (world.FogObscures(cell)) + else if (fogObscured(cell)) colors[uv.Y * stride + uv.X] = fog; } } diff --git a/OpenRA.Game/Map/CellRegion.cs b/OpenRA.Game/Map/CellRegion.cs index 66501c4fc1..ef7fc6debd 100644 --- a/OpenRA.Game/Map/CellRegion.cs +++ b/OpenRA.Game/Map/CellRegion.cs @@ -8,8 +8,10 @@ */ #endregion +using System; using System.Collections; using System.Collections.Generic; +using System.Linq; namespace OpenRA { @@ -47,29 +49,69 @@ namespace OpenRA return new CellRegion(region.shape, tl, br); } + /// Returns the minimal region that covers at least the specified cells. + public static CellRegion BoundingRegion(TileShape shape, IEnumerable cells) + { + if (cells == null || !cells.Any()) + throw new ArgumentException("cells must not be null or empty.", "cells"); + + var minX = int.MaxValue; + var minY = int.MaxValue; + var maxX = int.MinValue; + var maxY = int.MinValue; + foreach (var cell in cells) + { + if (minX > cell.X) + minX = cell.X; + if (maxX < cell.X) + maxX = cell.X; + if (minY > cell.Y) + minY = cell.Y; + if (maxY < cell.Y) + maxY = cell.Y; + } + + return new CellRegion(shape, new CPos(minX, minY), new CPos(maxX, maxY)); + } + + public bool Contains(CellRegion region) + { + return + TopLeft.X <= region.TopLeft.X && TopLeft.Y <= region.TopLeft.Y && + BottomRight.X >= region.BottomRight.X && BottomRight.Y >= region.BottomRight.Y; + } + public bool Contains(CPos cell) { var uv = Map.CellToMap(shape, cell); return uv.X >= mapTopLeft.X && uv.X <= mapBottomRight.X && uv.Y >= mapTopLeft.Y && uv.Y <= mapBottomRight.Y; } - public IEnumerator GetEnumerator() + public CellRegionEnumerator GetEnumerator() { return new CellRegionEnumerator(this); } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } - class CellRegionEnumerator : IEnumerator + public class CellRegionEnumerator : IEnumerator { readonly CellRegion r; // Current position, in map coordinates int u, v; + // Current position, in cell coordinates + CPos current; + public CellRegionEnumerator(CellRegion region) { r = region; @@ -91,6 +133,7 @@ namespace OpenRA return false; } + current = Map.MapToCell(r.shape, new CPos(u, v)); return true; } @@ -101,7 +144,7 @@ namespace OpenRA v = r.mapTopLeft.Y; } - public CPos Current { get { return Map.MapToCell(r.shape, new CPos(u, v)); } } + public CPos Current { get { return current; } } object IEnumerator.Current { get { return Current; } } public void Dispose() { } } diff --git a/OpenRA.Game/Traits/Player/FrozenActorLayer.cs b/OpenRA.Game/Traits/Player/FrozenActorLayer.cs index d59558eaf5..05d299e0f7 100755 --- a/OpenRA.Game/Traits/Player/FrozenActorLayer.cs +++ b/OpenRA.Game/Traits/Player/FrozenActorLayer.cs @@ -23,6 +23,7 @@ namespace OpenRA.Traits public class FrozenActor { public readonly CPos[] Footprint; + public readonly CellRegion FootprintRegion; public readonly WPos CenterPosition; public readonly Rectangle Bounds; readonly Actor actor; @@ -38,10 +39,12 @@ namespace OpenRA.Traits public bool Visible; - public FrozenActor(Actor self, IEnumerable footprint) + public FrozenActor(Actor self, CPos[] footprint, CellRegion footprintRegion) { actor = self; - Footprint = footprint.ToArray(); + Footprint = footprint; + FootprintRegion = footprintRegion; + CenterPosition = self.CenterPosition; Bounds = self.Bounds.Value; } @@ -54,16 +57,7 @@ namespace OpenRA.Traits int flashTicks; public void Tick(World world, Shroud shroud) { - Visible = true; - foreach (var pos in Footprint) - { - if (shroud.IsVisible(pos)) - { - Visible = false; - break; - } - } - + Visible = !Footprint.Any(shroud.IsVisibleTest(FootprintRegion)); if (flashTicks > 0) flashTicks--; } diff --git a/OpenRA.Game/Traits/World/Shroud.cs b/OpenRA.Game/Traits/World/Shroud.cs index c273db386b..ccff0506c8 100644 --- a/OpenRA.Game/Traits/World/Shroud.cs +++ b/OpenRA.Game/Traits/World/Shroud.cs @@ -26,8 +26,8 @@ namespace OpenRA.Traits readonly Actor self; readonly Map map; - readonly CellLayer visibleCount; - readonly CellLayer generatedShroudCount; + readonly CellLayer visibleCount; + readonly CellLayer generatedShroudCount; readonly CellLayer explored; readonly Lazy fogVisibilities; @@ -39,13 +39,19 @@ namespace OpenRA.Traits public int Hash { get; private set; } + static readonly Func TruthPredicate = cell => true; + readonly Func fastExploredTest; + readonly Func slowExploredTest; + readonly Func fastVisibleTest; + readonly Func slowVisibleTest; + public Shroud(Actor self) { this.self = self; map = self.World.Map; - visibleCount = new CellLayer(map); - generatedShroudCount = new CellLayer(map); + visibleCount = new CellLayer(map); + generatedShroudCount = new CellLayer(map); explored = new CellLayer(map); self.World.ActorAdded += AddVisibility; @@ -55,6 +61,11 @@ namespace OpenRA.Traits self.World.ActorRemoved += RemoveShroudGeneration; fogVisibilities = Exts.Lazy(() => self.TraitsImplementing().ToArray()); + + fastExploredTest = IsExploredCore; + slowExploredTest = IsExplored; + fastVisibleTest = IsVisibleCore; + slowVisibleTest = IsVisible; } void Invalidate() @@ -223,10 +234,32 @@ namespace OpenRA.Traits if (!map.Contains(cell)) return false; - if (Disabled || !self.World.LobbyInfo.GlobalSettings.Shroud) + if (!ShroudEnabled) return true; - return explored[cell] && (generatedShroudCount[cell] == 0 || visibleCount[cell] > 0); + return IsExploredCore(cell); + } + + bool ShroudEnabled { get { return !Disabled && self.World.LobbyInfo.GlobalSettings.Shroud; } } + + bool IsExploredCore(CPos cell) + { + var uv = Map.CellToMap(map.TileShape, cell); + return explored[uv.X, uv.Y] && (generatedShroudCount[uv.X, uv.Y] == 0 || visibleCount[uv.X, uv.Y] > 0); + } + + public Func IsExploredTest(CellRegion region) + { + // If the region to test extends outside the map we must use the slow test that checks the map boundary every time. + if (!map.Cells.Contains(region)) + return slowExploredTest; + + // If shroud isn't enabled, then we can see everything. + if (!ShroudEnabled) + return TruthPredicate; + + // If shroud is enabled, we can use the fast test that just does the core check. + return fastExploredTest; } public bool IsExplored(Actor a) @@ -239,12 +272,33 @@ namespace OpenRA.Traits if (!map.Contains(cell)) return false; - if (Disabled || !self.World.LobbyInfo.GlobalSettings.Fog) + if (!FogEnabled) return true; + return IsVisibleCore(cell); + } + + bool FogEnabled { get { return !Disabled && self.World.LobbyInfo.GlobalSettings.Fog; } } + + bool IsVisibleCore(CPos cell) + { return visibleCount[cell] > 0; } + public Func IsVisibleTest(CellRegion region) + { + // If the region to test extends outside the map we must use the slow test that checks the map boundary every time. + if (!map.Cells.Contains(region)) + return slowVisibleTest; + + // If fog isn't enabled, then we can see everything. + if (!FogEnabled) + return TruthPredicate; + + // If fog is enabled, we can use the fast test that just does the core check. + return fastVisibleTest; + } + // Actors are hidden under shroud, but not under fog by default public bool IsVisible(Actor a) { diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index 3661df521f..1bf06a9fd4 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -24,6 +24,7 @@ namespace OpenRA { public class World { + static readonly Func FalsePredicate = cell => false; internal readonly TraitDictionary traitDict = new TraitDictionary(); readonly HashSet actors = new HashSet(); readonly List effects = new List(); @@ -53,6 +54,24 @@ namespace OpenRA public bool FogObscures(CPos p) { return RenderPlayer != null && !RenderPlayer.Shroud.IsVisible(p); } public bool ShroudObscures(Actor a) { return RenderPlayer != null && !RenderPlayer.Shroud.IsExplored(a); } public bool ShroudObscures(CPos p) { return RenderPlayer != null && !RenderPlayer.Shroud.IsExplored(p); } + + public Func FogObscuresTest(CellRegion region) + { + var rp = RenderPlayer; + if (rp == null) + return FalsePredicate; + var predicate = rp.Shroud.IsVisibleTest(region); + return cell => !predicate(cell); + } + + public Func ShroudObscuresTest(CellRegion region) + { + var rp = RenderPlayer; + if (rp == null) + return FalsePredicate; + var predicate = rp.Shroud.IsExploredTest(region); + return cell => !predicate(cell); + } public bool IsReplay { diff --git a/OpenRA.Mods.RA/Modifiers/FrozenUnderFog.cs b/OpenRA.Mods.RA/Modifiers/FrozenUnderFog.cs index dfb85070e3..3687ae6976 100644 --- a/OpenRA.Mods.RA/Modifiers/FrozenUnderFog.cs +++ b/OpenRA.Mods.RA/Modifiers/FrozenUnderFog.cs @@ -30,6 +30,7 @@ namespace OpenRA.Mods.RA readonly bool startsRevealed; readonly CPos[] footprint; + readonly CellRegion footprintRegion; readonly Lazy tooltip; readonly Lazy health; @@ -44,6 +45,7 @@ namespace OpenRA.Mods.RA // Spawned actors (e.g. building husks) shouldn't be revealed startsRevealed = info.StartsRevealed && !init.Contains(); footprint = FootprintUtils.Tiles(init.self).ToArray(); + footprintRegion = CellRegion.BoundingRegion(init.world.Map.TileShape, footprint); tooltip = Exts.Lazy(() => init.self.TraitsImplementing().FirstOrDefault()); tooltip = Exts.Lazy(() => init.self.TraitsImplementing().FirstOrDefault()); health = Exts.Lazy(() => init.self.TraitOrDefault()); @@ -65,15 +67,7 @@ namespace OpenRA.Mods.RA VisibilityHash = 0; foreach (var p in self.World.Players) { - var isVisible = false; - foreach (var pos in footprint) - { - if (p.Shroud.IsVisible(pos)) - { - isVisible = true; - break; - } - } + var isVisible = footprint.Any(p.Shroud.IsVisibleTest(footprintRegion)); visible[p] = isVisible; if (isVisible) VisibilityHash += p.ClientIndex; @@ -84,7 +78,7 @@ namespace OpenRA.Mods.RA foreach (var p in self.World.Players) { visible[p] |= startsRevealed; - p.PlayerActor.Trait().Add(frozen[p] = new FrozenActor(self, footprint)); + p.PlayerActor.Trait().Add(frozen[p] = new FrozenActor(self, footprint, footprintRegion)); } initialized = true; diff --git a/OpenRA.Mods.RA/ShroudRenderer.cs b/OpenRA.Mods.RA/ShroudRenderer.cs index 51ac394387..3a726b1c1b 100644 --- a/OpenRA.Mods.RA/ShroudRenderer.cs +++ b/OpenRA.Mods.RA/ShroudRenderer.cs @@ -65,33 +65,37 @@ namespace OpenRA.Mods.RA All = Top | Right | Bottom | Left } - class ShroudTile + struct ShroudTile { - public readonly CPos Position; public readonly float2 ScreenPosition; - public readonly int Variant; + public readonly byte Variant; public Sprite Fog; public Sprite Shroud; - public ShroudTile(CPos position, float2 screenPosition, int variant) + public ShroudTile(float2 screenPosition, byte variant) { - Position = position; ScreenPosition = screenPosition; Variant = variant; + + Fog = null; + Shroud = null; } } readonly ShroudRendererInfo info; readonly Sprite[] shroudSprites, fogSprites; - readonly int[] spriteMap; + readonly byte[] spriteMap; readonly CellLayer tiles; - readonly int variantStride; + readonly byte variantStride; readonly Map map; readonly Edges notVisibleEdges; + bool clearedForNullShroud; + int lastShroudHash; + CellRegion updatedRegion; + PaletteReference fogPalette, shroudPalette; - int shroudHash; public ShroudRenderer(World world, ShroudRendererInfo info) { @@ -101,17 +105,20 @@ namespace OpenRA.Mods.RA if ((info.OverrideFullFog == null) ^ (info.OverrideFullShroud == null)) throw new ArgumentException("ShroudRenderer cannot define overrides for only one of shroud or fog!", "info"); + if (info.ShroudVariants.Length > byte.MaxValue) + throw new ArgumentException("ShroudRenderer cannot define this many shroud and fog variants.", "info"); + + if (info.Index.Length >= byte.MaxValue) + throw new ArgumentException("ShroudRenderer cannot define this many indexes for shroud directions.", "info"); + this.info = info; map = world.Map; tiles = new CellLayer(map); - // Force update on first render - shroudHash = -1; - // Load sprite variants var variantCount = info.ShroudVariants.Length; - variantStride = info.Index.Length + (info.OverrideFullShroud != null ? 1 : 0); + variantStride = (byte)(info.Index.Length + (info.OverrideFullShroud != null ? 1 : 0)); shroudSprites = new Sprite[variantCount * variantStride]; fogSprites = new Sprite[variantCount * variantStride]; @@ -134,33 +141,33 @@ namespace OpenRA.Mods.RA } // Mapping of shrouded directions -> sprite index - spriteMap = new int[info.UseExtendedIndex ? 256 : 16]; + spriteMap = new byte[(byte)(info.UseExtendedIndex ? Edges.All : Edges.AllCorners) + 1]; for (var i = 0; i < info.Index.Length; i++) - spriteMap[info.Index[i]] = i; + spriteMap[info.Index[i]] = (byte)i; if (info.OverrideFullShroud != null) - spriteMap[info.OverrideShroudIndex] = variantStride - 1; + spriteMap[info.OverrideShroudIndex] = (byte)(variantStride - 1); notVisibleEdges = info.UseExtendedIndex ? Edges.AllSides : Edges.AllCorners; } - Edges GetEdges(int x, int y, Func isVisible) + Edges GetEdges(CPos p, Func isVisible) { - if (!isVisible(x, y)) + if (!isVisible(p)) return notVisibleEdges; // If a side is shrouded then we also count the corners var u = Edges.None; - if (!isVisible(x, y - 1)) u |= Edges.Top; - if (!isVisible(x + 1, y)) u |= Edges.Right; - if (!isVisible(x, y + 1)) u |= Edges.Bottom; - if (!isVisible(x - 1, y)) u |= Edges.Left; + if (!isVisible(p + new CVec(0, -1))) u |= Edges.Top; + if (!isVisible(p + new CVec(1, 0))) u |= Edges.Right; + if (!isVisible(p + new CVec(0, 1))) u |= Edges.Bottom; + if (!isVisible(p + new CVec(-1, 0))) u |= Edges.Left; var ucorner = u & Edges.AllCorners; - if (!isVisible(x - 1, y - 1)) u |= Edges.TopLeft; - if (!isVisible(x + 1, y - 1)) u |= Edges.TopRight; - if (!isVisible(x + 1, y + 1)) u |= Edges.BottomRight; - if (!isVisible(x - 1, y + 1)) u |= Edges.BottomLeft; + if (!isVisible(p + new CVec(-1, -1))) u |= Edges.TopLeft; + if (!isVisible(p + new CVec(1, -1))) u |= Edges.TopRight; + if (!isVisible(p + new CVec(1, 1))) u |= Edges.BottomRight; + if (!isVisible(p + new CVec(-1, 1))) u |= Edges.BottomLeft; // RA provides a set of frames for tiles with shrouded // corners but unshrouded edges. We want to detect this @@ -171,19 +178,19 @@ namespace OpenRA.Mods.RA return info.UseExtendedIndex ? u ^ ucorner : u & Edges.AllCorners; } - Edges GetObserverEdges(int x, int y) + Edges GetObserverEdges(CPos p) { var u = Edges.None; - if (!map.Contains(new CPos(x, y - 1))) u |= Edges.Top; - if (!map.Contains(new CPos(x + 1, y))) u |= Edges.Right; - if (!map.Contains(new CPos(x, y + 1))) u |= Edges.Bottom; - if (!map.Contains(new CPos(x - 1, y))) u |= Edges.Left; + if (!map.Contains(p + new CVec(0, -1))) u |= Edges.Top; + if (!map.Contains(p + new CVec(1, 0))) u |= Edges.Right; + if (!map.Contains(p + new CVec(0, 1))) u |= Edges.Bottom; + if (!map.Contains(p + new CVec(-1, 0))) u |= Edges.Left; var ucorner = u & Edges.AllCorners; - if (!map.Contains(new CPos(x - 1, y - 1))) u |= Edges.TopLeft; - if (!map.Contains(new CPos(x + 1, y - 1))) u |= Edges.TopRight; - if (!map.Contains(new CPos(x + 1, y + 1))) u |= Edges.BottomRight; - if (!map.Contains(new CPos(x - 1, y + 1))) u |= Edges.BottomLeft; + if (!map.Contains(p + new CVec(-1, -1))) u |= Edges.TopLeft; + if (!map.Contains(p + new CVec(1, -1))) u |= Edges.TopRight; + if (!map.Contains(p + new CVec(1, 1))) u |= Edges.BottomRight; + if (!map.Contains(p + new CVec(-1, 1))) u |= Edges.BottomLeft; return info.UseExtendedIndex ? u ^ ucorner : u & Edges.AllCorners; } @@ -195,64 +202,84 @@ namespace OpenRA.Mods.RA foreach (var cell in CellRegion.Expand(w.Map.Cells, 1)) { var screen = wr.ScreenPosition(w.Map.CenterOfCell(cell)); - var variant = Game.CosmeticRandom.Next(info.ShroudVariants.Length); - tiles[cell] = new ShroudTile(cell, screen, variant); + var variant = (byte)Game.CosmeticRandom.Next(info.ShroudVariants.Length); + tiles[cell] = new ShroudTile(screen, variant); // Set the cells outside the border so they don't need to be touched again if (!map.Contains(cell)) - tiles[cell].Shroud = GetTile(notVisibleEdges, variant); + { + var shroudTile = tiles[cell]; + shroudTile.Shroud = GetTile(shroudSprites, notVisibleEdges, variant); + tiles[cell] = shroudTile; + } } fogPalette = wr.Palette(info.FogPalette); shroudPalette = wr.Palette(info.ShroudPalette); } - Sprite GetTile(Edges edges, int variant) + Sprite GetTile(Sprite[] sprites, Edges edges, int variant) { if (edges == Edges.None) return null; - return shroudSprites[variant * variantStride + spriteMap[(byte)edges]]; + return sprites[variant * variantStride + spriteMap[(byte)edges]]; } - void Update(Shroud shroud) + void Update(Shroud shroud, CellRegion region) { - var hash = shroud != null ? shroud.Hash : 0; - if (shroudHash == hash) - return; - - shroudHash = hash; - if (shroud == null) + if (shroud != null) { - // Players with no shroud see the whole map so we only need to set the edges - foreach (var cell in map.Cells) - { - var t = tiles[cell]; - var shrouded = GetObserverEdges(cell.X, cell.Y); + // If the current shroud hasn't changed and we have already updated the specified area, we don't need to do anything. + if (lastShroudHash == shroud.Hash && !clearedForNullShroud && updatedRegion.Contains(region)) + return; - t.Shroud = shrouded != 0 ? shroudSprites[t.Variant * variantStride + spriteMap[(byte)shrouded]] : null; - t.Fog = shrouded != 0 ? fogSprites[t.Variant * variantStride + spriteMap[(byte)shrouded]] : null; - } + lastShroudHash = shroud.Hash; + clearedForNullShroud = false; + updatedRegion = region; + UpdateShroud(shroud); } - else + else if (!clearedForNullShroud) { - foreach (var cell in map.Cells) - { - Func visibleUnderShroud = (x, y) => shroud.IsExplored(new CPos(x, y)); - Func visibleUnderFog = (x, y) => shroud.IsVisible(new CPos(x, y)); - var t = tiles[cell]; - var shrouded = GetEdges(cell.X, cell.Y, visibleUnderShroud); - var fogged = GetEdges(cell.X, cell.Y, visibleUnderFog); + // We need to clear any applied shroud. + clearedForNullShroud = true; + updatedRegion = new CellRegion(map.TileShape, new CPos(0, 0), new CPos(-1, -1)); + UpdateNullShroud(); + } + } - t.Shroud = shrouded != 0 ? shroudSprites[t.Variant * variantStride + spriteMap[(byte)shrouded]] : null; - t.Fog = fogged != 0 ? fogSprites[t.Variant * variantStride + spriteMap[(byte)fogged]] : null; - } + void UpdateShroud(Shroud shroud) + { + var visibleUnderShroud = shroud.IsExploredTest(updatedRegion); + var visibleUnderFog = shroud.IsVisibleTest(updatedRegion); + foreach (var cell in updatedRegion) + { + var shrouded = GetEdges(cell, visibleUnderShroud); + var fogged = GetEdges(cell, visibleUnderFog); + var shroudTile = tiles[cell]; + var variant = shroudTile.Variant; + shroudTile.Shroud = GetTile(shroudSprites, shrouded, variant); + shroudTile.Fog = GetTile(fogSprites, fogged, variant); + tiles[cell] = shroudTile; + } + } + + void UpdateNullShroud() + { + foreach (var cell in map.Cells) + { + var edges = GetObserverEdges(cell); + var shroudTile = tiles[cell]; + var variant = shroudTile.Variant; + shroudTile.Shroud = GetTile(shroudSprites, edges, variant); + shroudTile.Fog = GetTile(fogSprites, edges, variant); + tiles[cell] = shroudTile; } } public void RenderShroud(WorldRenderer wr, Shroud shroud) { - Update(shroud); + Update(shroud, wr.Viewport.VisibleCells); foreach (var cell in CellRegion.Expand(wr.Viewport.VisibleCells, 1)) { diff --git a/OpenRA.Mods.RA/World/ResourceLayer.cs b/OpenRA.Mods.RA/World/ResourceLayer.cs index 02eac34b10..b7bf86fbe5 100644 --- a/OpenRA.Mods.RA/World/ResourceLayer.cs +++ b/OpenRA.Mods.RA/World/ResourceLayer.cs @@ -34,9 +34,10 @@ namespace OpenRA.Mods.RA public void Render(WorldRenderer wr) { + var shroudObscured = world.ShroudObscuresTest(wr.Viewport.VisibleCells); foreach (var cell in wr.Viewport.VisibleCells) { - if (world.ShroudObscures(cell)) + if (shroudObscured(cell)) continue; var c = render[cell];