From 0e4c7e67ab37bcecfa6feceaa716526f11b411bf Mon Sep 17 00:00:00 2001
From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Date: Wed, 9 Feb 2022 15:11:06 +1300
Subject: [PATCH] Redo ClickableComponent (#6221)
---
.../Clickable/ClickableComponent.cs | 218 ++++++++++--------
.../Tests/ClickableTest.cs | 5 +
2 files changed, 126 insertions(+), 97 deletions(-)
diff --git a/Content.Client/Clickable/ClickableComponent.cs b/Content.Client/Clickable/ClickableComponent.cs
index 564df8a706..5cb03a5d60 100644
--- a/Content.Client/Clickable/ClickableComponent.cs
+++ b/Content.Client/Clickable/ClickableComponent.cs
@@ -7,7 +7,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
-using TerraFX.Interop.Windows;
+using static Robust.Client.GameObjects.SpriteComponent;
namespace Content.Client.Clickable
{
@@ -15,11 +15,14 @@ namespace Content.Client.Clickable
public sealed class ClickableComponent : Component
{
[Dependency] private readonly IClickMapManager _clickMapManager = default!;
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
- [ViewVariables] [DataField("bounds")] private DirBoundData? _data;
+ [ViewVariables] [DataField("bounds")] private DirBoundData? Bounds;
///
- /// Used to check whether a click worked.
+ /// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding
+ /// boxes (see ). If that fails, attempts to use automatically generated click maps.
///
/// The world position that was clicked.
///
@@ -28,106 +31,129 @@ namespace Content.Client.Clickable
/// True if the click worked, false otherwise.
public bool CheckClick(Vector2 worldPos, out int drawDepth, out uint renderOrder)
{
- var entMan = IoCManager.Resolve();
- if (!entMan.TryGetComponent(Owner, out ISpriteComponent? sprite) || !sprite.Visible)
+ if (!_entMan.TryGetComponent(Owner, out ISpriteComponent? sprite) || !sprite.Visible)
{
drawDepth = default;
renderOrder = default;
return false;
}
- var transform = entMan.GetComponent(Owner);
- var localPos = transform.InvWorldMatrix.Transform(worldPos);
- var spriteMatrix = Matrix3.Invert(sprite.GetLocalMatrix());
-
- localPos = spriteMatrix.Transform(localPos);
-
- var found = false;
- var worldRotation = transform.WorldRotation;
-
- if (_data != null)
- {
- if (_data.All.Contains(localPos))
- {
- found = true;
- }
- else
- {
- // TODO: diagonal support?
-
- var modAngle = sprite.NoRotation
- ? SpriteComponent.CalcRectWorldAngle(worldRotation, 4)
- : Angle.Zero;
- var dir = sprite.EnableDirectionOverride
- ? sprite.DirectionOverride
- : worldRotation.GetCardinalDir();
-
- modAngle += dir.ToAngle();
-
- var layerPos = modAngle.RotateVec(localPos);
-
- var boundsForDir = dir switch
- {
- Direction.East => _data.East,
- Direction.North => _data.North,
- Direction.South => _data.South,
- Direction.West => _data.West,
- _ => throw new InvalidOperationException()
- };
-
- if (boundsForDir.Contains(layerPos))
- {
- found = true;
- }
- }
- }
-
- if (!found)
- {
- foreach (var layer in sprite.AllLayers)
- {
- if (!layer.Visible) continue;
-
- var dirCount = sprite.GetLayerDirectionCount(layer);
- var dir = layer.EffectiveDirection(worldRotation);
- var modAngle = sprite.NoRotation ? SpriteComponent.CalcRectWorldAngle(worldRotation, dirCount) : Angle.Zero;
- modAngle += dir.Convert().ToAngle();
-
- var layerPos = modAngle.RotateVec(localPos);
-
- var localOffset = layerPos * EyeManager.PixelsPerMeter * (1, -1);
- if (layer.Texture != null)
- {
- if (_clickMapManager.IsOccluding(layer.Texture,
- (Vector2i) (localOffset + layer.Texture.Size / 2f)))
- {
- found = true;
- break;
- }
- }
- else if (layer.RsiState != default)
- {
- var rsi = layer.ActualRsi;
- if (rsi == null)
- {
- continue;
- }
-
- var (mX, mY) = localOffset + rsi.Size / 2;
-
- if (_clickMapManager.IsOccluding(rsi, layer.RsiState, dir,
- layer.AnimationFrame, ((int) mX, (int) mY)))
- {
- found = true;
- break;
- }
- }
- }
- }
-
drawDepth = sprite.DrawDepth;
renderOrder = sprite.RenderOrder;
- return found;
+
+ var transform = _entMan.GetComponent(Owner);
+ var worldRot = transform.WorldRotation;
+ var invSpriteMatrix = Matrix3.CreateTransform(Vector2.Zero, -sprite.Rotation, (1,1)/sprite.Scale);
+ var relativeRotation = worldRot + _eyeManager.CurrentEye.Rotation;
+
+ // localPos is the clicked location in the entity's coordinate frame, but with the sprite offset removed.
+ var localPos = transform.InvWorldMatrix.Transform(worldPos) - sprite.Offset;
+
+ // Check explicitly defined click-able bounds
+ if (CheckDirBound(sprite, relativeRotation, localPos, invSpriteMatrix))
+ return true;
+
+ // Next check each individual sprite layer using automatically computed click maps.
+ foreach (var spriteLayer in sprite.AllLayers)
+ {
+ if (!spriteLayer.Visible || spriteLayer is not Layer layer)
+ continue;
+
+ // How many orientations does this rsi have?
+ var dirCount = sprite.GetLayerDirectionCount(layer);
+
+ // If the sprite does not actually rotate we need to fix the rotation that was added to localPos via invSpriteMatrix
+ var modAngle = Angle.Zero;
+ if (sprite.NoRotation)
+ modAngle += CalcRectWorldAngle(relativeRotation, dirCount);
+
+ // Check the layer's texture, if it has one
+ if (layer.Texture != null)
+ {
+ // Convert to sprite-coordinates. This includes sprite matrix (scale, rotation), and noRot corrections (modAngle)
+ // Recall that the sprite offset was already removed
+ var spritePos = invSpriteMatrix.Transform(modAngle.RotateVec(localPos));
+
+ // Convert to image coordinates
+ var imagePos = (Vector2i) (spritePos * EyeManager.PixelsPerMeter * (1, -1) + layer.Texture.Size / 2f);
+
+ if (_clickMapManager.IsOccluding(layer.Texture, imagePos))
+ return true;
+ }
+
+ // As the texture failed, we check the RSI next
+ if (layer.State == null || layer.ActualRsi is not RSI rsi || !rsi.TryGetState(layer.State, out var state))
+ continue;
+
+ // Get the sprite direction, in the absence of any direction offset or override. this is just for
+ // determining the angle-snapping correction for directional sprites.
+ var dir = relativeRotation.ToRsiDirection(state.Directions);
+
+ // Add the correction to the sprite angle due to the fact that it is a directional sprite. This is some
+ // multiple of 90 or 45 degrees for 4/8 directional sprites, or 0 for one-directional sprites.
+ modAngle += dir.Convert().ToAngle();
+
+ // TODO SPRITE LAYER ROTATION
+ // Currently this doesn't support layers with scale & rotation. Whenever/if-ever that should be added,
+ // this needs fixing. See also Layer.CalculateBoundingBox and other engine code with similar warnings.
+
+ // Convert to sprite-coordinates. This is the same as for the texture check, but now also includes
+ // directional angle-snapping corrections in modAngle (+ the previous NoRot stuff)
+ var layerPos = invSpriteMatrix.Transform(modAngle.RotateVec(localPos));
+
+ // Convert to image coordinates
+ var layerImagePos = (Vector2i) (layerPos * EyeManager.PixelsPerMeter * (1, -1) + layer.ActualRsi.Size / 2f);
+
+ // Next, to get the right click map we need the "direction" of this layer that is actually being used to draw the sprite on the screen.
+ // This **can** differ from the dir defined before, but can also just be the same.
+ if (sprite.EnableDirectionOverride)
+ dir = sprite.DirectionOverride.Convert(state.Directions);;
+ dir = dir.OffsetRsiDir(layer.DirOffset);
+
+ if (_clickMapManager.IsOccluding(layer.ActualRsi, layer.State, dir, layer.AnimationFrame, layerImagePos))
+ return true;
+ }
+
+ drawDepth = default;
+ renderOrder = default;
+ return false;
+ }
+
+ public bool CheckDirBound(ISpriteComponent sprite, Angle relativeRotation, Vector2 localPos, Matrix3 spriteMatrix)
+ {
+ if (Bounds == null)
+ return false;
+
+ // Here we get sprite orientation, either from an explicit override or just from the relative rotation
+ var direction = relativeRotation.GetCardinalDir();
+
+ // Assuming the sprite snaps to 4 orientations we need to adjust our localPos relative to the entity by 90
+ // degree steps. Effectively, this accounts for the fact that the entity's world rotation + sprite rotation
+ // does not match the **actual** drawn rotation.
+ var modAngle = direction.ToAngle();
+
+ // If our sprite does not rotate at all, we shouldn't have been bothering with all that rotation logic,
+ // but it sorta came for free with the matrix transform, but we gotta undo that now.
+ if (sprite.NoRotation)
+ modAngle += CalcRectWorldAngle(relativeRotation, 4);
+
+ var spritePos = spriteMatrix.Transform(modAngle.RotateVec(localPos));
+
+ // First, check the bounding box that is valid for all orientations
+ if (Bounds.All.Contains(spritePos))
+ return true;
+
+ // Next, get and check the appropriate bounding box for the current sprite orientation
+ var boundsForDir = (sprite.EnableDirectionOverride ? sprite.DirectionOverride : direction) switch
+ {
+ Direction.East => Bounds.East,
+ Direction.North => Bounds.North,
+ Direction.South => Bounds.South,
+ Direction.West => Bounds.West,
+ _ => throw new InvalidOperationException()
+ };
+
+ return boundsForDir.Contains(spritePos);
}
[DataDefinition]
@@ -138,8 +164,6 @@ namespace Content.Client.Clickable
[ViewVariables] [DataField("south")] public Box2 South;
[ViewVariables] [DataField("east")] public Box2 East;
[ViewVariables] [DataField("west")] public Box2 West;
-
- public static DirBoundData Default { get; } = new();
}
}
}
diff --git a/Content.IntegrationTests/Tests/ClickableTest.cs b/Content.IntegrationTests/Tests/ClickableTest.cs
index 9f98ccc03f..263d6fed3f 100644
--- a/Content.IntegrationTests/Tests/ClickableTest.cs
+++ b/Content.IntegrationTests/Tests/ClickableTest.cs
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Content.Client.Clickable;
using Content.Server.GameTicking;
using NUnit.Framework;
+using Robust.Client.Graphics;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.GameObjects;
@@ -71,6 +72,7 @@ namespace Content.IntegrationTests.Tests
EntityUid entity = default;
var clientEntManager = _client.ResolveDependency();
var serverEntManager = _server.ResolveDependency();
+ var eyeManager = _client.ResolveDependency();
var mapManager = _server.ResolveDependency();
var gameTicker = _server.ResolveDependency().GetEntitySystem();
@@ -92,6 +94,9 @@ namespace Content.IntegrationTests.Tests
await _client.WaitPost(() =>
{
+ // these tests currently all assume player eye is 0
+ eyeManager.CurrentEye.Rotation = 0;
+
var clickable = clientEntManager.GetComponent(entity);
hit = clickable.CheckClick((clickPosX, clickPosY) + worldPos!.Value, out _, out _);