Compare commits

...

9 Commits

Author SHA1 Message Date
metalgearsloth
2b16e4db96 Version: 249.0.0 2025-03-21 00:52:45 +11:00
metalgearsloth
64f2245194 Add UpdateVisibilityMask method (#5745)
* Add UpdateVisibilityMask method

We tipped over to the point of systems stepping on each other's toes. Now we do the normal thing and just use the eventbus and it makes content a whole lot cleaner.

* Update resolve

* Update name in line with normal.

* Unserialize this

* weh
2025-03-21 00:47:40 +11:00
Milon
1029047e2f fix (#5752) 2025-03-20 22:30:59 +11:00
metalgearsloth
45dc9ad80e Inline polygon vertices (#5758)
* FastPoly

* Inline polygon vertices

No more pooling, pooling bad. No more arrays in engine, only span.

I made a slim version that's only the 4 verts so no padding on it compared to Polygon. Slightly more clamplicated code but entitylookup + mapmanager are both hotpaths and it's easy to do. Memory usage will likely go up for now but heap allocations should drop significantly due to removing the pooling.

* Unhide these

* Fixes

* More fixes

* More fixes

* Avoid potential bomb
2025-03-20 21:26:05 +11:00
metalgearsloth
54ad808eea Add GetWorldManifold overload (#5756)
* Add GetWorldManifold overload

* revert
2025-03-20 21:10:04 +11:00
metalgearsloth
37c75df6a2 Fix showvelocities (#5759)
* Fix showvelocities

Can't use StateRoot anymore so just pretend it's a window.

* Also file-scoped
2025-03-20 21:05:58 +11:00
slarticodefast
e93c1fae61 Add velocity and angular velocity debug overlays (#5693)
* add velocity and angular velocity debug overlays

* minor improvement

* add descriptions

* fix

* review

* Update RELEASE-NOTES.md

* Update RELEASE-NOTES.md

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-03-20 13:10:16 +11:00
metalgearsloth
cd0a35f542 Fix light Aabb query (#5744)
Forgot when this got dropped but we need it for grid-tree query as lights spill over grids.
2025-03-13 19:18:23 +11:00
PJB3005
80f2dc6dd3 Fix BoxContainer stretching causing controls to be made too small
In which I fix a bug by just deleting a ton of code and doing nothing else.

(also I added unit tests)
2025-03-13 01:01:18 +01:00
34 changed files with 862 additions and 443 deletions

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,6 +54,30 @@ END TEMPLATE-->
*None yet*
## 249.0.0
### Breaking changes
* Layer is now read-only on VisibilityComponent and isn't serialized.
### New features
* Added a debug overlay for the linear and angular velocity of all entities on the screen. Use the `showvel` and `showangvel` commands to toggle it.
* Add a GetWorldManifold overload that doesn't require a span of points.
* Added a GetVisMaskEvent. Calling `RefreshVisibilityMask` will raise it and subscribers can update the vismask via the event rather than subscribers having to each manually try and handle the vismask directly.
### Bugfixes
* `BoxContainer` no longer causes stretching children to go below their minimum size.
* Fix lights on other grids getting clipped due to ignoring the light range cvar.
* Fix the `showvelocities` command.
* Fix the DirtyFields overload not being sandbox safe for content.
### Internal
* Polygon vertices are now inlined with FixedArray8 and a separate SlimPolygon using FixedArray4 for hot paths rather than using pooled arrays.
## 248.0.2
### Bugfixes

View File

@@ -428,11 +428,20 @@ cmd-entfo-help = Usage: entfo <entityuid>
The entity UID can be prefixed with 'c' to convert it to a client entity UID.
cmd-fuck-desc = Throws an exception
cmd-fuck-help = Throws an exception
cmd-fuck-help = Usage: fuck
cmd-showpos-desc = Enables debug drawing over all entity positions in the game.
cmd-showpos-desc = Show the position of all entities on the screen.
cmd-showpos-help = Usage: showpos
cmd-showrot-desc = Show the rotation of all entities on the screen.
cmd-showrot-help = Usage: showrot
cmd-showvel-desc = Show the local velocity of all entites on the screen.
cmd-showvel-help = Usage: showvel
cmd-showangvel-desc = Show the angular velocity of all entities on the screen.
cmd-showangvel-help = Usage: showangvel
cmd-sggcell-desc = Lists entities on a snap grid cell.
cmd-sggcell-help = Usage: sggcell <gridID> <vector2i>\nThat vector2i param is in the form x<int>,y<int>.

View File

@@ -173,29 +173,51 @@ namespace Robust.Client.Console.Commands
}
}
internal sealed class ShowPositionsCommand : LocalizedCommands
internal sealed class ShowPositionsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showpos";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
mgr.DebugPositions = !mgr.DebugPositions;
_debugDrawing.DebugPositions = !_debugDrawing.DebugPositions;
}
}
internal sealed class ShowRotationsCommand : LocalizedCommands
internal sealed class ShowRotationsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showrot";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var mgr = _entitySystems.GetEntitySystem<DebugDrawingSystem>();
mgr.DebugRotations = !mgr.DebugRotations;
_debugDrawing.DebugRotations = !_debugDrawing.DebugRotations;
}
}
internal sealed class ShowVelocitiesCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showvel";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_debugDrawing.DebugVelocities = !_debugDrawing.DebugVelocities;
}
}
internal sealed class ShowAngularVelocitiesCommand : LocalizedEntityCommands
{
[Dependency] private readonly DebugDrawingSystem _debugDrawing = default!;
public override string Command => "showangvel";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_debugDrawing.DebugAngularVelocities = !_debugDrawing.DebugAngularVelocities;
}
}

View File

@@ -5,15 +5,15 @@ using Robust.Shared.IoC;
namespace Robust.Client.Console.Commands
{
public sealed class VelocitiesCommand : LocalizedCommands
public sealed class ShowPlayerVelocityCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystems = default!;
public override string Command => "showvelocities";
public override string Command => "showplayervelocity";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_entitySystems.GetEntitySystem<VelocityDebugSystem>().Enabled ^= true;
_entitySystems.GetEntitySystem<ShowPlayerVelocityDebugSystem>().Enabled ^= true;
}
}
}

View File

