diff --git a/OpenRA.Game/Actor.cs b/OpenRA.Game/Actor.cs index 15e439ffc6..0d97821f71 100644 --- a/OpenRA.Game/Actor.cs +++ b/OpenRA.Game/Actor.cs @@ -233,7 +233,7 @@ namespace OpenRA yield return r; } - public Rectangle MouseBounds(WorldRenderer wr) + public Polygon MouseBounds(WorldRenderer wr) { foreach (var mb in mouseBounds) { @@ -242,7 +242,7 @@ namespace OpenRA return bounds; } - return Rectangle.Empty; + return Polygon.Empty; } public void QueueActivity(bool queued, Activity nextActivity) diff --git a/OpenRA.Game/Exts.cs b/OpenRA.Game/Exts.cs index 555a3252fa..ebb67f281e 100644 --- a/OpenRA.Game/Exts.cs +++ b/OpenRA.Game/Exts.cs @@ -80,7 +80,7 @@ namespace OpenRA static int WindingDirectionTest(int2 v0, int2 v1, int2 p) { - return (v1.X - v0.X) * (p.Y - v0.Y) - (p.X - v0.X) * (v1.Y - v0.Y); + return Math.Sign((v1.X - v0.X) * (p.Y - v0.Y) - (p.X - v0.X) * (v1.Y - v0.Y)); } public static bool PolygonContains(this int2[] polygon, int2 p) @@ -101,6 +101,16 @@ namespace OpenRA return windingNumber != 0; } + public static bool LinesIntersect(int2 a, int2 b, int2 c, int2 d) + { + // If line segments AB and CD intersect: + // - the triangles ACD and BCD must have opposite sense (clockwise or anticlockwise) + // - the triangles CAB and DAB must have opposite sense + // Segments intersect if the orientation (clockwise or anticlockwise) of the two points in each line segment are opposite with respect to the other + // Assumes that lines are not colinear + return WindingDirectionTest(c, d, a) != WindingDirectionTest(c, d, b) && WindingDirectionTest(a, b, c) != WindingDirectionTest(a, b, d); + } + public static bool HasModifier(this Modifiers k, Modifiers mod) { // PERF: Enum.HasFlag is slower and requires allocations. diff --git a/OpenRA.Game/Graphics/WorldRenderer.cs b/OpenRA.Game/Graphics/WorldRenderer.cs index a6cd6cddd8..e7e4766251 100644 --- a/OpenRA.Game/Graphics/WorldRenderer.cs +++ b/OpenRA.Game/Graphics/WorldRenderer.cs @@ -277,11 +277,13 @@ namespace OpenRA.Graphics Game.Renderer.RgbaColorRenderer.DrawRect(tl, br, 1, Color.MediumSpringGreen); } - foreach (var r in World.ScreenMap.MouseBounds(World.RenderPlayer)) + foreach (var b in World.ScreenMap.MouseBounds(World.RenderPlayer)) { - var tl = Viewport.WorldToViewPx(new float2(r.Left, r.Top)); - var br = Viewport.WorldToViewPx(new float2(r.Right, r.Bottom)); - Game.Renderer.RgbaColorRenderer.DrawRect(tl, br, 1, Color.OrangeRed); + var points = b.Vertices + .Select(p => Viewport.WorldToViewPx(p).ToFloat2()) + .ToArray(); + + Game.Renderer.RgbaColorRenderer.DrawPolygon(points, 1, Color.OrangeRed); } } diff --git a/OpenRA.Game/Primitives/Polygon.cs b/OpenRA.Game/Primitives/Polygon.cs new file mode 100644 index 0000000000..693a998d91 --- /dev/null +++ b/OpenRA.Game/Primitives/Polygon.cs @@ -0,0 +1,114 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Linq; + +namespace OpenRA.Primitives +{ + public struct Polygon + { + public static readonly Polygon Empty = new Polygon(Rectangle.Empty); + + public readonly Rectangle BoundingRect; + public readonly int2[] Vertices; + bool isRectangle; + + public Polygon(Rectangle bounds) + { + BoundingRect = bounds; + Vertices = new[] { bounds.TopLeft, bounds.BottomLeft, bounds.BottomRight, bounds.TopRight }; + isRectangle = true; + } + + public Polygon(int2[] vertices) + { + if (vertices != null && vertices.Length > 0) + { + Vertices = vertices; + var left = int.MaxValue; + var right = int.MinValue; + var top = int.MaxValue; + var bottom = int.MinValue; + foreach (var p in vertices) + { + left = Math.Min(left, p.X); + right = Math.Max(right, p.X); + top = Math.Min(top, p.Y); + bottom = Math.Max(bottom, p.Y); + } + + BoundingRect = Rectangle.FromLTRB(left, top, right, bottom); + isRectangle = false; + } + else + { + isRectangle = true; + BoundingRect = Rectangle.Empty; + Vertices = Exts.MakeArray(4, _ => int2.Zero); + } + } + + public bool IsEmpty { get { return BoundingRect.IsEmpty; } } + public bool Contains(int2 xy) + { + return isRectangle ? BoundingRect.Contains(xy) : Vertices.PolygonContains(xy); + } + + public bool IntersectsWith(Rectangle rect) + { + var intersectsBoundingRect = BoundingRect.Left < rect.Right && BoundingRect.Right > rect.Left && BoundingRect.Top < rect.Bottom && BoundingRect.Bottom > rect.Top; + if (isRectangle) + return intersectsBoundingRect; + + // Easy case 1: Rect and bounding box don't intersect + if (!intersectsBoundingRect) + return false; + + // Easy case 2: Rect and bounding box intersect in a cross shape + if ((rect.Left <= BoundingRect.Left && rect.Right >= BoundingRect.Right) || (rect.Top <= BoundingRect.Top && rect.Bottom >= BoundingRect.Bottom)) + return true; + + // Easy case 3: Corner of rect is inside the polygon + if (Vertices.PolygonContains(rect.TopLeft) || Vertices.PolygonContains(rect.TopRight) || Vertices.PolygonContains(rect.BottomLeft) || Vertices.PolygonContains(rect.BottomRight)) + return true; + + // Easy case 4: Polygon vertex is inside rect + if (Vertices.Any(p => rect.Contains(p))) + return true; + + // Hard case: check intersection of every line segment pair + var rectVertices = new[] + { + rect.TopLeft, + rect.BottomLeft, + rect.BottomRight, + rect.TopRight + }; + + for (var i = 0; i < Vertices.Length; i++) + for (var j = 0; j < 4; j++) + if (Exts.LinesIntersect(Vertices[i], Vertices[(i + 1) % Vertices.Length], rectVertices[j], rectVertices[(j + 1) % 4])) + return true; + + return false; + } + + public override int GetHashCode() + { + var code = BoundingRect.GetHashCode(); + foreach (var v in Vertices) + code = ((code << 5) + code) ^ v.GetHashCode(); + + return code; + } + } +} diff --git a/OpenRA.Game/Primitives/SpatiallyPartitioned.cs b/OpenRA.Game/Primitives/SpatiallyPartitioned.cs index 2295bbf87d..af8d467066 100644 --- a/OpenRA.Game/Primitives/SpatiallyPartitioned.cs +++ b/OpenRA.Game/Primitives/SpatiallyPartitioned.cs @@ -139,5 +139,7 @@ namespace OpenRA.Primitives } public IEnumerable ItemBounds { get { return itemBounds.Values; } } + + public IEnumerable Items { get { return itemBounds.Keys; } } } } diff --git a/OpenRA.Game/SelectableExts.cs b/OpenRA.Game/SelectableExts.cs index d47321acb2..511920fbc0 100644 --- a/OpenRA.Game/SelectableExts.cs +++ b/OpenRA.Game/SelectableExts.cs @@ -73,14 +73,18 @@ namespace OpenRA.Traits return actors.MaxByOrDefault(a => CalculateActorSelectionPriority(a.Info, a.MouseBounds, selectionPixel, modifiers)); } - static long CalculateActorSelectionPriority(ActorInfo info, Rectangle bounds, int2 selectionPixel, Modifiers modifiers) + static long CalculateActorSelectionPriority(ActorInfo info, Polygon bounds, int2 selectionPixel, Modifiers modifiers) { if (bounds.IsEmpty) return info.SelectionPriority(modifiers); + // Assume that the center of the polygon is the same as the center of the bounding box + // This isn't necessarily true for arbitrary polygons, but is fine for the hexagonal and diamond + // shapes that are currently implemented + var br = bounds.BoundingRect; var centerPixel = new int2( - bounds.Left + bounds.Size.Width / 2, - bounds.Top + bounds.Size.Height / 2); + br.Left + br.Size.Width / 2, + br.Top + br.Size.Height / 2); var pixelDistance = (centerPixel - selectionPixel).Length; return info.SelectionPriority(modifiers) - (long)pixelDistance << 16; diff --git a/OpenRA.Game/Traits/Player/FrozenActorLayer.cs b/OpenRA.Game/Traits/Player/FrozenActorLayer.cs index 2e834ac401..86a42bf4af 100644 --- a/OpenRA.Game/Traits/Player/FrozenActorLayer.cs +++ b/OpenRA.Game/Traits/Player/FrozenActorLayer.cs @@ -68,8 +68,7 @@ namespace OpenRA.Traits public IRenderable[] Renderables = NoRenderables; public Rectangle[] ScreenBounds = NoBounds; - // TODO: Replace this with an int2[] polygon - public Rectangle MouseBounds = Rectangle.Empty; + public Polygon MouseBounds = Polygon.Empty; static readonly IRenderable[] NoRenderables = new IRenderable[0]; static readonly Rectangle[] NoBounds = new Rectangle[0]; diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 2b31676d2a..fa6792ff56 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -120,8 +120,7 @@ namespace OpenRA.Traits IEnumerable ScreenBounds(Actor self, WorldRenderer wr); } - // TODO: Replace Rectangle with an int2[] polygon - public interface IMouseBounds { Rectangle MouseoverBounds(Actor self, WorldRenderer wr); } + public interface IMouseBounds { Polygon MouseoverBounds(Actor self, WorldRenderer wr); } public interface IMouseBoundsInfo : ITraitInfoInterface { } public interface IAutoMouseBounds { Rectangle AutoMouseoverBounds(Actor self, WorldRenderer wr); } diff --git a/OpenRA.Game/Traits/World/ScreenMap.cs b/OpenRA.Game/Traits/World/ScreenMap.cs index 7f6465fffd..534518bd5d 100644 --- a/OpenRA.Game/Traits/World/ScreenMap.cs +++ b/OpenRA.Game/Traits/World/ScreenMap.cs @@ -21,11 +21,9 @@ namespace OpenRA.Traits public struct ActorBoundsPair { public readonly Actor Actor; + public readonly Polygon Bounds; - // TODO: Replace this with an int2[] polygon - public readonly Rectangle Bounds; - - public ActorBoundsPair(Actor actor, Rectangle bounds) { Actor = actor; Bounds = bounds; } + public ActorBoundsPair(Actor actor, Polygon bounds) { Actor = actor; Bounds = bounds; } public override int GetHashCode() { return Actor.GetHashCode() ^ Bounds.GetHashCode(); } @@ -192,7 +190,7 @@ namespace OpenRA.Traits return partitionedMouseActors.InBox(r) .Where(actorIsInWorld) .Select(selectActorAndBounds) - .Where(x => r.IntersectsWith(x.Bounds)); + .Where(x => x.Bounds.IntersectsWith(r)); } public IEnumerable RenderableActorsInBox(int2 a, int2 b) @@ -218,12 +216,12 @@ namespace OpenRA.Traits foreach (var a in addOrUpdateActors) { var mouseBounds = a.MouseBounds(worldRenderer); - if (!mouseBounds.Size.IsEmpty) + if (!mouseBounds.IsEmpty) { if (partitionedMouseActors.Contains(a)) - partitionedMouseActors.Update(a, mouseBounds); + partitionedMouseActors.Update(a, mouseBounds.BoundingRect); else - partitionedMouseActors.Add(a, mouseBounds); + partitionedMouseActors.Add(a, mouseBounds.BoundingRect); partitionedMouseActorBounds[a] = new ActorBoundsPair(a, mouseBounds); } @@ -257,12 +255,12 @@ namespace OpenRA.Traits foreach (var fa in kv.Value) { var mouseBounds = fa.MouseBounds; - if (!mouseBounds.Size.IsEmpty) + if (!mouseBounds.IsEmpty) { if (partitionedMouseFrozenActors[kv.Key].Contains(fa)) - partitionedMouseFrozenActors[kv.Key].Update(fa, mouseBounds); + partitionedMouseFrozenActors[kv.Key].Update(fa, mouseBounds.BoundingRect); else - partitionedMouseFrozenActors[kv.Key].Add(fa, mouseBounds); + partitionedMouseFrozenActors[kv.Key].Add(fa, mouseBounds.BoundingRect); } else partitionedMouseFrozenActors[kv.Key].Remove(fa); @@ -302,11 +300,10 @@ namespace OpenRA.Traits return viewer != null ? bounds.Concat(partitionedRenderableFrozenActors[viewer].ItemBounds) : bounds; } - public IEnumerable MouseBounds(Player viewer) + public IEnumerable MouseBounds(Player viewer) { - var bounds = partitionedMouseActors.ItemBounds; - - return viewer != null ? bounds.Concat(partitionedMouseFrozenActors[viewer].ItemBounds) : bounds; + var bounds = partitionedMouseActorBounds.Values.Select(a => a.Bounds); + return viewer != null ? bounds.Concat(partitionedMouseFrozenActors[viewer].Items.Select(fa => fa.MouseBounds)) : bounds; } } } diff --git a/OpenRA.Mods.Common/Traits/Interactable.cs b/OpenRA.Mods.Common/Traits/Interactable.cs index a22674d18b..a8f41026d2 100644 --- a/OpenRA.Mods.Common/Traits/Interactable.cs +++ b/OpenRA.Mods.Common/Traits/Interactable.cs @@ -52,10 +52,10 @@ namespace OpenRA.Mods.Common.Traits return autoBounds.Select(s => s.AutoMouseoverBounds(self, wr)).FirstOrDefault(r => !r.IsEmpty); } - Rectangle Bounds(Actor self, WorldRenderer wr, int[] bounds) + Polygon Bounds(Actor self, WorldRenderer wr, int[] bounds) { if (bounds == null) - return AutoBounds(self, wr); + return new Polygon(AutoBounds(self, wr)); var size = new int2(bounds[0], bounds[1]); @@ -64,17 +64,17 @@ namespace OpenRA.Mods.Common.Traits offset += new int2(bounds[2], bounds[3]); var xy = wr.ScreenPxPosition(self.CenterPosition) + offset; - return new Rectangle(xy.X, xy.Y, size.X, size.Y); + return new Polygon(new Rectangle(xy.X, xy.Y, size.X, size.Y)); } - Rectangle IMouseBounds.MouseoverBounds(Actor self, WorldRenderer wr) + Polygon IMouseBounds.MouseoverBounds(Actor self, WorldRenderer wr) { return Bounds(self, wr, info.Bounds); } public Rectangle DecorationBounds(Actor self, WorldRenderer wr) { - return Bounds(self, wr, info.DecorationBounds ?? info.Bounds); + return Bounds(self, wr, info.DecorationBounds ?? info.Bounds).BoundingRect; } } } diff --git a/OpenRA.Mods.Common/Traits/Modifiers/FrozenUnderFog.cs b/OpenRA.Mods.Common/Traits/Modifiers/FrozenUnderFog.cs index 24ca3a9c80..c953845a07 100644 --- a/OpenRA.Mods.Common/Traits/Modifiers/FrozenUnderFog.cs +++ b/OpenRA.Mods.Common/Traits/Modifiers/FrozenUnderFog.cs @@ -145,7 +145,7 @@ namespace OpenRA.Mods.Common.Traits { IRenderable[] renderables = null; Rectangle[] bounds = null; - Rectangle mouseBounds = Rectangle.Empty; + var mouseBounds = Polygon.Empty; for (var playerIndex = 0; playerIndex < frozenStates.Count; playerIndex++) { var frozen = frozenStates[playerIndex].FrozenActor;