-
Notifications
You must be signed in to change notification settings - Fork 168
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Make vox roundstart I believe all the issues are fixed. * Click detection bandaid * Make clickable 1% nicer Still bad. Still doesn't handle multi-viewports well.
- Loading branch information
1 parent
11fc763
commit 56eb86b
Showing
5 changed files
with
223 additions
and
152 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,146 +1,17 @@ | ||
using System.Numerics; | ||
using Robust.Client.GameObjects; | ||
using Robust.Client.Graphics; | ||
using Robust.Client.Utility; | ||
using Robust.Shared.Graphics; | ||
using static Robust.Client.GameObjects.SpriteComponent; | ||
using Direction = Robust.Shared.Maths.Direction; | ||
namespace Content.Client.Clickable; | ||
|
||
namespace Content.Client.Clickable | ||
[RegisterComponent] | ||
public sealed partial class ClickableComponent : Component | ||
{ | ||
[RegisterComponent] | ||
public sealed partial class ClickableComponent : Component | ||
{ | ||
[Dependency] private readonly IClickMapManager _clickMapManager = default!; | ||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!; | ||
|
||
[DataField("bounds")] public DirBoundData? Bounds; | ||
|
||
/// <summary> | ||
/// 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> | ||
/// <param name="worldPos">The world position that was clicked.</param> | ||
/// <param name="drawDepth"> | ||
/// The draw depth for the sprite that captured the click. | ||
/// </param> | ||
/// <returns>True if the click worked, false otherwise.</returns> | ||
public bool CheckClick(SpriteComponent sprite, TransformComponent transform, EntityQuery<TransformComponent> xformQuery, Vector2 worldPos, IEye eye, out int drawDepth, out uint renderOrder, out float bottom) | ||
{ | ||
if (!sprite.Visible) | ||
{ | ||
drawDepth = default; | ||
renderOrder = default; | ||
bottom = default; | ||
return false; | ||
} | ||
|
||
drawDepth = sprite.DrawDepth; | ||
renderOrder = sprite.RenderOrder; | ||
var (spritePos, spriteRot) = _transformSystem.GetWorldPositionRotation(transform.Owner); | ||
var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation); | ||
bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom; | ||
|
||
Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix); | ||
|
||
// This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites. | ||
var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive(); | ||
|
||
Angle cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero; | ||
|
||
// First we get `localPos`, the clicked location in the sprite-coordinate frame. | ||
var entityXform = Matrix3Helpers.CreateInverseTransform(_transformSystem.GetWorldPosition(transform), sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping); | ||
var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix); | ||
|
||
// Check explicitly defined click-able bounds | ||
if (CheckDirBound(sprite, relativeRotation, localPos)) | ||
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; | ||
|
||
// Check the layer's texture, if it has one | ||
if (layer.Texture != null) | ||
{ | ||
// Convert to image coordinates | ||
var imagePos = (Vector2i) (localPos * EyeManager.PixelsPerMeter * new Vector2(1, -1) + layer.Texture.Size / 2f); | ||
|
||
if (_clickMapManager.IsOccluding(layer.Texture, imagePos)) | ||
return true; | ||
} | ||
|
||
// Either we weren't clicking on the texture, or there wasn't one. In which case: check the RSI next | ||
if (layer.ActualRsi is not { } rsi || !rsi.TryGetState(layer.State, out var rsiState)) | ||
continue; | ||
|
||
var dir = Layer.GetDirection(rsiState.RsiDirections, relativeRotation); | ||
[DataField] public DirBoundData? Bounds; | ||
|
||
// convert to layer-local coordinates | ||
layer.GetLayerDrawMatrix(dir, out var matrix); | ||
Matrix3x2.Invert(matrix, out var inverseMatrix); | ||
var layerLocal = Vector2.Transform(localPos, inverseMatrix); | ||
|
||
// Convert to image coordinates | ||
var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.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(rsiState.RsiDirections); | ||
dir = dir.OffsetRsiDir(layer.DirOffset); | ||
|
||
if (_clickMapManager.IsOccluding(layer.ActualRsi!, layer.State, dir, layer.AnimationFrame, layerImagePos)) | ||
return true; | ||
} | ||
|
||
drawDepth = default; | ||
renderOrder = default; | ||
bottom = default; | ||
return false; | ||
} | ||
|
||
public bool CheckDirBound(SpriteComponent sprite, Angle relativeRotation, Vector2 localPos) | ||
{ | ||
if (Bounds == null) | ||
return false; | ||
|
||
// These explicit bounds only work for either 1 or 4 directional sprites. | ||
|
||
// This would be the orientation of a 4-directional sprite. | ||
var direction = relativeRotation.GetCardinalDir(); | ||
|
||
var modLocalPos = sprite.NoRotation | ||
? localPos | ||
: direction.ToAngle().RotateVec(localPos); | ||
|
||
// First, check the bounding box that is valid for all orientations | ||
if (Bounds.All.Contains(modLocalPos)) | ||
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(modLocalPos); | ||
} | ||
|
||
[DataDefinition] | ||
public sealed partial class DirBoundData | ||
{ | ||
[DataField("all")] public Box2 All; | ||
[DataField("north")] public Box2 North; | ||
[DataField("south")] public Box2 South; | ||
[DataField("east")] public Box2 East; | ||
[DataField("west")] public Box2 West; | ||
} | ||
[DataDefinition] | ||
public sealed partial class DirBoundData | ||
{ | ||
[DataField] public Box2 All; | ||
[DataField] public Box2 North; | ||
[DataField] public Box2 South; | ||
[DataField] public Box2 East; | ||
[DataField] public Box2 West; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
using System.Numerics; | ||
using Robust.Client.GameObjects; | ||
using Robust.Client.Graphics; | ||
using Robust.Client.Utility; | ||
using Robust.Shared.Graphics; | ||
|
||
namespace Content.Client.Clickable; | ||
|
||
/// <summary> | ||
/// Handles click detection for sprites. | ||
/// </summary> | ||
public sealed class ClickableSystem : EntitySystem | ||
{ | ||
[Dependency] private readonly IClickMapManager _clickMapManager = default!; | ||
[Dependency] private readonly SharedTransformSystem _transforms = default!; | ||
[Dependency] private readonly SpriteSystem _sprites = default!; | ||
|
||
private EntityQuery<ClickableComponent> _clickableQuery; | ||
private EntityQuery<TransformComponent> _xformQuery; | ||
|
||
public override void Initialize() | ||
{ | ||
base.Initialize(); | ||
_clickableQuery = GetEntityQuery<ClickableComponent>(); | ||
_xformQuery = GetEntityQuery<TransformComponent>(); | ||
} | ||
|
||
/// <summary> | ||
/// 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> | ||
/// <param name="worldPos">The world position that was clicked.</param> | ||
/// <param name="drawDepth"> | ||
/// The draw depth for the sprite that captured the click. | ||
/// </param> | ||
/// <returns>True if the click worked, false otherwise.</returns> | ||
public bool CheckClick(Entity<ClickableComponent?, SpriteComponent, TransformComponent?> entity, Vector2 worldPos, IEye eye, out int drawDepth, out uint renderOrder, out float bottom) | ||
{ | ||
if (!_clickableQuery.Resolve(entity.Owner, ref entity.Comp1, false)) | ||
{ | ||
drawDepth = default; | ||
renderOrder = default; | ||
bottom = default; | ||
return false; | ||
} | ||
|
||
if (!_xformQuery.Resolve(entity.Owner, ref entity.Comp3)) | ||
{ | ||
drawDepth = default; | ||
renderOrder = default; | ||
bottom = default; | ||
return false; | ||
} | ||
|
||
var sprite = entity.Comp2; | ||
var transform = entity.Comp3; | ||
|
||
if (!sprite.Visible) | ||
{ | ||
drawDepth = default; | ||
renderOrder = default; | ||
bottom = default; | ||
return false; | ||
} | ||
|
||
drawDepth = sprite.DrawDepth; | ||
renderOrder = sprite.RenderOrder; | ||
var (spritePos, spriteRot) = _transforms.GetWorldPositionRotation(transform); | ||
var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation); | ||
bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom; | ||
|
||
Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix); | ||
|
||
// This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites. | ||
var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive(); | ||
|
||
var cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero; | ||
|
||
// First we get `localPos`, the clicked location in the sprite-coordinate frame. | ||
var entityXform = Matrix3Helpers.CreateInverseTransform(spritePos, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping); | ||
var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix); | ||
|
||
// Check explicitly defined click-able bounds | ||
if (CheckDirBound((entity.Owner, entity.Comp1, entity.Comp2), relativeRotation, localPos)) | ||
return true; | ||
|
||
// Next check each individual sprite layer using automatically computed click maps. | ||
foreach (var spriteLayer in sprite.AllLayers) | ||
{ | ||
if (spriteLayer is not SpriteComponent.Layer layer || !_sprites.IsVisible(layer)) | ||
{ | ||
continue; | ||
} | ||
|
||
// Check the layer's texture, if it has one | ||
if (layer.Texture != null) | ||
{ | ||
// Convert to image coordinates | ||
var imagePos = (Vector2i) (localPos * EyeManager.PixelsPerMeter * new Vector2(1, -1) + layer.Texture.Size / 2f); | ||
|
||
if (_clickMapManager.IsOccluding(layer.Texture, imagePos)) | ||
return true; | ||
} | ||
|
||
// Either we weren't clicking on the texture, or there wasn't one. In which case: check the RSI next | ||
if (layer.ActualRsi is not { } rsi || !rsi.TryGetState(layer.State, out var rsiState)) | ||
continue; | ||
|
||
var dir = SpriteComponent.Layer.GetDirection(rsiState.RsiDirections, relativeRotation); | ||
|
||
// convert to layer-local coordinates | ||
layer.GetLayerDrawMatrix(dir, out var matrix); | ||
Matrix3x2.Invert(matrix, out var inverseMatrix); | ||
var layerLocal = Vector2.Transform(localPos, inverseMatrix); | ||
|
||
// Convert to image coordinates | ||
var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.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(rsiState.RsiDirections); | ||
dir = dir.OffsetRsiDir(layer.DirOffset); | ||
|
||
if (_clickMapManager.IsOccluding(layer.ActualRsi!, layer.State, dir, layer.AnimationFrame, layerImagePos)) | ||
return true; | ||
} | ||
|
||
drawDepth = default; | ||
renderOrder = default; | ||
bottom = default; | ||
return false; | ||
} | ||
|
||
public bool CheckDirBound(Entity<ClickableComponent, SpriteComponent> entity, Angle relativeRotation, Vector2 localPos) | ||
{ | ||
var clickable = entity.Comp1; | ||
var sprite = entity.Comp2; | ||
|
||
if (clickable.Bounds == null) | ||
return false; | ||
|
||
// These explicit bounds only work for either 1 or 4 directional sprites. | ||
|
||
// This would be the orientation of a 4-directional sprite. | ||
var direction = relativeRotation.GetCardinalDir(); | ||
|
||
var modLocalPos = sprite.NoRotation | ||
? localPos | ||
: direction.ToAngle().RotateVec(localPos); | ||
|
||
// First, check the bounding box that is valid for all orientations | ||
if (clickable.Bounds.All.Contains(modLocalPos)) | ||
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 => clickable.Bounds.East, | ||
Direction.North => clickable.Bounds.North, | ||
Direction.South => clickable.Bounds.South, | ||
Direction.West => clickable.Bounds.West, | ||
_ => throw new InvalidOperationException() | ||
}; | ||
|
||
return boundsForDir.Contains(modLocalPos); | ||
} | ||
} |
Oops, something went wrong.