@@ -1,140 +1,221 @@
using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using System.Numerics;
namespace Robust.Client.Debugging
namespace Robust.Client.Debugging;
/// <summary>
/// A collection of visual debug overlays for the client game.
/// </summary>
public sealed class DebugDrawingSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private bool _debugPositions;
private bool _debugRotations;
private bool _debugVelocities;
private bool _debugAngularVelocities;
/// <summary>
/// A collection of visual debug overlays for the client game.
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
public sealed class DebugDrawingSystem : EntitySystem
public bool DebugPositions
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
private bool _debugPositions;
private bool _debugRotations;
/// <summary>
/// Toggles the visual overlay of the local origin for each entity on screen.
/// </summary>
public bool DebugPositions
get => _debugPositions;
set
{
get => _debugPositions;
set
if (value == DebugPositions)
{
if (value == DebugPositions)
{
return;
}
return;
}
_debugPositions = value;
_debugPositions = value;
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, EntityManager, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
if (value && !_overlayManager.HasOverlay<EntityPositionOverlay>())
{
_overlayManager.AddOverlay(new EntityPositionOverlay(_lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityPositionOverlay>();
}
}
}
/// <summary>
/// Toggles the visual overlay of the local rotation.
/// </summary>
public bool DebugRotations
/// <summary>
/// Toggles the visual overlay of the rotation for each entity on screen.
/// </summary>
public bool DebugRotations
{
get => _debugRotations;
set
{
get => _debugRotations;
set
if (value == DebugRotations)
{
if (value == DebugRotations)
{
return;
}
return;
}
_debugRotations = value;
_debugRotations = value;
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
{
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, EntityManager));
}
else
{
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
}
if (value && !_overlayManager.HasOverlay<EntityRotationOverlay>())
{
_overlayManager.AddOverlay(new EntityRotationOverlay(_lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityRotationOverlay>();
}
}
}
private sealed class EntityPositionOverlay : Overlay
/// <summary>
/// Toggles the visual overlay of the local velocity for each entity on screen.
/// </summary>
public bool DebugVelocities
{
get => _debugVelocities;
set
{
private readonly EntityLookupSystem _lookup;
private readonly IEntityManager _entityManager;
private readonly SharedTransformSystem _transform;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityPositionOverlay(EntityLookupSystem lookup, IEntityManager entityManager, SharedTransformSystem transform)
if (value == DebugVelocities)
{
_lookup = lookup;
_entityManager = entityManager;
_transform = transform;
return;
}
protected internal override void Draw(in OverlayDrawArgs args)
_debugVelocities = value;
if (value && !_overlayManager.HasOverlay<EntityVelocityOverlay>())
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(entity);
var xLine = worldRotation.RotateVec(Vector2.UnitX);
var yLine = worldRotation.RotateVec(Vector2.UnitY);
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
}
_overlayManager.AddOverlay(new EntityVelocityOverlay(EntityManager, _lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityVelocityOverlay>();
}
}
}
private sealed class EntityRotationOverlay : Overlay
/// <summary>
/// Toggles the visual overlay of the angular velocity for each entity on screen.
/// </summary>
public bool DebugAngularVelocities
{
get => _debugAngularVelocities;
set
{
private readonly EntityLookupSystem _lookup;
private readonly IEntityManager _entityManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityRotationOverlay(EntityLookupSystem lookup, IEntityManager entityManager)
if (value == DebugAngularVelocities)
{
_lookup = lookup;
_entityManager = entityManager;
return;
}
protected internal override void Draw(in OverlayDrawArgs args)
_debugAngularVelocities = value;
if (value && !_overlayManager.HasOverlay<EntityAngularVelocityOverlay>())
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
_overlayManager.AddOverlay(new EntityAngularVelocityOverlay(EntityManager, _lookup, _transform));
}
else
{
_overlayManager.RemoveOverlay<EntityAngularVelocityOverlay>();
}
}
}
private sealed class EntityPositionOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
foreach (var entity in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = xformQuery.GetComponent(entity).GetWorldPositionRotation();
protected internal override void Draw(in OverlayDrawArgs args)
{
const float stubLength = 0.25f;
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
}
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
var xLine = worldRotation.RotateVec(Vector2.UnitX);
var yLine = worldRotation.RotateVec(Vector2.UnitY);
worldHandle.DrawLine(center, center + xLine * stubLength, Color.Red);
worldHandle.DrawLine(center, center + yLine * stubLength, Color.Green);
}
}
}
private sealed class EntityRotationOverlay(EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float stubLength = 0.25f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
var (center, worldRotation) = _transform.GetWorldPositionRotation(uid);
var drawLine = worldRotation.RotateVec(-Vector2.UnitY);
worldHandle.DrawLine(center, center + drawLine * stubLength, Color.Red);
}
}
}
private sealed class EntityVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float multiplier = 0.2f;
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
continue;
var center = _transform.GetWorldPosition(uid);
var localVelocity = physicsComp.LinearVelocity;
if (localVelocity != Vector2.Zero)
worldHandle.DrawLine(center, center + localVelocity * multiplier, Color.Yellow);
}
}
}
private sealed class EntityAngularVelocityOverlay(IEntityManager _entityManager, EntityLookupSystem _lookup, SharedTransformSystem _transform) : Overlay
{
public override OverlaySpace Space => OverlaySpace.WorldSpace;
protected internal override void Draw(in OverlayDrawArgs args)
{
const float multiplier = (float)(0.2 / (2 * System.Math.PI));
var worldHandle = (DrawingHandleWorld) args.DrawingHandle;
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
foreach (var uid in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds))
{
if(!physicsQuery.TryGetComponent(uid, out var physicsComp))
continue;
var center = _transform.GetWorldPosition(uid);
var angularVelocity = physicsComp.AngularVelocity;
if (angularVelocity != 0.0f)
worldHandle.DrawCircle(center, angularVelocity * multiplier, angularVelocity > 0 ? Color.Magenta : Color.Blue, false);
}
}
}
}

View File

@@ -117,7 +117,7 @@ namespace Robust.Client.GameObjects
base.DirtyField(uid, comp, fieldName, metadata);
}
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
public override void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields)
{
// TODO Prediction
// does the client actually need to dirty the field?

View File

@@ -0,0 +1,66 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics.Components;
namespace Robust.Client.GameObjects;
public sealed class ShowPlayerVelocityDebugSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
internal bool Enabled
{
get => _label.Parent != null;
set
{
if (value)
{
_uiManager.WindowRoot.AddChild(_label);
}
else
{
_label.Orphan();
}
}
}
private Label _label = default!;
public override void Initialize()
{
base.Initialize();
_label = new Label();
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (!Enabled)
{
_label.Visible = false;
return;
}
var player = _playerManager.LocalEntity;
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{
_label.Visible = false;
return;
}
var screenPos = _eyeManager.WorldToScreen(_transform.GetWorldPosition(Transform(player.Value)));
LayoutContainer.SetPosition(_label, screenPos + new Vector2(0, 50));
_label.Visible = true;
_label.Text = $"Speed: {body.LinearVelocity.Length():0.00}\nLinear: {body.LinearVelocity.X:0.00}, {body.LinearVelocity.Y:0.00}\nAngular: {body.AngularVelocity}";
}
}

View File

@@ -1,54 +0,0 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
namespace Robust.Client.GameObjects
{
public sealed class VelocityDebugSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly TransformSystem _transform = default!;
internal bool Enabled { get; set; }
private Label _label = default!;
public override void Initialize()
{
base.Initialize();
_label = new Label();
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_label);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (!Enabled)
{
_label.Visible = false;
return;
}
var player = _playerManager.LocalEntity;
if (player == null || !EntityManager.TryGetComponent(player.Value, out PhysicsComponent? body))
{
_label.Visible = false;
return;
}
var screenPos = _eyeManager.WorldToScreen(_transform.GetWorldPosition(Transform(player.Value)));
LayoutContainer.SetPosition(_label, screenPos + new Vector2(0, 50));
_label.Visible = true;
_label.Text = $"Speed: {body.LinearVelocity.Length():0.00}\nLinear: {body.LinearVelocity.X:0.00}, {body.LinearVelocity.Y:0.00}\nAngular: {body.AngularVelocity}";
}
}
}

View File

@@ -98,9 +98,11 @@ namespace Robust.Client.Graphics.Clyde
private LightCapacityComparer _lightCap = new();
private ShadowCapacityComparer _shadowCap = new ShadowCapacityComparer();
private float _maxLightRadius;
private unsafe void InitLighting()
{
_cfg.OnValueChanged(CVars.MaxLightRadius, val => { _maxLightRadius = val;}, true);
// Other...
LoadLightingShaders();
@@ -617,8 +619,9 @@ namespace Robust.Client.Graphics.Clyde
// Use worldbounds for this one as we only care if the light intersects our actual bounds
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
var state = (this, count: 0, shadowCastingCount: 0, xforms, worldAABB);
var lightAabb = worldAABB.Enlarged(_maxLightRadius);
foreach (var (uid, comp) in _lightTreeSystem.GetIntersectingTrees(map, worldAABB))
foreach (var (uid, comp) in _lightTreeSystem.GetIntersectingTrees(map, lightAabb))
{
var bounds = _transformSystem.GetInvWorldMatrix(uid, xforms).TransformBox(worldBounds);
comp.Tree.QueryAabb(ref state, LightQuery, bounds);

View File

@@ -71,21 +71,11 @@ namespace Robust.Client.UserInterface.Controls
}
// First, we measure non-stretching children.
var stretching = new List<Control>();
float totalStretchRatio = 0;
foreach (var child in Children)
{
if (!child.Visible)
continue;
var stretch = Vertical ? child.VerticalExpand : child.HorizontalExpand;
if (stretch)
{
totalStretchRatio += child.SizeFlagsStretchRatio;
stretching.Add(child);
continue;
}
child.Measure(availableSize);
if (Vertical)
@@ -102,35 +92,6 @@ namespace Robust.Client.UserInterface.Controls
}
}
if (stretching.Count == 0)
return desiredSize;
// Then we measure stretching children
foreach (var child in stretching)
{
var size = availableSize;
if (Vertical)
{
size.Y *= child.SizeFlagsStretchRatio / totalStretchRatio;
child.Measure(size);
desiredSize.Y += child.DesiredSize.Y;
desiredSize.X = Math.Max(desiredSize.X, child.DesiredSize.X);
}
else
{
size.X *= child.SizeFlagsStretchRatio / totalStretchRatio;
child.Measure(size);
desiredSize.X += child.DesiredSize.X;
desiredSize.Y = Math.Max(desiredSize.Y, child.DesiredSize.Y);
}
// TODO Maybe make BoxContainer.MeasureOverride more rigorous.
// This should check if size < desired size. If it is, treat child as non-stretching (see the code in
// ArrangeOverride). This requires remeasuring all stretching controls + the control that just became
// non-stretching. But the re-measured controls might then become smaller (e.g. rich text wrapping),
// leading to a recursion problem.
}
return desiredSize;
}

