Redo ClickableComponent (#6221)
This commit is contained in:
@@ -7,7 +7,7 @@ using Robust.Shared.IoC;
|
|||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
using Robust.Shared.Serialization.Manager.Attributes;
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
using TerraFX.Interop.Windows;
|
using static Robust.Client.GameObjects.SpriteComponent;
|
||||||
|
|
||||||
namespace Content.Client.Clickable
|
namespace Content.Client.Clickable
|
||||||
{
|
{
|
||||||
@@ -15,11 +15,14 @@ namespace Content.Client.Clickable
|
|||||||
public sealed class ClickableComponent : Component
|
public sealed class ClickableComponent : Component
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IClickMapManager _clickMapManager = default!;
|
[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;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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 <see cref="Bounds"/>). If that fails, attempts to use automatically generated click maps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="worldPos">The world position that was clicked.</param>
|
/// <param name="worldPos">The world position that was clicked.</param>
|
||||||
/// <param name="drawDepth">
|
/// <param name="drawDepth">
|
||||||
@@ -28,106 +31,129 @@ namespace Content.Client.Clickable
|
|||||||
/// <returns>True if the click worked, false otherwise.</returns>
|
/// <returns>True if the click worked, false otherwise.</returns>
|
||||||
public bool CheckClick(Vector2 worldPos, out int drawDepth, out uint renderOrder)
|
public bool CheckClick(Vector2 worldPos, out int drawDepth, out uint renderOrder)
|
||||||
{
|
{
|
||||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
if (!_entMan.TryGetComponent(Owner, out ISpriteComponent? sprite) || !sprite.Visible)
|
||||||
if (!entMan.TryGetComponent(Owner, out ISpriteComponent? sprite) || !sprite.Visible)
|
|
||||||
{
|
{
|
||||||
drawDepth = default;
|
drawDepth = default;
|
||||||
renderOrder = default;
|
renderOrder = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var transform = entMan.GetComponent<TransformComponent>(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;
|
drawDepth = sprite.DrawDepth;
|
||||||
renderOrder = sprite.RenderOrder;
|
renderOrder = sprite.RenderOrder;
|
||||||
return found;
|
|
||||||
|
var transform = _entMan.GetComponent<TransformComponent>(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]
|
[DataDefinition]
|
||||||
@@ -138,8 +164,6 @@ namespace Content.Client.Clickable
|
|||||||
[ViewVariables] [DataField("south")] public Box2 South;
|
[ViewVariables] [DataField("south")] public Box2 South;
|
||||||
[ViewVariables] [DataField("east")] public Box2 East;
|
[ViewVariables] [DataField("east")] public Box2 East;
|
||||||
[ViewVariables] [DataField("west")] public Box2 West;
|
[ViewVariables] [DataField("west")] public Box2 West;
|
||||||
|
|
||||||
public static DirBoundData Default { get; } = new();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
|||||||
using Content.Client.Clickable;
|
using Content.Client.Clickable;
|
||||||
using Content.Server.GameTicking;
|
using Content.Server.GameTicking;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared;
|
using Robust.Shared;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
@@ -71,6 +72,7 @@ namespace Content.IntegrationTests.Tests
|
|||||||
EntityUid entity = default;
|
EntityUid entity = default;
|
||||||
var clientEntManager = _client.ResolveDependency<IEntityManager>();
|
var clientEntManager = _client.ResolveDependency<IEntityManager>();
|
||||||
var serverEntManager = _server.ResolveDependency<IEntityManager>();
|
var serverEntManager = _server.ResolveDependency<IEntityManager>();
|
||||||
|
var eyeManager = _client.ResolveDependency<IEyeManager>();
|
||||||
var mapManager = _server.ResolveDependency<IMapManager>();
|
var mapManager = _server.ResolveDependency<IMapManager>();
|
||||||
var gameTicker = _server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
|
var gameTicker = _server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
|
||||||
|
|
||||||
@@ -92,6 +94,9 @@ namespace Content.IntegrationTests.Tests
|
|||||||
|
|
||||||
await _client.WaitPost(() =>
|
await _client.WaitPost(() =>
|
||||||
{
|
{
|
||||||
|
// these tests currently all assume player eye is 0
|
||||||
|
eyeManager.CurrentEye.Rotation = 0;
|
||||||
|
|
||||||
var clickable = clientEntManager.GetComponent<ClickableComponent>(entity);
|
var clickable = clientEntManager.GetComponent<ClickableComponent>(entity);
|
||||||
|
|
||||||
hit = clickable.CheckClick((clickPosX, clickPosY) + worldPos!.Value, out _, out _);
|
hit = clickable.CheckClick((clickPosX, clickPosY) + worldPos!.Value, out _, out _);
|
||||||
|
|||||||
Reference in New Issue
Block a user