View File

@@ -1,20 +1,22 @@
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.Shared.GameObjects
namespace Robust.Shared.GameObjects;
/// <summary>
/// Controls PVS visibility of entities. THIS COMPONENT CONTROLS WHETHER ENTITIES ARE NETWORKED TO PLAYERS
/// AND SHOULD NOT BE USED AS THE SOLE WAY TO HIDE AN ENTITY FROM A PLAYER.
/// </summary>
[RegisterComponent]
[Access(typeof(SharedVisibilitySystem))]
public sealed partial class VisibilityComponent : Component
{
/// <summary>
/// Controls PVS visibility of entities. THIS COMPONENT CONTROLS WHETHER ENTITIES ARE NETWORKED TO PLAYERS
/// AND SHOULD NOT BE USED AS THE SOLE WAY TO HIDE AN ENTITY FROM A PLAYER.
/// The visibility layer for the entity.
/// Players whose visibility masks don't match this won't get state updates for it.
/// </summary>
[RegisterComponent]
[Access(typeof(SharedVisibilitySystem))]
public sealed partial class VisibilityComponent : Component
{
/// <summary>
/// The visibility layer for the entity.
/// Players whose visibility masks don't match this won't get state updates for it.
/// </summary>
[DataField("layer")]
public ushort Layer = 1;
}
/// <remarks>
/// Not serialized as visibility is normally immediate (i.e. prior to MapInit) and content should be handling it as such.
/// </remarks>
[DataField(readOnly: true)]
public ushort Layer = 1;
}

View File

@@ -59,7 +59,7 @@ public abstract partial class EntityManager
Dirty(uid, comp, metadata);
}
public virtual void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
public virtual void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields)
where T : IComponentDelta
{
var compReg = ComponentFactory.GetRegistration(CompIdx.Index<T>());

View File

@@ -172,12 +172,22 @@ public partial class EntitySystem
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params ReadOnlySpan<string> fields)
protected void DirtyFields<T>(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields)
where T : IComponentDelta
{
EntityManager.DirtyFields(uid, comp, meta, fields);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void DirtyFields<T>(Entity<T?> ent, MetaDataComponent? meta, params string[] fields)
where T : IComponentDelta
{
if (!Resolve(ent, ref ent.Comp))
return;
EntityManager.DirtyFields(ent, ent.Comp, meta, fields);
}
/// <summary>
/// Marks a component as dirty. This also implicitly dirties the entity this component belongs to.
/// </summary>

View File

@@ -9,6 +9,7 @@ using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Shapes;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Utility;
@@ -406,7 +407,7 @@ public sealed partial class EntityLookupSystem
var broadphaseInv = _transform.GetInvWorldMatrix(lookupUid);
var localBounds = broadphaseInv.TransformBounds(worldBounds);
var polygon = _physics.GetPooled(localBounds);
var polygon = new SlimPolygon(localBounds);
var result = AnyEntitiesIntersecting(lookupUid,
polygon,
localBounds.CalcBoundingBox(),
@@ -414,7 +415,6 @@ public sealed partial class EntityLookupSystem
flags,
ignored);
_physics.ReturnPooled(polygon);
return result;
}
@@ -458,9 +458,8 @@ public sealed partial class EntityLookupSystem
{
if (mapId == MapId.Nullspace) return false;
var polygon = _physics.GetPooled(worldAABB);
var polygon = new SlimPolygon(worldAABB);
var result = AnyEntitiesIntersecting(mapId, polygon, Physics.Transform.Empty, flags);
_physics.ReturnPooled(polygon);
return result;
}
@@ -475,9 +474,8 @@ public sealed partial class EntityLookupSystem
{
if (mapId == MapId.Nullspace) return;
var polygon = _physics.GetPooled(worldAABB);
var polygon = new SlimPolygon(worldAABB);
AddEntitiesIntersecting(mapId, intersecting, polygon, Physics.Transform.Empty, flags);
_physics.ReturnPooled(polygon);
}
#endregion
@@ -487,18 +485,16 @@ public sealed partial class EntityLookupSystem
public bool AnyEntitiesIntersecting(MapId mapId, Box2Rotated worldBounds, LookupFlags flags = DefaultFlags)
{
// Don't need to check contained entities as they have the same bounds as the parent.
var polygon = _physics.GetPooled(worldBounds);
var polygon = new SlimPolygon(worldBounds);
var result = AnyEntitiesIntersecting(mapId, polygon, Physics.Transform.Empty, flags);
_physics.ReturnPooled(polygon);
return result;
}
public HashSet<EntityUid> GetEntitiesIntersecting(MapId mapId, Box2Rotated worldBounds, LookupFlags flags = DefaultFlags)
{
var intersecting = new HashSet<EntityUid>();
var polygon = _physics.GetPooled(worldBounds);
var polygon = new SlimPolygon(worldBounds);
AddEntitiesIntersecting(mapId, intersecting, polygon, Physics.Transform.Empty, flags);
_physics.ReturnPooled(polygon);
return intersecting;
}
@@ -761,11 +757,10 @@ public sealed partial class EntityLookupSystem
return;
var localAABB = _transform.GetInvWorldMatrix(gridId).TransformBox(worldAABB);
var polygon = _physics.GetPooled(localAABB);
var polygon = new SlimPolygon(localAABB);
AddEntitiesIntersecting(gridId, intersecting, polygon, localAABB, Physics.Transform.Empty, flags, lookup);
AddContained(intersecting, flags);
_physics.ReturnPooled(polygon);
}
public void GetEntitiesIntersecting(EntityUid gridId, Box2Rotated worldBounds, HashSet<EntityUid> intersecting, LookupFlags flags = DefaultFlags)
@@ -774,11 +769,10 @@ public sealed partial class EntityLookupSystem
return;
var localBounds = _transform.GetInvWorldMatrix(gridId).TransformBounds(worldBounds);
var polygon = _physics.GetPooled(localBounds);
var polygon = new SlimPolygon(localBounds);
AddEntitiesIntersecting(gridId, intersecting, polygon, localBounds.CalcBoundingBox(), Physics.Transform.Empty, flags, lookup);
AddContained(intersecting, flags);
_physics.ReturnPooled(polygon);
}
#endregion

View File

@@ -121,9 +121,8 @@ public sealed partial class EntityLookupSystem
if (!_broadQuery.Resolve(lookupUid, ref lookup))
return;
var polygon = _physics.GetPooled(localAABB);
var polygon = new SlimPolygon(localAABB);
AddEntitiesIntersecting(lookupUid, intersecting, polygon, localAABB, Physics.Transform.Empty, flags, query, lookup);
_physics.ReturnPooled(polygon);
}
private void AddEntitiesIntersecting<T, TShape>(
@@ -252,11 +251,10 @@ public sealed partial class EntityLookupSystem
if (!_broadQuery.Resolve(lookupUid, ref lookup))
return false;
var polygon = _physics.GetPooled(localAABB);
var polygon = new SlimPolygon(localAABB);
var (lookupPos, lookupRot) = _transform.GetWorldPositionRotation(lookupUid);
var transform = new Transform(lookupPos, lookupRot);
var result = AnyComponentsIntersecting(lookupUid, polygon, localAABB, transform, flags, query, ignored, lookup);
_physics.ReturnPooled(polygon);
return result;
}
@@ -427,10 +425,9 @@ public sealed partial class EntityLookupSystem
public bool AnyComponentsIntersecting(Type type, MapId mapId, Box2 worldAABB, EntityUid? ignored = null, LookupFlags flags = DefaultFlags)
{
var polygon = _physics.GetPooled(worldAABB);
var polygon = new SlimPolygon(worldAABB);
var transform = Physics.Transform.Empty;
var result = AnyComponentsIntersecting(type, mapId, polygon, transform, ignored, flags);
_physics.ReturnPooled(polygon);
return result;
}
@@ -496,33 +493,30 @@ public sealed partial class EntityLookupSystem
if (mapId == MapId.Nullspace)
return;
var polygon = _physics.GetPooled(worldAABB);
var polygon = new SlimPolygon(worldAABB);
var transform = Physics.Transform.Empty;
GetEntitiesIntersecting(type, mapId, polygon, transform, intersecting, flags);
_physics.ReturnPooled(polygon);
}
public void GetEntitiesIntersecting<T>(MapId mapId, Box2Rotated worldBounds, HashSet<Entity<T>> entities, LookupFlags flags = DefaultFlags) where T : IComponent
{
if (mapId == MapId.Nullspace) return;
var polygon = _physics.GetPooled(worldBounds);
var polygon = new SlimPolygon(worldBounds);
var shapeTransform = Physics.Transform.Empty;
GetEntitiesIntersecting(mapId, polygon, shapeTransform, entities, flags);
_physics.ReturnPooled(polygon);
}
public void GetEntitiesIntersecting<T>(MapId mapId, Box2 worldAABB, HashSet<Entity<T>> entities, LookupFlags flags = DefaultFlags) where T : IComponent
{
if (mapId == MapId.Nullspace) return;
var polygon = _physics.GetPooled(worldAABB);
var polygon = new SlimPolygon(worldAABB);
var shapeTransform = Physics.Transform.Empty;
GetEntitiesIntersecting(mapId, polygon, shapeTransform, entities, flags);
_physics.ReturnPooled(polygon);
}
#endregion

View File

@@ -26,7 +26,7 @@ public sealed partial class EntityLookupSystem
if (!_broadQuery.Resolve(lookupUid, ref lookup))
return;
var lookupPoly = new Polygon(localAABB);
var lookupPoly = new SlimPolygon(localAABB);
AddEntitiesIntersecting(lookupUid, intersecting, lookupPoly, localAABB, Physics.Transform.Empty, flags, lookup);
}
@@ -40,7 +40,7 @@ public sealed partial class EntityLookupSystem
if (!_broadQuery.Resolve(lookupUid, ref lookup))
return;
var shape = new Polygon(localBounds);
var shape = new SlimPolygon(localBounds);
var localAABB = localBounds.CalcBoundingBox();
AddEntitiesIntersecting(lookupUid, intersecting, shape, localAABB, Physics.Transform.Empty, flags);
@@ -55,7 +55,7 @@ public sealed partial class EntityLookupSystem
if (!_broadQuery.Resolve(lookupUid, ref lookup))
return false;
var shape = new Polygon(localAABB);
var shape = new SlimPolygon(localAABB);
return AnyEntitiesIntersecting(lookupUid, shape, localAABB, Physics.Transform.Empty, flags, ignored, lookup);
}

View File

@@ -159,6 +159,10 @@ public abstract class SharedEyeSystem : EntitySystem
eye.Comp.PvsScale = Math.Clamp(scale, 0.1f, 100f);
}
/// <summary>
/// Overwrites visibility mask of an entity's eye.
/// If you wish for other systems to potentially change it consider raising <see cref="RefreshVisibilityMask"/>.
/// </summary>
public void SetVisibilityMask(EntityUid uid, int value, EyeComponent? eyeComponent = null)
{
if (!Resolve(uid, ref eyeComponent))
@@ -170,4 +174,32 @@ public abstract class SharedEyeSystem : EntitySystem
eyeComponent.VisibilityMask = value;
DirtyField(uid, eyeComponent, nameof(EyeComponent.VisibilityMask));
}
/// <summary>
/// Updates the visibility mask for an entity by raising a <see cref="GetVisMaskEvent"/>
/// </summary>
public void RefreshVisibilityMask(Entity<EyeComponent?> entity)
{
if (!Resolve(entity.Owner, ref entity.Comp, false))
return;
var ev = new GetVisMaskEvent()
{
Entity = entity.Owner,
};
RaiseLocalEvent(entity.Owner, ref ev, true);
SetVisibilityMask(entity.Owner, ev.VisibilityMask, entity.Comp);
}
}
/// <summary>
/// Event raised to update the vismask of an entity's eye.
/// </summary>
[ByRefEvent]
public record struct GetVisMaskEvent()
{
public EntityUid Entity;
public int VisibilityMask = EyeComponent.DefaultVisibilityMask;
}

View File

@@ -8,6 +8,7 @@ using Robust.Shared.Map.Enumerators;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Shapes;
namespace Robust.Shared.Map;
@@ -224,48 +225,42 @@ internal partial class MapManager
public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
var polygon = _physics.GetPooled(worldAABB);
var polygon = new SlimPolygon(worldAABB);
FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, callback, approx, includeMap);
_physics.ReturnPooled(polygon);
}
public void FindGridsIntersecting<TState>(EntityUid mapEnt, Box2 worldAABB, ref TState state, GridCallback<TState> callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
var polygon = _physics.GetPooled(worldAABB);
var polygon = new SlimPolygon(worldAABB);
FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, ref state, callback, approx, includeMap);
_physics.ReturnPooled(polygon);
}
public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref List<Entity<MapGridComponent>> grids,
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
var polygon = _physics.GetPooled(worldAABB);
var polygon = new SlimPolygon(worldAABB);
FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, ref grids, approx, includeMap);
_physics.ReturnPooled(polygon);
}
public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate,
bool includeMap = IMapManager.IncludeMap)
{
var polygon = _physics.GetPooled(worldBounds);
var polygon = new SlimPolygon(worldBounds);
FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, callback, approx, includeMap);
_physics.ReturnPooled(polygon);
}
public void FindGridsIntersecting<TState>(EntityUid mapEnt, Box2Rotated worldBounds, ref TState state, GridCallback<TState> callback,
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
var polygon = _physics.GetPooled(worldBounds);
var polygon = new SlimPolygon(worldBounds);
FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, ref state, callback, approx, includeMap);
_physics.ReturnPooled(polygon);
}
public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref List<Entity<MapGridComponent>> grids,
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
var polygon = _physics.GetPooled(worldBounds);
var polygon = new SlimPolygon(worldBounds);
FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, ref grids, approx, includeMap);
_physics.ReturnPooled(polygon);
}
#endregion

View File

@@ -12,7 +12,9 @@ internal sealed partial class CollisionManager
/// <param name="xfA">The transform for the first shape.</param>
/// <param name="xfB">The transform for the seconds shape.</param>
/// <returns></returns>
public bool TestOverlap<T, U>(T shapeA, int indexA, U shapeB, int indexB, in Transform xfA, in Transform xfB) where T : IPhysShape where U : IPhysShape
public bool TestOverlap<T, U>(T shapeA, int indexA, U shapeB, int indexB, in Transform xfA, in Transform xfB)
where T : IPhysShape
where U : IPhysShape
{
var input = new DistanceInput();

View File

@@ -21,12 +21,9 @@
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Shapes;
@@ -46,7 +43,7 @@ internal ref struct DistanceProxy
// GJK using Voronoi regions (Christer Ericson) and Barycentric coordinates.
internal DistanceProxy(Vector2[] vertices, float radius)
internal DistanceProxy(ReadOnlySpan<Vector2> vertices, float radius)
{
Vertices = vertices;
Radius = radius;
@@ -71,9 +68,18 @@ internal ref struct DistanceProxy
case ShapeType.Polygon:
if (shape is Polygon poly)
{
Vertices = poly.Vertices.AsSpan()[..poly.VertexCount];
Span<Vector2> verts = new Vector2[poly.VertexCount];
poly._vertices.AsSpan[..poly.VertexCount].CopyTo(verts);
Vertices = verts;
Radius = poly.Radius;
}
else if (shape is SlimPolygon fast)
{
Span<Vector2> verts = new Vector2[fast.VertexCount];
fast._vertices.AsSpan[..fast.VertexCount].CopyTo(verts);
Vertices = verts;
Radius = fast.Radius;
}
else
{
var polyShape = Unsafe.As<PolygonShape>(shape);
@@ -151,7 +157,7 @@ internal ref struct DistanceProxy
return Vertices[bestIndex];
}
internal static DistanceProxy MakeProxy(Vector2[] vertices, int count, float radius )
internal static DistanceProxy MakeProxy(ReadOnlySpan<Vector2> vertices, int count, float radius )
{
count = Math.Min(count, PhysicsConstants.MaxPolygonVertices);
var proxy = new DistanceProxy(vertices[..count], radius);

View File

@@ -173,10 +173,25 @@ namespace Robust.Shared.Physics.Collision.Shapes
{
}
internal PolygonShape(SlimPolygon poly)
{
Vertices = new Vector2[poly.VertexCount];
Normals = new Vector2[poly.VertexCount];
poly._vertices.AsSpan[..VertexCount].CopyTo(Vertices);
poly._normals.AsSpan[..VertexCount].CopyTo(Normals);
Centroid = poly.Centroid;
}
internal PolygonShape(Polygon poly)
{
Vertices = poly.Vertices;
Normals = poly.Normals;
Vertices = new Vector2[poly.VertexCount];
Normals = new Vector2[poly.VertexCount];
poly._vertices.AsSpan[..VertexCount].CopyTo(Vertices);
poly._normals.AsSpan[..VertexCount].CopyTo(Normals);
Centroid = poly.Centroid;
}

View File

@@ -150,6 +150,15 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
Friction = MathF.Sqrt((FixtureA?.Friction ?? 0.0f) * (FixtureB?.Friction ?? 0.0f));
}
public void GetWorldManifold(Transform transformA, Transform transformB, out Vector2 normal)
{
var shapeA = FixtureA?.Shape!;
var shapeB = FixtureB?.Shape!;
Span<Vector2> points = stackalloc Vector2[PhysicsConstants.MaxPolygonVertices];
SharedPhysicsSystem.InitializeManifold(ref Manifold, transformA, transformB, shapeA.Radius, shapeB.Radius, out normal, points);
}
/// <summary>
/// Gets the world manifold.
/// </summary>

View File

@@ -11,19 +11,28 @@ namespace Robust.Shared.Physics.Shapes;
// Internal so people don't use it when it will have breaking changes very soon.
internal record struct Polygon : IPhysShape
{
public static Polygon Empty = new(Box2.Empty);
[DataField]
public byte VertexCount { get; internal set; }
/// <summary>
/// Vertices associated with this polygon. Will be sliced to <see cref="VertexCount"/>
/// </summary>
/// <remarks>
/// Consider using _vertices if doing engine work.
/// </remarks>
public Vector2[] Vertices => _vertices.AsSpan[..VertexCount].ToArray();
public Vector2[] Normals => _normals.AsSpan[..VertexCount].ToArray();
[DataField]
public Vector2[] Vertices;
internal FixedArray8<Vector2> _vertices;
public byte VertexCount;
public Vector2[] Normals;
internal FixedArray8<Vector2> _normals;
public Vector2 Centroid;
public int ChildCount => 1;
public float Radius { get; set; }
public float Radius { get; set; } = PhysicsConstants.PolygonRadius;
public ShapeType ShapeType => ShapeType.Polygon;
// Hopefully this one is short-lived for a few months
@@ -35,67 +44,60 @@ internal record struct Polygon : IPhysShape
public Polygon(PolygonShape polyShape)
{
Unsafe.SkipInit(out this);
Vertices = new Vector2[polyShape.VertexCount];
Normals = new Vector2[polyShape.Normals.Length];
Radius = polyShape.Radius;
Centroid = polyShape.Centroid;
VertexCount = (byte) polyShape.VertexCount;
Array.Copy(polyShape.Vertices, Vertices, Vertices.Length);
Array.Copy(polyShape.Normals, Normals, Vertices.Length);
VertexCount = (byte) Vertices.Length;
polyShape.Vertices.AsSpan()[..VertexCount].CopyTo(_vertices.AsSpan);
polyShape.Normals.AsSpan()[..VertexCount].CopyTo(_normals.AsSpan);
}
/// <summary>
/// Manually constructed polygon for internal use to take advantage of pooling.
/// </summary>
internal Polygon(Vector2[] vertices, Vector2[] normals, Vector2 centroid, byte count)
public Polygon(Box2 box)
{
Unsafe.SkipInit(out this);
Vertices = vertices;
Normals = normals;
Centroid = centroid;
VertexCount = count;
}
public Polygon(Box2 aabb)
{
Unsafe.SkipInit(out this);
Vertices = new Vector2[4];
Normals = new Vector2[4];
Radius = 0f;
Vertices[0] = aabb.BottomLeft;
Vertices[1] = aabb.BottomRight;
Vertices[2] = aabb.TopRight;
Vertices[3] = aabb.TopLeft;
Normals[0] = new Vector2(0.0f, -1.0f);
Normals[1] = new Vector2(1.0f, 0.0f);
Normals[2] = new Vector2(0.0f, 1.0f);
Normals[3] = new Vector2(-1.0f, 0.0f);
VertexCount = 4;
Centroid = aabb.Center;
_vertices._00 = box.BottomLeft;
_vertices._01 = box.BottomRight;
_vertices._02 = box.TopRight;
_vertices._03 = box.TopLeft;
_normals._00 = new Vector2(0.0f, -1.0f);
_normals._01 = new Vector2(1.0f, 0.0f);
_normals._02 = new Vector2(0.0f, 1.0f);
_normals._03 = new Vector2(-1.0f, 0.0f);
}
public Polygon(Box2Rotated bounds)
{
Unsafe.SkipInit(out this);
Vertices = new Vector2[4];
Normals = new Vector2[4];
Radius = 0f;
Vertices[0] = bounds.BottomLeft;
Vertices[1] = bounds.BottomRight;
Vertices[2] = bounds.TopRight;
Vertices[3] = bounds.TopLeft;
CalculateNormals(Normals, 4);
VertexCount = 4;
_vertices._00 = bounds.BottomLeft;
_vertices._01 = bounds.BottomRight;
_vertices._02 = bounds.TopRight;
_vertices._03 = bounds.TopLeft;
CalculateNormals(_vertices.AsSpan, _normals.AsSpan, 4);
Centroid = bounds.Center;
}
/// <summary>
/// Manually constructed polygon for internal use to take advantage of pooling.
/// </summary>
internal Polygon(ReadOnlySpan<Vector2> vertices, ReadOnlySpan<Vector2> normals, Vector2 centroid, byte count)
{
Unsafe.SkipInit(out this);
vertices[..VertexCount].CopyTo(_vertices.AsSpan);
normals[..VertexCount].CopyTo(_normals.AsSpan);
Centroid = centroid;
VertexCount = count;
Radius = 0f;
}
public Polygon(Vector2[] vertices)
{
Unsafe.SkipInit(out this);
@@ -104,16 +106,15 @@ internal record struct Polygon : IPhysShape
if (hull.Count < 3)
{
VertexCount = 0;
Vertices = [];
Normals = [];
return;
}
VertexCount = (byte) vertices.Length;
Vertices = vertices;
Normals = new Vector2[vertices.Length];
var vertSpan = _vertices.AsSpan;
vertices.AsSpan().CopyTo(vertSpan);
Set(hull);
Centroid = ComputeCentroid(Vertices);
Centroid = ComputeCentroid(vertSpan);
}
public static explicit operator Polygon(PolygonShape polyShape)
@@ -125,32 +126,32 @@ internal record struct Polygon : IPhysShape
{
DebugTools.Assert(hull.Count >= 3);
var vertexCount = hull.Count;
Array.Resize(ref Vertices, vertexCount);
Array.Resize(ref Normals, vertexCount);
var verts = _vertices.AsSpan;
var norms = _normals.AsSpan;
for (var i = 0; i < vertexCount; i++)
{
Vertices[i] = hull.Points[i];
verts[i] = hull.Points[i];
}
// Compute normals. Ensure the edges have non-zero length.
CalculateNormals(Normals, vertexCount);
CalculateNormals(verts, norms, vertexCount);
}
internal void CalculateNormals(Span<Vector2> normals, int count)
public static void CalculateNormals(ReadOnlySpan<Vector2> vertices, Span<Vector2> normals, int count)
{
for (var i = 0; i < count; i++)
{
var next = i + 1 < count ? i + 1 : 0;
var edge = Vertices[next] - Vertices[i];
var edge = vertices[next] - vertices[i];
DebugTools.Assert(edge.LengthSquared() > float.Epsilon * float.Epsilon);
var temp = Vector2Helpers.Cross(edge, 1f);
Normals[i] = temp.Normalized();
normals[i] = temp.Normalized();
}
}
private static Vector2 ComputeCentroid(Vector2[] vs)
public static Vector2 ComputeCentroid(ReadOnlySpan<Vector2> vs)
{
var count = vs.Length;
DebugTools.Assert(count >= 3);
@@ -191,13 +192,15 @@ internal record struct Polygon : IPhysShape
public Box2 ComputeAABB(Transform transform, int childIndex)
{
DebugTools.Assert(VertexCount > 0);
DebugTools.Assert(childIndex == 0);
var lower = Transform.Mul(transform, Vertices[0]);
var verts = _vertices.AsSpan;
var lower = Transform.Mul(transform, verts[0]);
var upper = lower;
for (var i = 1; i < VertexCount; ++i)
{
var v = Transform.Mul(transform, Vertices[i]);
var v = Transform.Mul(transform, verts[i]);
lower = Vector2.Min(lower, v);
upper = Vector2.Max(upper, v);
}
@@ -208,12 +211,41 @@ internal record struct Polygon : IPhysShape
public bool Equals(IPhysShape? other)
{
if (other is not PolygonShape poly) return false;
if (VertexCount != poly.VertexCount) return false;
if (other is SlimPolygon slim)
{
return Equals(slim);
}
return other is Polygon poly && Equals(poly);
}
public bool Equals(Polygon other)
{
if (VertexCount != other.VertexCount) return false;
var ourVerts = _vertices.AsSpan;
var otherVerts = other._vertices.AsSpan;
for (var i = 0; i < VertexCount; i++)
{
var vert = Vertices[i];
if (!vert.Equals(poly.Vertices[i])) return false;
var vert = ourVerts[i];
if (!vert.Equals(otherVerts[i])) return false;
}
return true;
}
public bool Equals(SlimPolygon other)
{
if (VertexCount != other.VertexCount) return false;
var ourVerts = _vertices.AsSpan;
var otherVerts = other._vertices.AsSpan;
for (var i = 0; i < VertexCount; i++)
{
var vert = ourVerts[i];
if (!vert.Equals(otherVerts[i])) return false;
}
return true;
@@ -221,6 +253,6 @@ internal record struct Polygon : IPhysShape
public override int GetHashCode()
{
return HashCode.Combine(VertexCount, Vertices.AsSpan(0, VertexCount).ToArray(), Radius);
return HashCode.Combine(VertexCount, _vertices.AsSpan.ToArray(), Radius);
}
}

View File

@@ -0,0 +1,103 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Shapes;
/// <summary>
/// Polygon backed by FixedArray4 to be smaller.
/// Useful for internal ops where the inputs are boxes to avoid the additional padding.
/// </summary>
internal record struct SlimPolygon : IPhysShape
{
public Vector2[] Vertices => _vertices.AsSpan[..VertexCount].ToArray();
public Vector2[] Normals => _normals.AsSpan[..VertexCount].ToArray();
[DataField]
public FixedArray4<Vector2> _vertices;
public FixedArray4<Vector2> _normals;
public Vector2 Centroid;
public byte VertexCount => 4;
public int ChildCount => 1;
public float Radius { get; set; } = PhysicsConstants.PolygonRadius;
public ShapeType ShapeType => ShapeType.Polygon;
public SlimPolygon(Box2 box)
{
Unsafe.SkipInit(out this);
Radius = 0f;
_vertices._00 = box.BottomLeft;
_vertices._01 = box.BottomRight;
_vertices._02 = box.TopRight;
_vertices._03 = box.TopLeft;
_normals._00 = new Vector2(0.0f, -1.0f);
_normals._01 = new Vector2(1.0f, 0.0f);
_normals._02 = new Vector2(0.0f, 1.0f);
_normals._03 = new Vector2(-1.0f, 0.0f);
}
public SlimPolygon(Box2Rotated bounds)
{
Unsafe.SkipInit(out this);
Radius = 0f;
_vertices._00 = bounds.BottomLeft;
_vertices._01 = bounds.BottomRight;
_vertices._02 = bounds.TopRight;
_vertices._03 = bounds.TopLeft;
Polygon.CalculateNormals(_vertices.AsSpan, _normals.AsSpan, 4);
Centroid = bounds.Center;
}
public Box2 ComputeAABB(Transform transform, int childIndex)
{
DebugTools.Assert(VertexCount > 0);
DebugTools.Assert(childIndex == 0);
var verts = _vertices.AsSpan;
var lower = Transform.Mul(transform, verts[0]);
var upper = lower;
for (var i = 1; i < VertexCount; ++i)
{
var v = Transform.Mul(transform, verts[i]);
lower = Vector2.Min(lower, v);
upper = Vector2.Max(upper, v);
}
var r = new Vector2(Radius, Radius);
return new Box2(lower - r, upper + r);
}
public bool Equals(SlimPolygon other)
{
return Radius.Equals(other.Radius) && _vertices.AsSpan[..VertexCount].SequenceEqual(other._vertices.AsSpan[..VertexCount]);
}
public readonly override int GetHashCode()
{
return HashCode.Combine(_vertices, _normals, Centroid, Radius);
}
public bool Equals(IPhysShape? other)
{
if (other is Polygon poly)
{
return poly.Equals(this);
}
return other is SlimPolygon slim && Equals(slim);
}
}

View File

@@ -40,13 +40,29 @@ namespace Robust.Shared.Physics.Systems
return true;
}
case SlimPolygon slim:
{
var pLocal = Physics.Transform.MulT(xform.Quaternion2D, worldPoint - xform.Position);
var norms = slim._normals.AsSpan;
var verts = slim._vertices.AsSpan;
for (var i = 0; i < slim.VertexCount; i++)
{
var dot = Vector2.Dot(norms[i], pLocal - verts[i]);
if (dot > 0f) return false;
}
return true;
}
case Polygon poly:
{
var pLocal = Physics.Transform.MulT(xform.Quaternion2D, worldPoint - xform.Position);
var norms = poly._normals.AsSpan;
var verts = poly._vertices.AsSpan;
for (var i = 0; i < poly.VertexCount; i++)
{
var dot = Vector2.Dot(poly.Normals[i], pLocal - poly.Vertices[i]);
var dot = Vector2.Dot(norms[i], pLocal - verts[i]);
if (dot > 0f) return false;
}
@@ -86,6 +102,10 @@ namespace Robust.Shared.Physics.Systems
var polygon = (PolygonShape) aabb;
GetMassData(polygon, ref data, density);
break;
case Polygon fastPoly:
return GetMassData(new PolygonShape(fastPoly), density);
case SlimPolygon slim:
return GetMassData(new PolygonShape(slim), density);
case PolygonShape poly:
// Polygon mass, centroid, and inertia.
// Let rho be the polygon density in mass per unit area.
@@ -195,6 +215,12 @@ namespace Robust.Shared.Physics.Systems
var polygon = (PolygonShape) aabb;
GetMassData(polygon, ref data, density);
break;
case Polygon fastPoly:
GetMassData(new PolygonShape(fastPoly), ref data, density);
break;
case SlimPolygon slim:
GetMassData(new PolygonShape(slim), ref data, density);
break;
case PolygonShape poly:
// Polygon mass, centroid, and inertia.
// Let rho be the polygon density in mass per unit area.

View File

@@ -191,6 +191,12 @@ public sealed partial class RayCastSystem
private CastOutput RayCastPolygon(RayCastInput input, Polygon shape)
{
var verts = shape._vertices.AsSpan;
var output = new CastOutput()
{
Fraction = 0f,
};
if (shape.Radius == 0.0f)
{
// Put the ray into the polygon's frame of reference.
@@ -201,18 +207,15 @@ public sealed partial class RayCastSystem
var index = -1;
var output = new CastOutput()
{
Fraction = 0f,
};
var norms = shape._normals.AsSpan;
for ( var i = 0; i < shape.VertexCount; ++i )
{
// p = p1 + a * d
// dot(normal, p - v) = 0
// dot(normal, p1 - v) + a * dot(normal, d) = 0
float numerator = Vector2.Dot(shape.Normals[i], Vector2.Subtract( shape.Vertices[i], p1 ) );
float denominator = Vector2.Dot(shape.Normals[i], d );
float numerator = Vector2.Dot(norms[i], Vector2.Subtract( verts[i], p1 ) );
float denominator = Vector2.Dot(norms[i], d );
if ( denominator == 0.0f )
{
@@ -257,7 +260,7 @@ public sealed partial class RayCastSystem
if (index >= 0)
{
output.Fraction = lower;
output.Normal = shape.Normals[index];
output.Normal = norms[index];
output.Point = Vector2.Add(p1, lower * d);
output.Hit = true;
}
@@ -265,17 +268,24 @@ public sealed partial class RayCastSystem
return output;
}
// TODO_ERIN this is not working for ray vs box (zero radii)
Span<Vector2> proxyBVerts = new Vector2[]
{
input.Origin,
};
// TODO_ERIN this is not working for ray vs box (zero radii)
var castInput = new ShapeCastPairInput
{
ProxyA = DistanceProxy.MakeProxy(shape.Vertices, shape.VertexCount, shape.Radius),
ProxyB = DistanceProxy.MakeProxy([input.Origin], 1, 0.0f),
ProxyA = DistanceProxy.MakeProxy(verts, shape.VertexCount, shape.Radius),
ProxyB = DistanceProxy.MakeProxy(proxyBVerts, 1, 0.0f),
TransformA = Physics.Transform.Empty,
TransformB = Physics.Transform.Empty,
TranslationB = input.Translation,
MaxFraction = input.MaxFraction
};
return ShapeCast(castInput);
ShapeCast(ref output, castInput);
return output;
}
// Ray vs line segment
@@ -436,12 +446,9 @@ public sealed partial class RayCastSystem
// "Smooth Mesh Contacts with GJK" in Game Physics Pearls. 2010
// todo this is failing when used to raycast a box
// todo this converges slowly with a radius
private CastOutput ShapeCast(ShapeCastPairInput input)
private void ShapeCast(ref CastOutput output, in ShapeCastPairInput input)
{
var output = new CastOutput()
{
Fraction = input.MaxFraction,
};
output.Fraction = input.MaxFraction;
var proxyA = input.ProxyA;
var count = input.ProxyB.Vertices.Length;
@@ -513,15 +520,15 @@ public sealed partial class RayCastSystem
if ( vr <= 0.0f )
{
// miss
return output;
}
return;
}
lambda = ( vp - sigma ) / vr;
if ( lambda > maxFraction )
{
// too far
return output;
}
return;
}
// reset the simplex
simplex.Count = 0;
@@ -562,7 +569,7 @@ public sealed partial class RayCastSystem
{
// Overlap
// Yes this means you need to manually query for overlaps.
return output;
return;
}
// Get search direction.
@@ -576,7 +583,7 @@ public sealed partial class RayCastSystem
if ( iter == 0 || lambda == 0.0f )
{
// Initial overlap
return output;
return;
}
// Prepare output.
@@ -591,7 +598,6 @@ public sealed partial class RayCastSystem
output.Fraction = lambda;
output.Iterations = iter;
output.Hit = true;
return output;
}
private int FindSupport(DistanceProxy proxy, Vector2 direction)
@@ -613,9 +619,14 @@ public sealed partial class RayCastSystem
private CastOutput ShapeCastCircle(ShapeCastInput input, PhysShapeCircle shape)
{
Span<Vector2> proxyAVerts = new[]
{
shape.Position,
};
var pairInput = new ShapeCastPairInput
{
ProxyA = DistanceProxy.MakeProxy([shape.Position], 1, shape.Radius ),
ProxyA = DistanceProxy.MakeProxy(proxyAVerts, 1, shape.Radius ),
ProxyB = DistanceProxy.MakeProxy(input.Points, input.Count, input.Radius ),
TransformA = Physics.Transform.Empty,
TransformB = Physics.Transform.Empty,
@@ -623,7 +634,8 @@ public sealed partial class RayCastSystem
MaxFraction = input.MaxFraction
};
var output = ShapeCast(pairInput);
var output = new CastOutput();
ShapeCast(ref output, pairInput);
return output;
}
@@ -631,7 +643,7 @@ public sealed partial class RayCastSystem
{
var pairInput = new ShapeCastPairInput
{
ProxyA = DistanceProxy.MakeProxy(shape.Vertices, shape.VertexCount, shape.Radius),
ProxyA = DistanceProxy.MakeProxy(shape._vertices.AsSpan, shape.VertexCount, shape.Radius),
ProxyB = DistanceProxy.MakeProxy(input.Points, input.Count, input.Radius),
TransformA = Physics.Transform.Empty,
TransformB = Physics.Transform.Empty,
@@ -639,21 +651,30 @@ public sealed partial class RayCastSystem
MaxFraction = input.MaxFraction
};
var output = ShapeCast(pairInput);
var output = new CastOutput();
ShapeCast(ref output, pairInput);
return output;
}
private CastOutput ShapeCastSegment(ShapeCastInput input, EdgeShape shape)
{
var pairInput = new ShapeCastPairInput();
pairInput.ProxyA = DistanceProxy.MakeProxy([shape.Vertex0], 2, 0.0f);
pairInput.ProxyB = DistanceProxy.MakeProxy(input.Points, input.Count, input.Radius);
pairInput.TransformA = Physics.Transform.Empty;
pairInput.TransformB = Physics.Transform.Empty;
pairInput.TranslationB = input.Translation;
pairInput.MaxFraction = input.MaxFraction;
Span<Vector2> proxyAVerts = new[]
{
shape.Vertex0,
};
var output = ShapeCast(pairInput);
var pairInput = new ShapeCastPairInput
{
ProxyA = DistanceProxy.MakeProxy(proxyAVerts, 2, 0.0f),
ProxyB = DistanceProxy.MakeProxy(input.Points, input.Count, input.Radius),
TransformA = Physics.Transform.Empty,
TransformB = Physics.Transform.Empty,
TranslationB = input.Translation,
MaxFraction = input.MaxFraction
};
var output = new CastOutput();
ShapeCast(ref output, pairInput);
return output;
}

View File

@@ -275,6 +275,9 @@ public sealed partial class RayCastSystem : EntitySystem
case PhysShapeCircle circle:
CastCircle(entity, ref result, circle, originTransform, translation, filter, callback);
break;
case SlimPolygon slim:
CastPolygon(entity, ref result, new PolygonShape(slim), originTransform, translation, filter, callback);
break;
case Polygon poly:
CastPolygon(entity, ref result, new PolygonShape(poly), originTransform, translation, filter, callback);
break;

View File

@@ -1,55 +0,0 @@
using System.Buffers;
using System.Numerics;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Shapes;
namespace Robust.Shared.Physics.Systems;
public abstract partial class SharedPhysicsSystem
{
/// <summary>
/// Gets a polygon with pooled arrays backing it.
/// </summary>
internal Polygon GetPooled(Box2 box)
{
var vertices = ArrayPool<Vector2>.Shared.Rent(4);
var normals = ArrayPool<Vector2>.Shared.Rent(4);
var centroid = box.Center;
vertices[0] = box.BottomLeft;
vertices[1] = box.BottomRight;
vertices[2] = box.TopRight;
vertices[3] = box.TopLeft;
normals[0] = new Vector2(0.0f, -1.0f);
normals[1] = new Vector2(1.0f, 0.0f);
normals[2] = new Vector2(0.0f, 1.0f);
normals[3] = new Vector2(-1.0f, 0.0f);
return new Polygon(vertices, normals, centroid, 4);
}
internal Polygon GetPooled(Box2Rotated box)
{
var vertices = ArrayPool<Vector2>.Shared.Rent(4);
var normals = ArrayPool<Vector2>.Shared.Rent(4);
var centroid = box.Center;
vertices[0] = box.BottomLeft;
vertices[1] = box.BottomRight;
vertices[2] = box.TopRight;
vertices[3] = box.TopLeft;
var polygon = new Polygon(vertices, normals, centroid, 4);
polygon.CalculateNormals(normals, 4);
return polygon;
}
internal void ReturnPooled(Polygon polygon)
{
ArrayPool<Vector2>.Shared.Return(polygon.Vertices);
ArrayPool<Vector2>.Shared.Return(polygon.Normals);
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
@@ -86,7 +87,7 @@ namespace Robust.Shared.Utility
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 2);
}
internal struct FixedArray4<T>
internal struct FixedArray4<T> : IEquatable<FixedArray4<T>>
{
public T _00;
public T _01;
@@ -94,9 +95,27 @@ namespace Robust.Shared.Utility
public T _03;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 4);
public bool Equals(FixedArray4<T> other)
{
return _00?.Equals(other._00) == true &&
_01?.Equals(other._01) == true &&
_02?.Equals(other._02) == true &&
_03?.Equals(other._03) == true;
}
public override bool Equals(object? obj)
{
return obj is FixedArray4<T> other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(_00, _01, _02, _03);
}
}
internal struct FixedArray8<T>
internal struct FixedArray8<T> : IEquatable<FixedArray8<T>>
{
public T _00;
public T _01;
@@ -108,6 +127,28 @@ namespace Robust.Shared.Utility
public T _07;
public Span<T> AsSpan => MemoryMarshal.CreateSpan(ref _00, 8);
public bool Equals(FixedArray8<T> other)
{
return _00?.Equals(other._00) == true &&
_01?.Equals(other._01) == true &&
_02?.Equals(other._02) == true &&
_03?.Equals(other._03) == true &&
_04?.Equals(other._04) == true &&
_05?.Equals(other._05) == true &&
_06?.Equals(other._06) == true &&
_07?.Equals(other._07) == true;
}
public override bool Equals(object? obj)
{
return obj is FixedArray8<T> other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(_00, _01, _02, _03, _04, _05, _06, _07);
}
}
internal struct FixedArray16<T>

View File

@@ -123,5 +123,71 @@ namespace Robust.UnitTesting.Client.UserInterface.Controls
Assert.That(control2.Position, Is.EqualTo(new Vector2(0, 65)));
Assert.That(control2.Size, Is.EqualTo(new Vector2(30, 15)));
}
[Test]
public void TestTwoExpandRatio()
{
var boxContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
SetSize = new Vector2(100, 10),
Children =
{
new Control
{
MinWidth = 10,
HorizontalExpand = true,
SizeFlagsStretchRatio = 20,
},
new Control
{
MinWidth = 10,
HorizontalExpand = true,
SizeFlagsStretchRatio = 80
}
}
};
boxContainer.Arrange(UIBox2.FromDimensions(Vector2.Zero, boxContainer.SetSize));
Assert.Multiple(() =>
{
Assert.That(boxContainer.GetChild(0).Width, Is.EqualTo(20));
Assert.That(boxContainer.GetChild(1).Width, Is.EqualTo(80));
});
}
[Test]
public void TestTwoExpandOneSmall()
{
var boxContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
SetSize = new Vector2(100, 10),
Children =
{
new Control
{
MinWidth = 30,
HorizontalExpand = true,
SizeFlagsStretchRatio = 20,
},
new Control
{
MinWidth = 30,
HorizontalExpand = true,
SizeFlagsStretchRatio = 80
}
}
};
boxContainer.Arrange(UIBox2.FromDimensions(Vector2.Zero, boxContainer.SetSize));
Assert.Multiple(() =>
{
Assert.That(boxContainer.GetChild(0).Width, Is.EqualTo(30));
Assert.That(boxContainer.GetChild(1).Width, Is.EqualTo(70));
});
}
}
}

View File

@@ -46,13 +46,13 @@ public sealed class RayCast_Test
// Polygon
// - Initial overlap, no shapecast
new(new Polygon(Box2.UnitCentered), new Transform(Vector2.UnitY / 2f, Angle.Zero), Vector2.UnitY, null),
new(new SlimPolygon(Box2.UnitCentered), new Transform(Vector2.UnitY / 2f, Angle.Zero), Vector2.UnitY, null),
// - Cast
new(new Polygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), Vector2.UnitY, new Vector2(0.5f, 1f - PhysicsConstants.PolygonRadius)),
new(new SlimPolygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), Vector2.UnitY, new Vector2(0.5f, 1f - PhysicsConstants.PolygonRadius)),
// - Miss
new(new Polygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), -Vector2.UnitY, null),
new(new SlimPolygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), -Vector2.UnitY, null),
};
[Test, TestCaseSource(nameof(_rayCases))]

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Collision.Shapes;
namespace Robust.UnitTesting.Shared.Physics

View File

@@ -9,10 +9,23 @@ namespace Robust.UnitTesting.Shared.Physics;
[TestFixture]
public sealed class Polygon_Test
{
/// <summary>
/// Check that Slim and normal Polygon are equals
/// </summary>
[Test]
public void TestSlim()
{
var slim = new SlimPolygon(Box2.UnitCentered.Translated(Vector2.One));
var poly = new Polygon(Box2.UnitCentered.Translated(Vector2.One));
Assert.That(slim.Equals(poly));
}
[Test]
public void TestAABB()
{
var shape = new Polygon(Box2.UnitCentered.Translated(Vector2.One));
var shape = new SlimPolygon(Box2.UnitCentered.Translated(Vector2.One));
Assert.That(shape.ComputeAABB(Transform.Empty, 0), Is.EqualTo(Box2.UnitCentered.Translated(Vector2.One)));
}
@@ -20,8 +33,8 @@ public sealed class Polygon_Test
[Test]
public void TestBox2()
{
var shape = new Polygon(Box2.UnitCentered.Translated(Vector2.One));
Assert.That(shape.Vertices, Is.EqualTo(new Vector2[]
var shape = new SlimPolygon(Box2.UnitCentered.Translated(Vector2.One));
Assert.That(shape._vertices.AsSpan.ToArray(), Is.EqualTo(new Vector2[]
{
new Vector2(0.5f, 0.5f),
new Vector2(1.5f, 0.5f),
@@ -33,9 +46,9 @@ public sealed class Polygon_Test
[Test]
public void TestBox2Rotated()
{
var shape = new Polygon(new Box2Rotated(Box2.UnitCentered, Angle.FromDegrees(90)));
var shape = new SlimPolygon(new Box2Rotated(Box2.UnitCentered, Angle.FromDegrees(90)));
Assert.That(shape.Vertices, Is.EqualTo(new Vector2[]
Assert.That(shape._vertices.AsSpan.ToArray(), Is.EqualTo(new Vector2[]
{
new Vector2(0.5f, -0.5f),
new Vector2(0.5f, 0.5f),