mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
Generalize component trees (#3598)
This commit is contained in:
@@ -35,7 +35,11 @@ END TEMPLATE-->
|
||||
|
||||
### Breaking changes
|
||||
|
||||
*None yet*
|
||||
* ClientOccluderComponent has been removed & OccluderComponent component functions have been moved to the occluder system.
|
||||
* The OccluderDirectionsEvent namespace and properties have changed.
|
||||
* The rendering and occluder trees have been refactored to use generic render tree systems.
|
||||
* Several pointlight and occluder component properties now need to be set via system methods.
|
||||
|
||||
|
||||
### New features
|
||||
|
||||
|
||||
12
Robust.Client/ComponentTrees/LightTreeComponent.cs
Normal file
12
Robust.Client/ComponentTrees/LightTreeComponent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Robust.Client.ComponentTrees;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class LightTreeComponent: Component, IComponentTreeComponent<PointLightComponent>
|
||||
{
|
||||
public DynamicTree<ComponentTreeEntry<PointLightComponent>> Tree { get; set; } = default!;
|
||||
}
|
||||
38
Robust.Client/ComponentTrees/LightTreeSystem.cs
Normal file
38
Robust.Client/ComponentTrees/LightTreeSystem.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Robust.Client.ComponentTrees;
|
||||
|
||||
public sealed class LightTreeSystem : ComponentTreeSystem<LightTreeComponent, PointLightComponent>
|
||||
{
|
||||
#region Component Tree Overrides
|
||||
protected override bool DoFrameUpdate => true;
|
||||
protected override bool DoTickUpdate => false;
|
||||
protected override bool Recursive => true;
|
||||
protected override int InitialCapacity => 128;
|
||||
|
||||
protected override Box2 ExtractAabb(in ComponentTreeEntry<PointLightComponent> entry, Vector2 pos, Angle rot)
|
||||
{
|
||||
// Really we should be rotating the light offset by the relative rotation. But I assume the light offset will
|
||||
// always be relatively small, so fuck it, this is probably faster than having to compute the angle every time.
|
||||
var radius = entry.Component.Radius + entry.Component.Offset.Length;
|
||||
return new Box2(pos - radius, pos + radius);
|
||||
}
|
||||
|
||||
protected override Box2 ExtractAabb(in ComponentTreeEntry<PointLightComponent> entry)
|
||||
{
|
||||
if (entry.Component.TreeUid == null)
|
||||
return default;
|
||||
|
||||
var pos = XformSystem.GetRelativePosition(
|
||||
entry.Transform,
|
||||
entry.Component.TreeUid.Value,
|
||||
GetEntityQuery<TransformComponent>());
|
||||
|
||||
return ExtractAabb(in entry, pos, default);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
12
Robust.Client/ComponentTrees/SpriteTreeComponent.cs
Normal file
12
Robust.Client/ComponentTrees/SpriteTreeComponent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Robust.Client.ComponentTrees;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class SpriteTreeComponent: Component, IComponentTreeComponent<SpriteComponent>
|
||||
{
|
||||
public DynamicTree<ComponentTreeEntry<SpriteComponent>> Tree { get; set; } = default!;
|
||||
}
|
||||
40
Robust.Client/ComponentTrees/SpriteTreeSystem.cs
Normal file
40
Robust.Client/ComponentTrees/SpriteTreeSystem.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Robust.Client.ComponentTrees;
|
||||
|
||||
public sealed class SpriteTreeSystem : ComponentTreeSystem<SpriteTreeComponent, SpriteComponent>
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SpriteComponent, QueueSpriteTreeUpdateEvent>(OnQueueUpdate);
|
||||
}
|
||||
|
||||
private void OnQueueUpdate(EntityUid uid, SpriteComponent component, ref QueueSpriteTreeUpdateEvent args)
|
||||
=> QueueTreeUpdate(uid, component, args.Xform);
|
||||
|
||||
// TODO remove this when finally ECSing sprite components
|
||||
[ByRefEvent]
|
||||
internal readonly struct QueueSpriteTreeUpdateEvent
|
||||
{
|
||||
public readonly TransformComponent Xform;
|
||||
public QueueSpriteTreeUpdateEvent(TransformComponent xform)
|
||||
{
|
||||
Xform = xform;
|
||||
}
|
||||
}
|
||||
|
||||
#region Component Tree Overrides
|
||||
protected override bool DoFrameUpdate => true;
|
||||
protected override bool DoTickUpdate => false;
|
||||
protected override bool Recursive => true;
|
||||
protected override int InitialCapacity => 1024;
|
||||
|
||||
protected override Box2 ExtractAabb(in ComponentTreeEntry<SpriteComponent> entry, Vector2 pos, Angle rot)
|
||||
=> entry.Component.CalculateRotatedBoundingBox(pos, rot, default).CalcBoundingBox();
|
||||
#endregion
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -22,8 +23,6 @@ namespace Robust.Client.GameObjects
|
||||
RegisterClass<ContainerManagerComponent>();
|
||||
RegisterClass<InputComponent>();
|
||||
RegisterClass<SpriteComponent>();
|
||||
RegisterClass<ClientOccluderComponent>();
|
||||
RegisterClass<OccluderTreeComponent>();
|
||||
RegisterClass<EyeComponent>();
|
||||
RegisterClass<AnimationPlayerComponent>();
|
||||
RegisterClass<TimerComponent>();
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[ComponentReference(typeof(OccluderComponent))]
|
||||
public sealed class ClientOccluderComponent : OccluderComponent
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
[ViewVariables] private (EntityUid, Vector2i) _lastPosition;
|
||||
[ViewVariables] internal OccluderDir Occluding { get; private set; }
|
||||
[ViewVariables] internal uint UpdateGeneration { get; set; }
|
||||
|
||||
public override bool Enabled
|
||||
{
|
||||
get => base.Enabled;
|
||||
set
|
||||
{
|
||||
base.Enabled = value;
|
||||
|
||||
SendDirty();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
if (_entityManager.GetComponent<TransformComponent>(Owner).Anchored)
|
||||
{
|
||||
AnchorStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void AnchorStateChanged()
|
||||
{
|
||||
var xform = _entityManager.GetComponent<TransformComponent>(Owner);
|
||||
SendDirty(xform);
|
||||
|
||||
if(!xform.Anchored)
|
||||
return;
|
||||
|
||||
var gridId = xform.GridUid ?? throw new InvalidOperationException("Anchored without a grid");
|
||||
var grid = _mapManager.GetGrid(gridId);
|
||||
_lastPosition = (gridId, grid.TileIndicesFor(xform.Coordinates));
|
||||
}
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
SendDirty();
|
||||
}
|
||||
|
||||
private void SendDirty(TransformComponent? xform = null)
|
||||
{
|
||||
xform ??= _entityManager.GetComponent<TransformComponent>(Owner);
|
||||
if (xform.Anchored)
|
||||
{
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local,
|
||||
new OccluderDirtyEvent(Owner, _lastPosition));
|
||||
}
|
||||
}
|
||||
|
||||
internal void Update()
|
||||
{
|
||||
Occluding = OccluderDir.None;
|
||||
|
||||
if (Deleted)
|
||||
return;
|
||||
|
||||
// Content may want to override the default behavior for occlusion.
|
||||
var xform = _entityManager.GetComponent<TransformComponent>(Owner);
|
||||
var ev = new OccluderDirectionsEvent
|
||||
{
|
||||
Component = xform,
|
||||
};
|
||||
|
||||
_entityManager.EventBus.RaiseLocalEvent(Owner, ref ev, true);
|
||||
|
||||
if (ev.Handled)
|
||||
{
|
||||
Occluding = ev.Directions;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!xform.Anchored)
|
||||
return;
|
||||
|
||||
var grid = _mapManager.GetGrid(xform.GridUid ?? throw new InvalidOperationException("Anchored without a grid"));
|
||||
var position = xform.Coordinates;
|
||||
void CheckDir(Direction dir, OccluderDir oclDir)
|
||||
{
|
||||
foreach (var neighbor in grid.GetInDir(position, dir))
|
||||
{
|
||||
if (_entityManager.TryGetComponent(neighbor, out ClientOccluderComponent? comp) && comp.Enabled)
|
||||
{
|
||||
Occluding |= oclDir;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var angle = xform.LocalRotation;
|
||||
var dirRolling = angle.GetCardinalDir();
|
||||
// dirRolling starts at effective south
|
||||
|
||||
CheckDir(dirRolling, OccluderDir.South);
|
||||
dirRolling = dirRolling.GetClockwise90Degrees();
|
||||
|
||||
CheckDir(dirRolling, OccluderDir.West);
|
||||
dirRolling = dirRolling.GetClockwise90Degrees();
|
||||
|
||||
CheckDir(dirRolling, OccluderDir.North);
|
||||
dirRolling = dirRolling.GetClockwise90Degrees();
|
||||
|
||||
CheckDir(dirRolling, OccluderDir.East);
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum OccluderDir : byte
|
||||
{
|
||||
None = 0,
|
||||
North = 1,
|
||||
East = 1 << 1,
|
||||
South = 1 << 2,
|
||||
West = 1 << 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised by occluders when trying to get occlusion directions.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public struct OccluderDirectionsEvent
|
||||
{
|
||||
public bool Handled = false;
|
||||
public OccluderDir Directions = OccluderDir.None;
|
||||
public TransformComponent Component = default!;
|
||||
|
||||
public OccluderDirectionsEvent() {}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -11,11 +12,14 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedPointLightComponent))]
|
||||
public sealed class PointLightComponent : SharedPointLightComponent
|
||||
public sealed class PointLightComponent : SharedPointLightComponent, IComponentTreeEntry<PointLightComponent>
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
public EntityUid? TreeUid { get; set; }
|
||||
|
||||
internal bool TreeUpdateQueued { get; set; }
|
||||
public DynamicTree<ComponentTreeEntry<PointLightComponent>>? Tree { get; set; }
|
||||
|
||||
public bool AddToTree => Enabled && !ContainerOccluded;
|
||||
public bool TreeUpdateQueued { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Animatable]
|
||||
@@ -25,33 +29,8 @@ namespace Robust.Client.GameObjects
|
||||
set => base.Color = value;
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Animatable]
|
||||
public override bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
if (_enabled == value) return;
|
||||
base.Enabled = value;
|
||||
_entityManager.EventBus.RaiseLocalEvent(Owner, new PointLightUpdateEvent(), true);
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool ContainerOccluded
|
||||
{
|
||||
get => _containerOccluded;
|
||||
set
|
||||
{
|
||||
if (_containerOccluded == value) return;
|
||||
|
||||
_containerOccluded = value;
|
||||
_entityManager.EventBus.RaiseLocalEvent(Owner, new PointLightUpdateEvent(), true);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _containerOccluded;
|
||||
[Access(typeof(PointLightSystem))]
|
||||
public bool ContainerOccluded;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the light mask should automatically rotate with the entity. (like a flashlight)
|
||||
@@ -96,55 +75,11 @@ namespace Robust.Client.GameObjects
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Texture? Mask { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool VisibleNested
|
||||
{
|
||||
get => _visibleNested;
|
||||
set => _visibleNested = value;
|
||||
}
|
||||
|
||||
[DataField("nestedvisible")]
|
||||
private bool _visibleNested = true;
|
||||
[DataField("autoRot")]
|
||||
private bool _maskAutoRotate;
|
||||
private Angle _rotation;
|
||||
|
||||
[DataField("mask")]
|
||||
internal string? _maskPath;
|
||||
|
||||
/// <summary>
|
||||
/// Radius, in meters.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[Animatable]
|
||||
public override float Radius
|
||||
{
|
||||
get => _radius;
|
||||
set
|
||||
{
|
||||
if (MathHelper.CloseToPercent(value, _radius)) return;
|
||||
|
||||
base.Radius = value;
|
||||
_entityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedEvent(this));
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
internal RenderingTreeComponent? RenderTree { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PointLightRadiusChangedEvent : EntityEventArgs
|
||||
{
|
||||
public PointLightComponent PointLightComponent { get; }
|
||||
|
||||
public PointLightRadiusChangedEvent(PointLightComponent pointLightComponent)
|
||||
{
|
||||
PointLightComponent = pointLightComponent;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PointLightUpdateEvent : EntityEventArgs
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -22,10 +21,9 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public sealed class SpriteBoundsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eye = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly RenderingTreeSystem _renderingTree = default!;
|
||||
[Dependency] private readonly SpriteTreeSystem _spriteTree = default!;
|
||||
|
||||
private SpriteBoundsOverlay? _overlay;
|
||||
|
||||
@@ -41,7 +39,7 @@ namespace Robust.Client.GameObjects
|
||||
if (_enabled)
|
||||
{
|
||||
DebugTools.AssertNull(_overlay);
|
||||
_overlay = new SpriteBoundsOverlay(_renderingTree, _eye, _entityManager);
|
||||
_overlay = new SpriteBoundsOverlay(_spriteTree, _entityManager);
|
||||
_overlayManager.AddOverlay(_overlay);
|
||||
}
|
||||
else
|
||||
@@ -60,14 +58,12 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
private readonly IEyeManager _eyeManager;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private RenderingTreeSystem _renderTree;
|
||||
private SpriteTreeSystem _renderTree;
|
||||
|
||||
public SpriteBoundsOverlay(RenderingTreeSystem renderTree, IEyeManager eyeManager, IEntityManager entityManager)
|
||||
public SpriteBoundsOverlay(SpriteTreeSystem renderTree, IEntityManager entityManager)
|
||||
{
|
||||
_renderTree = renderTree;
|
||||
_eyeManager = eyeManager;
|
||||
_entityManager = entityManager;
|
||||
}
|
||||
|
||||
@@ -77,23 +73,18 @@ namespace Robust.Client.GameObjects
|
||||
var currentMap = args.MapId;
|
||||
var viewport = args.WorldBounds;
|
||||
|
||||
foreach (var comp in _renderTree.GetRenderTrees(currentMap, viewport))
|
||||
foreach (var (sprite, xform) in _renderTree.QueryAabb(currentMap, viewport))
|
||||
{
|
||||
var localAABB = _entityManager.GetComponent<TransformComponent>(comp.Owner).InvWorldMatrix.TransformBox(viewport);
|
||||
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
|
||||
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, args.Viewport.Eye?.Rotation ?? default);
|
||||
|
||||
foreach (var (sprite, xform) in comp.SpriteTree.QueryAabb(localAABB))
|
||||
{
|
||||
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
|
||||
var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, _eyeManager.CurrentEye.Rotation);
|
||||
// Get scaled down bounds used to indicate the "south" of a sprite.
|
||||
var localBound = bounds.Box;
|
||||
var smallLocal = localBound.Scale(0.2f).Translated(-new Vector2(0f, localBound.Extents.Y));
|
||||
var southIndicator = new Box2Rotated(smallLocal, bounds.Rotation, bounds.Origin);
|
||||
|
||||
// Get scaled down bounds used to indicate the "south" of a sprite.
|
||||
var localBound = bounds.Box;
|
||||
var smallLocal = localBound.Scale(0.2f).Translated(-new Vector2(0f, localBound.Extents.Y));
|
||||
var southIndicator = new Box2Rotated(smallLocal, bounds.Rotation, bounds.Origin);
|
||||
|
||||
handle.DrawRect(bounds, Color.Red.WithAlpha(0.2f));
|
||||
handle.DrawRect(southIndicator, Color.Blue.WithAlpha(0.5f));
|
||||
}
|
||||
handle.DrawRect(bounds, Color.Red.WithAlpha(0.2f));
|
||||
handle.DrawRect(southIndicator, Color.Blue.WithAlpha(0.5f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
@@ -21,7 +23,7 @@ using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using TerraFX.Interop.Windows;
|
||||
using static Robust.Client.ComponentTrees.SpriteTreeSystem;
|
||||
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
|
||||
using RSIDirection = Robust.Client.Graphics.RSI.State.Direction;
|
||||
|
||||
@@ -30,7 +32,7 @@ namespace Robust.Client.GameObjects
|
||||
[ComponentReference(typeof(SharedSpriteComponent))]
|
||||
[ComponentReference(typeof(ISpriteComponent))]
|
||||
public sealed class SpriteComponent : SharedSpriteComponent, ISpriteComponent,
|
||||
IComponentDebug, ISerializationHooks
|
||||
IComponentDebug, ISerializationHooks, IComponentTreeEntry<SpriteComponent>
|
||||
{
|
||||
[Dependency] private readonly IResourceCache resourceCache = default!;
|
||||
[Dependency] private readonly IPrototypeManager prototypes = default!;
|
||||
@@ -149,7 +151,13 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
internal RenderingTreeComponent? RenderTree { get; set; } = null;
|
||||
public DynamicTree<ComponentTreeEntry<SpriteComponent>>? Tree { get; set; }
|
||||
|
||||
public EntityUid? TreeUid { get; set; }
|
||||
|
||||
public bool AddToTree => Visible && !ContainerOccluded && Layers.Count > 0;
|
||||
|
||||
public bool TreeUpdateQueued { get; set; }
|
||||
|
||||
[DataField("layerDatums")]
|
||||
private List<PrototypeLayerData> LayerDatums
|
||||
@@ -253,10 +261,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
public Box2 Bounds => _bounds;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool TreeUpdateQueued { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)] private bool _inertUpdateQueued;
|
||||
[ViewVariables(VVAccess.ReadWrite)] internal bool _inertUpdateQueued;
|
||||
|
||||
/// <summary>
|
||||
/// Shader instance to use when drawing the final sprite to the world.
|
||||
@@ -1395,22 +1400,22 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
private void QueueUpdateRenderTree()
|
||||
{
|
||||
if (TreeUpdateQueued || Owner == default || entities?.EventBus == null)
|
||||
if (TreeUpdateQueued || entities?.EventBus == null)
|
||||
return;
|
||||
|
||||
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
|
||||
TreeUpdateQueued = true;
|
||||
entities.EventBus.RaiseLocalEvent(Owner, new UpdateSpriteTreeEvent());
|
||||
var ev = new QueueSpriteTreeUpdateEvent(entities.GetComponent<TransformComponent>(Owner));
|
||||
entities.EventBus.RaiseComponentEvent(this, ref ev);
|
||||
}
|
||||
|
||||
private void QueueUpdateIsInert()
|
||||
{
|
||||
if (_inertUpdateQueued || Owner == default || entities?.EventBus == null)
|
||||
if (_inertUpdateQueued || entities?.EventBus == null)
|
||||
return;
|
||||
|
||||
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
|
||||
_inertUpdateQueued = true;
|
||||
entities.EventBus?.RaiseEvent(EventSource.Local, new SpriteUpdateInertEvent {Sprite = this});
|
||||
var ev = new SpriteUpdateInertEvent();
|
||||
entities.EventBus.RaiseComponentEvent(this, ref ev);
|
||||
}
|
||||
|
||||
internal void DoUpdateIsInert()
|
||||
@@ -2214,14 +2219,9 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
// TODO whenever sprite comp gets ECS'd , just make this a direct method call.
|
||||
internal sealed class UpdateSpriteTreeEvent : EntityEventArgs
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
internal struct SpriteUpdateInertEvent
|
||||
{
|
||||
public SpriteComponent Sprite;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class RenderingTreeComponent : Component
|
||||
{
|
||||
internal DynamicTree<ComponentTreeEntry<SpriteComponent>> SpriteTree { get; set; } = default!;
|
||||
internal DynamicTree<ComponentTreeEntry<PointLightComponent>> LightTree { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +1,255 @@
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Map.Enumerators;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static Robust.Shared.GameObjects.OccluderComponent;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
namespace Robust.Client.GameObjects;
|
||||
|
||||
// NOTE: this class handles both snap grid updates of occluders, as well as occluder tree updates (via its parent).
|
||||
// This seems like it's doing somewhat double work because it already has an update queue for occluders but...
|
||||
// See the thing is the snap grid stuff was coded earlier
|
||||
// and technically it only cares about changes in the entity's SNAP GRID position.
|
||||
// Whereas the tree stuff is precise.
|
||||
// Also I just realized this and I cba to refactor this again.
|
||||
[UsedImplicitly]
|
||||
internal sealed class ClientOccluderSystem : OccluderSystem
|
||||
{
|
||||
// NOTE: this class handles both snap grid updates of occluders, as well as occluder tree updates (via its parent).
|
||||
// This seems like it's doing somewhat double work because it already has an update queue for occluders but...
|
||||
// See the thing is the snap grid stuff was coded earlier
|
||||
// and technically it only cares about changes in the entity's SNAP GRID position.
|
||||
// Whereas the tree stuff is precise.
|
||||
// Also I just realized this and I cba to refactor this again.
|
||||
[UsedImplicitly]
|
||||
internal sealed class ClientOccluderSystem : OccluderSystem
|
||||
private readonly HashSet<EntityUid> _dirtyEntities = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
base.Initialize();
|
||||
|
||||
private readonly Queue<EntityUid> _dirtyEntities = new();
|
||||
SubscribeLocalEvent<OccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<OccluderComponent, ReAnchorEvent>(OnReAnchor);
|
||||
SubscribeLocalEvent<OccluderComponent, ComponentShutdown>(OnShutdown);
|
||||
}
|
||||
|
||||
private uint _updateGeneration;
|
||||
public override void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled)
|
||||
return;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
comp.Enabled = enabled;
|
||||
Dirty(comp);
|
||||
|
||||
var xform = Transform(uid);
|
||||
QueueTreeUpdate(uid, comp, xform);
|
||||
QueueOccludedDirectionUpdate(uid, comp, xform);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, OccluderComponent comp, ComponentShutdown args)
|
||||
{
|
||||
if (!Terminating(uid))
|
||||
QueueOccludedDirectionUpdate(uid, comp);
|
||||
}
|
||||
|
||||
protected override void OnCompStartup(EntityUid uid, OccluderComponent comp, ComponentStartup args)
|
||||
{
|
||||
base.OnCompStartup(uid, comp, args);
|
||||
AnchorStateChanged(uid, comp, Transform(uid));
|
||||
}
|
||||
|
||||
public void AnchorStateChanged(EntityUid uid, OccluderComponent comp, TransformComponent xform)
|
||||
{
|
||||
QueueOccludedDirectionUpdate(uid, comp, xform);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
if (_dirtyEntities.Count == 0)
|
||||
return;
|
||||
|
||||
var query = GetEntityQuery<OccluderComponent>();
|
||||
var xforms = GetEntityQuery<TransformComponent>();
|
||||
var grids = GetEntityQuery<MapGridComponent>();
|
||||
|
||||
try
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
UpdatesAfter.Add(typeof(TransformSystem));
|
||||
UpdatesAfter.Add(typeof(PhysicsSystem));
|
||||
|
||||
SubscribeLocalEvent<OccluderDirtyEvent>(OnOccluderDirty);
|
||||
|
||||
SubscribeLocalEvent<ClientOccluderComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<ClientOccluderComponent, ReAnchorEvent>(OnReAnchor);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
if (_dirtyEntities.Count == 0)
|
||||
foreach (var entity in _dirtyEntities)
|
||||
{
|
||||
if (query.TryGetComponent(entity, out var occluder))
|
||||
UpdateOccluder(entity, occluder, query, xforms, grids);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dirtyEntities.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnchorChanged(EntityUid uid, OccluderComponent comp, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
AnchorStateChanged(uid, comp, args.Transform);
|
||||
}
|
||||
|
||||
private void OnReAnchor(EntityUid uid, OccluderComponent comp, ref ReAnchorEvent args)
|
||||
{
|
||||
AnchorStateChanged(uid, comp, args.Xform);
|
||||
}
|
||||
|
||||
private void QueueOccludedDirectionUpdate(EntityUid sender, OccluderComponent occluder, TransformComponent? xform = null)
|
||||
{
|
||||
if (!Resolve(sender, ref xform))
|
||||
return;
|
||||
|
||||
occluder.Occluding = OccluderDir.None;
|
||||
var query = GetEntityQuery<OccluderComponent>();
|
||||
Vector2i pos;
|
||||
EntityUid gridId;
|
||||
MapGridComponent? grid;
|
||||
|
||||
if (occluder.Enabled && xform.Anchored && TryComp(xform.GridUid, out grid))
|
||||
{
|
||||
pos = grid.TileIndicesFor(xform.Coordinates);
|
||||
_dirtyEntities.Add(sender);
|
||||
}
|
||||
else if (occluder.LastPosition != null)
|
||||
{
|
||||
(gridId, pos) = occluder.LastPosition.Value;
|
||||
occluder.LastPosition = null;
|
||||
if (!TryComp(gridId, out grid))
|
||||
return;
|
||||
}
|
||||
|
||||
_updateGeneration += 1;
|
||||
|
||||
while (_dirtyEntities.TryDequeue(out var entity))
|
||||
{
|
||||
if (EntityManager.EntityExists(entity)
|
||||
&& EntityManager.TryGetComponent(entity, out ClientOccluderComponent? occluder)
|
||||
&& occluder.UpdateGeneration != _updateGeneration)
|
||||
{
|
||||
occluder.Update();
|
||||
|
||||
occluder.UpdateGeneration = _updateGeneration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnAnchorChanged(EntityUid uid, ClientOccluderComponent component, ref AnchorStateChangedEvent args)
|
||||
else
|
||||
{
|
||||
component.AnchorStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
private void OnReAnchor(EntityUid uid, ClientOccluderComponent component, ref ReAnchorEvent args)
|
||||
DirtyNeighbours(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, 1)), query);
|
||||
DirtyNeighbours(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, -1)), query);
|
||||
DirtyNeighbours(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 0)), query);
|
||||
DirtyNeighbours(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 0)), query);
|
||||
}
|
||||
|
||||
private void DirtyNeighbours(AnchoredEntitiesEnumerator enumerator, EntityQuery<OccluderComponent> occluderQuery)
|
||||
{
|
||||
while (enumerator.MoveNext(out var entity))
|
||||
{
|
||||
component.AnchorStateChanged();
|
||||
}
|
||||
|
||||
private void OnOccluderDirty(OccluderDirtyEvent ev)
|
||||
{
|
||||
var sender = ev.Sender;
|
||||
MapGridComponent? grid;
|
||||
var occluderQuery = GetEntityQuery<ClientOccluderComponent>();
|
||||
|
||||
if (EntityManager.EntityExists(sender) &&
|
||||
occluderQuery.HasComponent(sender))
|
||||
if (occluderQuery.TryGetComponent(entity.Value, out var occluder))
|
||||
{
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(sender);
|
||||
if (!_mapManager.TryGetGrid(xform.GridUid, out grid))
|
||||
return;
|
||||
|
||||
var coords = xform.Coordinates;
|
||||
var localGrid = grid.TileIndicesFor(coords);
|
||||
|
||||
_dirtyEntities.Enqueue(sender);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(0, 1)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(0, -1)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(1, 0)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(localGrid + new Vector2i(-1, 0)), occluderQuery);
|
||||
}
|
||||
|
||||
// Entity is no longer valid, update around the last position it was at.
|
||||
else if (ev.LastPosition.HasValue && _mapManager.TryGetGrid(ev.LastPosition.Value.grid, out grid))
|
||||
{
|
||||
var pos = ev.LastPosition.Value.pos;
|
||||
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, 1)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(0, -1)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(1, 0)), occluderQuery);
|
||||
AddValidEntities(grid.GetAnchoredEntitiesEnumerator(pos + new Vector2i(-1, 0)), occluderQuery);
|
||||
_dirtyEntities.Add(entity.Value);
|
||||
occluder.Occluding = OccluderDir.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddValidEntities(AnchoredEntitiesEnumerator enumerator, EntityQuery<ClientOccluderComponent> occluderQuery)
|
||||
private void UpdateOccluder(EntityUid uid,
|
||||
OccluderComponent occluder,
|
||||
EntityQuery<OccluderComponent> occluders,
|
||||
EntityQuery<TransformComponent> xforms,
|
||||
EntityQuery<MapGridComponent> grids)
|
||||
{
|
||||
// Content may want to override the default behavior for occlusion.
|
||||
// Apparently OD needs this?
|
||||
{
|
||||
while (enumerator.MoveNext(out var entity))
|
||||
{
|
||||
if (!occluderQuery.HasComponent(entity.Value)) continue;
|
||||
var ev = new OccluderDirectionsEvent(uid, occluder);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
|
||||
_dirtyEntities.Enqueue(entity.Value);
|
||||
}
|
||||
if (ev.Handled)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!occluder.Enabled)
|
||||
{
|
||||
DebugTools.Assert(occluder.Occluding == OccluderDir.None);
|
||||
DebugTools.Assert(occluder.LastPosition == null);
|
||||
return;
|
||||
}
|
||||
|
||||
var xform = xforms.GetComponent(uid);
|
||||
if (!xform.Anchored || !grids.TryGetComponent(xform.GridUid, out var grid))
|
||||
{
|
||||
DebugTools.Assert(occluder.Occluding == OccluderDir.None);
|
||||
DebugTools.Assert(occluder.LastPosition == null);
|
||||
return;
|
||||
}
|
||||
|
||||
var tile = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
DebugTools.Assert(occluder.LastPosition == null
|
||||
|| occluder.LastPosition.Value.Grid == xform.GridUid && occluder.LastPosition.Value.Tile == tile);
|
||||
occluder.LastPosition = (xform.GridUid.Value, tile);
|
||||
|
||||
// dir starts at the relative effective south direction;
|
||||
var dir = xform.LocalRotation.GetCardinalDir();
|
||||
CheckDir(dir, OccluderDir.South, tile, occluder, grid, occluders, xforms);
|
||||
|
||||
dir = dir.GetClockwise90Degrees();
|
||||
CheckDir(dir, OccluderDir.West, tile, occluder, grid, occluders, xforms);
|
||||
|
||||
dir = dir.GetClockwise90Degrees();
|
||||
CheckDir(dir, OccluderDir.North, tile, occluder, grid, occluders, xforms);
|
||||
|
||||
dir = dir.GetClockwise90Degrees();
|
||||
CheckDir(dir, OccluderDir.East, tile, occluder, grid, occluders, xforms);
|
||||
}
|
||||
|
||||
private void CheckDir(
|
||||
Direction dir,
|
||||
OccluderDir occDir,
|
||||
Vector2i tile,
|
||||
OccluderComponent occluder,
|
||||
MapGridComponent grid,
|
||||
EntityQuery<OccluderComponent> query,
|
||||
EntityQuery<TransformComponent> xforms)
|
||||
{
|
||||
if ((occluder.Occluding & occDir) != 0)
|
||||
return;
|
||||
|
||||
foreach (var neighbor in grid.GetAnchoredEntities(tile.Offset(dir)))
|
||||
{
|
||||
if (!query.TryGetComponent(neighbor, out var otherOccluder) || !otherOccluder.Enabled)
|
||||
continue;
|
||||
|
||||
occluder.Occluding |= occDir;
|
||||
|
||||
// while we are here, also set the occluder flag for the other entity;
|
||||
var otherXform = xforms.GetComponent(neighbor);
|
||||
DebugTools.Assert(otherXform.Anchored);
|
||||
var rot = -otherXform.LocalRotation;
|
||||
var otherOcDir = FromDirection(rot.RotateDir(dir.GetOpposite()));
|
||||
otherOccluder.Occluding |= otherOcDir;
|
||||
}
|
||||
}
|
||||
|
||||
public static OccluderDir FromDirection(Direction dir)
|
||||
{
|
||||
return dir switch
|
||||
{
|
||||
Direction.South => OccluderDir.South,
|
||||
Direction.North => OccluderDir.North,
|
||||
Direction.East => OccluderDir.East,
|
||||
Direction.West => OccluderDir.West,
|
||||
_ => throw new ArgumentException($"Invalid dir: {dir}.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised by a <see cref="ClientOccluderComponent"/> when it needs to be recalculated.
|
||||
/// Raised by occluders when trying to get occlusion directions.
|
||||
/// </summary>
|
||||
internal sealed class OccluderDirtyEvent : EntityEventArgs
|
||||
[ByRefEvent]
|
||||
public struct OccluderDirectionsEvent
|
||||
{
|
||||
public OccluderDirtyEvent(EntityUid sender, (EntityUid grid, Vector2i pos)? lastPosition)
|
||||
{
|
||||
LastPosition = lastPosition;
|
||||
Sender = sender;
|
||||
}
|
||||
public bool Handled = false;
|
||||
public readonly EntityUid Sender = default!;
|
||||
public readonly OccluderComponent Occluder = default!;
|
||||
|
||||
public (EntityUid grid, Vector2i pos)? LastPosition { get; }
|
||||
public EntityUid Sender { get; }
|
||||
public OccluderDirectionsEvent(EntityUid sender, OccluderComponent occluder)
|
||||
{
|
||||
Sender = sender;
|
||||
Occluder = occluder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Robust.Client.GameObjects
|
||||
[Dependency] private readonly INetManager _netMan = default!;
|
||||
[Dependency] private readonly IRobustSerializer _serializer = default!;
|
||||
[Dependency] private readonly IDynamicTypeFactoryInternal _dynFactory = default!;
|
||||
[Dependency] private readonly PointLightSystem _lightSys = default!;
|
||||
|
||||
private readonly HashSet<EntityUid> _updateQueue = new();
|
||||
|
||||
@@ -289,9 +290,7 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
|
||||
if (pointQuery.TryGetComponent(entity, out var light))
|
||||
{
|
||||
light.ContainerOccluded = lightOccluded;
|
||||
}
|
||||
_lightSys.SetContainerOccluded(entity, lightOccluded, light);
|
||||
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#if DEBUG
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -28,7 +29,7 @@ namespace Robust.Client.GameObjects
|
||||
EntitySystem.Get<EntityLookupSystem>(),
|
||||
IoCManager.Resolve<IEyeManager>(),
|
||||
IoCManager.Resolve<IMapManager>(),
|
||||
Get<RenderingTreeSystem>());
|
||||
Get<LightTreeSystem>());
|
||||
|
||||
overlayManager.AddOverlay(_lightOverlay);
|
||||
}
|
||||
@@ -48,16 +49,16 @@ namespace Robust.Client.GameObjects
|
||||
private IEyeManager _eyeManager;
|
||||
private IMapManager _mapManager;
|
||||
|
||||
private RenderingTreeSystem _tree;
|
||||
private LightTreeSystem _trees;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public DebugLightOverlay(EntityLookupSystem lookup, IEyeManager eyeManager, IMapManager mapManager, RenderingTreeSystem tree)
|
||||
public DebugLightOverlay(EntityLookupSystem lookup, IEyeManager eyeManager, IMapManager mapManager, LightTreeSystem trees)
|
||||
{
|
||||
_lookup = lookup;
|
||||
_eyeManager = eyeManager;
|
||||
_mapManager = mapManager;
|
||||
_tree = tree;
|
||||
_trees = trees;
|
||||
}
|
||||
|
||||
protected internal override void Draw(in OverlayDrawArgs args)
|
||||
@@ -65,9 +66,9 @@ namespace Robust.Client.GameObjects
|
||||
var map = _eyeManager.CurrentMap;
|
||||
if (map == MapId.Nullspace) return;
|
||||
|
||||
foreach (var tree in _tree.GetRenderTrees(map, args.WorldBounds))
|
||||
foreach (var treeComp in _trees.GetIntersectingTrees(map, args.WorldBounds))
|
||||
{
|
||||
foreach (var (light, xform) in tree.LightTree)
|
||||
foreach (var (light, xform) in treeComp.Tree)
|
||||
{
|
||||
var aabb = _lookup.GetWorldAABB(light.Owner, xform);
|
||||
if (!aabb.Intersects(args.WorldAABB)) continue;
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
public sealed class PointLightSystem : SharedPointLightSystem
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly RenderingTreeSystem _renderingTreeSystem = default!;
|
||||
[Dependency] private readonly LightTreeSystem _lightTree = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentInit>(HandleInit);
|
||||
SubscribeLocalEvent<PointLightComponent, ComponentRemove>(HandleRemove);
|
||||
}
|
||||
|
||||
private void HandleInit(EntityUid uid, PointLightComponent component, ComponentInit args)
|
||||
{
|
||||
UpdateMask(component);
|
||||
RaiseLocalEvent(uid, new PointLightUpdateEvent(), true);
|
||||
}
|
||||
|
||||
private void HandleRemove(EntityUid uid, PointLightComponent component, ComponentRemove args)
|
||||
{
|
||||
if (Transform(uid).MapID != MapId.Nullspace)
|
||||
{
|
||||
_renderingTreeSystem.ClearLight(component);
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateMask(PointLightComponent component)
|
||||
@@ -38,5 +29,46 @@ namespace Robust.Client.GameObjects
|
||||
else
|
||||
component.Mask = null;
|
||||
}
|
||||
|
||||
#region Setters
|
||||
public void SetContainerOccluded(EntityUid uid, bool occluded, PointLightComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp) || occluded == comp.ContainerOccluded)
|
||||
return;
|
||||
|
||||
comp.ContainerOccluded = occluded;
|
||||
Dirty(comp);
|
||||
|
||||
if (comp.Enabled)
|
||||
_lightTree.QueueTreeUpdate(uid, comp);
|
||||
}
|
||||
|
||||
public override void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp) || enabled == comp.Enabled)
|
||||
return;
|
||||
|
||||
comp._enabled = enabled;
|
||||
RaiseLocalEvent(uid, new PointLightToggleEvent(comp.Enabled));
|
||||
Dirty(comp);
|
||||
|
||||
var cast = (PointLightComponent)comp;
|
||||
if (!cast.ContainerOccluded)
|
||||
_lightTree.QueueTreeUpdate(uid, cast);
|
||||
}
|
||||
|
||||
public override void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp) || MathHelper.CloseToPercent(radius, comp.Radius))
|
||||
return;
|
||||
|
||||
comp._radius = radius;
|
||||
Dirty(comp);
|
||||
|
||||
var cast = (PointLightComponent)comp;
|
||||
if (cast.TreeUid != null)
|
||||
_lightTree.QueueTreeUpdate(uid, cast);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.GameObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps track of <see cref="DynamicTree{T}"/>s for various rendering-related components.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class RenderingTreeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly TransformSystem _xformSystem = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
internal const string LoggerSawmill = "rendertree";
|
||||
|
||||
// Nullspace is not indexed. Keep that in mind.
|
||||
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
private readonly List<SpriteComponent> _spriteQueue = new();
|
||||
private readonly List<PointLightComponent> _lightQueue = new();
|
||||
|
||||
private readonly HashSet<EntityUid> _checkedChildren = new();
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="CVars.MaxLightRadius"/>
|
||||
/// </summary>
|
||||
public float MaxLightRadius { get; private set; }
|
||||
|
||||
internal IEnumerable<RenderingTreeComponent> GetRenderTrees(MapId mapId, Box2Rotated worldBounds)
|
||||
{
|
||||
if (mapId == MapId.Nullspace) yield break;
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
|
||||
{
|
||||
var tempQualifier = grid.Owner;
|
||||
yield return EntityManager.GetComponent<RenderingTreeComponent>(tempQualifier);
|
||||
}
|
||||
|
||||
var tempQualifier1 = _mapManager.GetMapEntityId(mapId);
|
||||
yield return EntityManager.GetComponent<RenderingTreeComponent>(tempQualifier1);
|
||||
}
|
||||
|
||||
internal IEnumerable<RenderingTreeComponent> GetRenderTrees(MapId mapId, Box2 worldAABB)
|
||||
{
|
||||
if (mapId == MapId.Nullspace) yield break;
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
|
||||
{
|
||||
var tempQualifier = grid.Owner;
|
||||
yield return EntityManager.GetComponent<RenderingTreeComponent>(tempQualifier);
|
||||
}
|
||||
|
||||
var tempQualifier1 = _mapManager.GetMapEntityId(mapId);
|
||||
yield return EntityManager.GetComponent<RenderingTreeComponent>(tempQualifier1);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
UpdatesBefore.Add(typeof(SpriteSystem));
|
||||
UpdatesAfter.Add(typeof(TransformSystem));
|
||||
UpdatesAfter.Add(typeof(PhysicsSystem));
|
||||
|
||||
SubscribeLocalEvent<MapChangedEvent>(MapManagerOnMapCreated);
|
||||
|
||||
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
|
||||
|
||||
// Due to how recursion works, this must be done.
|
||||
// Note that this also implicitly handles parent changes.
|
||||
SubscribeLocalEvent<MoveEvent>(AnythingMoved);
|
||||
|
||||
SubscribeLocalEvent<SpriteComponent, ComponentRemove>(RemoveSprite);
|
||||
SubscribeLocalEvent<SpriteComponent, UpdateSpriteTreeEvent>(HandleSpriteUpdate);
|
||||
|
||||
SubscribeLocalEvent<PointLightComponent, PointLightRadiusChangedEvent>(PointLightRadiusChanged);
|
||||
SubscribeLocalEvent<PointLightComponent, PointLightUpdateEvent>(HandleLightUpdate);
|
||||
|
||||
SubscribeLocalEvent<RenderingTreeComponent, ComponentInit>(OnTreeInit);
|
||||
SubscribeLocalEvent<RenderingTreeComponent, ComponentRemove>(OnTreeRemove);
|
||||
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
configManager.OnValueChanged(CVars.MaxLightRadius, value => MaxLightRadius = value, true);
|
||||
}
|
||||
|
||||
private void OnTreeInit(EntityUid uid, RenderingTreeComponent component, ComponentInit args)
|
||||
{
|
||||
component.LightTree = new(LightAabbFunc);
|
||||
component.SpriteTree = new(SpriteAabbFunc);
|
||||
}
|
||||
|
||||
private void HandleLightUpdate(EntityUid uid, PointLightComponent component, PointLightUpdateEvent args)
|
||||
{
|
||||
if (component.TreeUpdateQueued) return;
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
private void HandleSpriteUpdate(EntityUid uid, SpriteComponent component, UpdateSpriteTreeEvent args)
|
||||
{
|
||||
_spriteQueue.Add(component);
|
||||
}
|
||||
|
||||
private void AnythingMoved(ref MoveEvent args)
|
||||
{
|
||||
var pointQuery = EntityManager.GetEntityQuery<PointLightComponent>();
|
||||
var spriteQuery = EntityManager.GetEntityQuery<SpriteComponent>();
|
||||
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
|
||||
var renderingQuery = EntityManager.GetEntityQuery<RenderingTreeComponent>();
|
||||
|
||||
AnythingMovedSubHandler(args.Sender, args.Component, xformQuery, pointQuery, spriteQuery, renderingQuery);
|
||||
}
|
||||
|
||||
private void AnythingMovedSubHandler(
|
||||
EntityUid uid,
|
||||
TransformComponent xform,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
EntityQuery<PointLightComponent> pointQuery,
|
||||
EntityQuery<SpriteComponent> spriteQuery,
|
||||
EntityQuery<RenderingTreeComponent> renderingQuery)
|
||||
{
|
||||
DebugTools.Assert(xform.Owner == uid);
|
||||
|
||||
// To avoid doing redundant updates (and we don't need to update a grid's children ever)
|
||||
if (!_checkedChildren.Add(uid) || renderingQuery.HasComponent(uid)) return;
|
||||
|
||||
// This recursive search is needed, as MoveEvent is defined to not care about indirect events like children.
|
||||
// WHATEVER YOU DO, DON'T REPLACE THIS WITH SPAMMING EVENTS UNLESS YOU HAVE A GUARANTEE IT WON'T LAG THE GC.
|
||||
// (Struct-based events ok though)
|
||||
// Ironically this was lagging the GC lolz
|
||||
if (spriteQuery.TryGetComponent(uid, out var sprite))
|
||||
QueueSpriteUpdate(sprite);
|
||||
|
||||
if (pointQuery.TryGetComponent(uid, out var light))
|
||||
QueueLightUpdate(light);
|
||||
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
|
||||
while (childEnumerator.MoveNext(out var child))
|
||||
{
|
||||
if (xformQuery.TryGetComponent(child.Value, out var childXform))
|
||||
AnythingMovedSubHandler(child.Value, childXform, xformQuery, pointQuery, spriteQuery, renderingQuery);
|
||||
}
|
||||
}
|
||||
|
||||
// For the RemoveX methods
|
||||
// If the Transform is removed BEFORE the Sprite/Light,
|
||||
// then the MapIdChanged code will handle and remove it (because MapId gets set to nullspace).
|
||||
// Otherwise these will still have their past MapId and that's all we need..
|
||||
|
||||
#region SpriteHandlers
|
||||
|
||||
private void RemoveSprite(EntityUid uid, SpriteComponent component, ComponentRemove args)
|
||||
{
|
||||
ClearSprite(component);
|
||||
}
|
||||
|
||||
private void ClearSprite(SpriteComponent component)
|
||||
{
|
||||
if (component.RenderTree == null) return;
|
||||
|
||||
component.RenderTree.SpriteTree.Remove(new() { Component = component });
|
||||
component.RenderTree = null;
|
||||
}
|
||||
|
||||
private void QueueSpriteUpdate(SpriteComponent component)
|
||||
{
|
||||
if (component.TreeUpdateQueued) return;
|
||||
|
||||
component.TreeUpdateQueued = true;
|
||||
_spriteQueue.Add(component);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region LightHandlers
|
||||
|
||||
private void PointLightRadiusChanged(EntityUid uid, PointLightComponent component, PointLightRadiusChangedEvent args)
|
||||
{
|
||||
QueueLightUpdate(component);
|
||||
}
|
||||
|
||||
public void ClearLight(PointLightComponent component)
|
||||
{
|
||||
if (component.RenderTree == null) return;
|
||||
|
||||
component.RenderTree.LightTree.Remove(new() { Component = component });
|
||||
component.RenderTree = null;
|
||||
}
|
||||
|
||||
private void QueueLightUpdate(PointLightComponent component)
|
||||
{
|
||||
if (component.TreeUpdateQueued) return;
|
||||
|
||||
component.TreeUpdateQueued = true;
|
||||
_lightQueue.Add(component);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void OnTreeRemove(EntityUid uid, RenderingTreeComponent component, ComponentRemove args)
|
||||
{
|
||||
foreach (var sprite in component.SpriteTree)
|
||||
{
|
||||
sprite.Component.RenderTree = null;
|
||||
}
|
||||
|
||||
foreach (var light in component.LightTree)
|
||||
{
|
||||
light.Component.RenderTree = null;
|
||||
}
|
||||
|
||||
component.SpriteTree.Clear();
|
||||
component.LightTree.Clear();
|
||||
}
|
||||
|
||||
private void MapManagerOnMapCreated(MapChangedEvent e)
|
||||
{
|
||||
if (e.Destroyed || e.Map == MapId.Nullspace)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EntityManager.EnsureComponent<RenderingTreeComponent>(e.Uid);
|
||||
}
|
||||
|
||||
private void MapManagerOnGridCreated(GridInitializeEvent ev)
|
||||
{
|
||||
EntityManager.EnsureComponent<RenderingTreeComponent>(_mapManager.GetGrid(ev.EntityUid).Owner);
|
||||
}
|
||||
|
||||
private RenderingTreeComponent? GetRenderTree(EntityUid entity, TransformComponent xform, EntityQuery<TransformComponent> xforms)
|
||||
{
|
||||
var lookups = EntityManager.GetEntityQuery<RenderingTreeComponent>();
|
||||
|
||||
if (!EntityManager.EntityExists(entity) ||
|
||||
xform.MapID == MapId.Nullspace ||
|
||||
lookups.HasComponent(entity)) return null;
|
||||
|
||||
var parent = xform.ParentUid;
|
||||
|
||||
while (parent.IsValid())
|
||||
{
|
||||
if (lookups.TryGetComponent(parent, out var comp)) return comp;
|
||||
parent = xforms.GetComponent(parent).ParentUid;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsVisible(SpriteComponent component)
|
||||
{
|
||||
return component.Visible && !component.ContainerOccluded && !component.Deleted;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
_checkedChildren.Clear();
|
||||
|
||||
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var sprite in _spriteQueue)
|
||||
{
|
||||
sprite.TreeUpdateQueued = false;
|
||||
if (!IsVisible(sprite))
|
||||
{
|
||||
ClearSprite(sprite);
|
||||
continue;
|
||||
}
|
||||
|
||||
var xform = xforms.GetComponent(sprite.Owner);
|
||||
var oldMapTree = sprite.RenderTree;
|
||||
var newMapTree = GetRenderTree(sprite.Owner, xform, xforms);
|
||||
// TODO: Temp PVS guard
|
||||
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform, xforms);
|
||||
|
||||
if (float.IsNaN(worldPos.X) || float.IsNaN(worldPos.Y))
|
||||
{
|
||||
ClearSprite(sprite);
|
||||
continue;
|
||||
}
|
||||
|
||||
var aabb = SpriteAabbFunc(sprite, xform, worldPos, worldRot, xforms);
|
||||
|
||||
// If we're on a new map then clear the old one.
|
||||
if (oldMapTree != newMapTree)
|
||||
{
|
||||
ClearSprite(sprite);
|
||||
newMapTree?.SpriteTree.Add((sprite,xform) , aabb);
|
||||
}
|
||||
else
|
||||
{
|
||||
newMapTree?.SpriteTree.Update((sprite, xform), aabb);
|
||||
}
|
||||
|
||||
sprite.RenderTree = newMapTree;
|
||||
}
|
||||
|
||||
foreach (var light in _lightQueue)
|
||||
{
|
||||
light.TreeUpdateQueued = false;
|
||||
|
||||
if (light.Deleted || !light.Enabled || light.ContainerOccluded)
|
||||
{
|
||||
ClearLight(light);
|
||||
continue;
|
||||
}
|
||||
|
||||
var xform = xforms.GetComponent(light.Owner);
|
||||
var oldMapTree = light.RenderTree;
|
||||
var newMapTree = GetRenderTree(light.Owner, xform, xforms);
|
||||
// TODO: Temp PVS guard
|
||||
var worldPos = _xformSystem.GetWorldPosition(xform, xforms);
|
||||
|
||||
if (float.IsNaN(worldPos.X) || float.IsNaN(worldPos.Y))
|
||||
{
|
||||
ClearLight(light);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Events need a bit of cleanup so we only validate this on initialize and radius changed events
|
||||
// this is fine for now IMO as it's 1 float check for every light that moves
|
||||
if (light.Radius > MaxLightRadius)
|
||||
{
|
||||
Logger.WarningS(LoggerSawmill, $"Light radius for {light.Owner} set above max radius of {MaxLightRadius}. This may lead to pop-in.");
|
||||
}
|
||||
var aabb = LightAabbFunc(light, xform, worldPos, xforms);
|
||||
|
||||
// If we're on a new map then clear the old one.
|
||||
if (oldMapTree != newMapTree)
|
||||
{
|
||||
ClearLight(light);
|
||||
newMapTree?.LightTree.Add((light, xform), aabb);
|
||||
}
|
||||
else
|
||||
{
|
||||
newMapTree?.LightTree.Update((light, xform), aabb);
|
||||
}
|
||||
|
||||
light.RenderTree = newMapTree;
|
||||
}
|
||||
|
||||
_spriteQueue.Clear();
|
||||
_lightQueue.Clear();
|
||||
}
|
||||
|
||||
private Box2 SpriteAabbFunc(in ComponentTreeEntry<SpriteComponent> entry)
|
||||
{
|
||||
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(entry.Transform, xforms);
|
||||
|
||||
return SpriteAabbFunc(entry.Component, entry.Transform, worldPos, worldRot, xforms);
|
||||
}
|
||||
|
||||
private Box2 LightAabbFunc(in ComponentTreeEntry<PointLightComponent> entry)
|
||||
{
|
||||
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
|
||||
var worldPos = _xformSystem.GetWorldPosition(entry.Transform, xforms);
|
||||
var tree = GetRenderTree(entry.Uid, entry.Transform, xforms);
|
||||
var boxSize = entry.Component.Radius * 2;
|
||||
|
||||
var localPos = tree == null ? worldPos : _xformSystem.GetInvWorldMatrix(tree.Owner, xforms).Transform(worldPos);
|
||||
return Box2.CenteredAround(localPos, (boxSize, boxSize));
|
||||
}
|
||||
|
||||
private Box2 SpriteAabbFunc(SpriteComponent value, TransformComponent xform, Vector2 worldPos, Angle worldRot, EntityQuery<TransformComponent> xforms)
|
||||
{
|
||||
var bounds = value.CalculateRotatedBoundingBox(worldPos, worldRot, _eyeManager.CurrentEye.Rotation);
|
||||
var tree = GetRenderTree(value.Owner, xform, xforms);
|
||||
|
||||
return tree == null ? bounds.CalcBoundingBox() : _xformSystem.GetInvWorldMatrix(tree.Owner, xforms).TransformBox(bounds);
|
||||
}
|
||||
|
||||
private Box2 LightAabbFunc(PointLightComponent value, TransformComponent xform, Vector2 worldPos, EntityQuery<TransformComponent> xforms)
|
||||
{
|
||||
// Lights are circles so don't need entity's rotation
|
||||
var tree = GetRenderTree(value.Owner, xform, xforms);
|
||||
var boxSize = value.Radius * 2;
|
||||
|
||||
var localPos = tree == null ? worldPos : xforms.GetComponent(tree.Owner).InvWorldMatrix.Transform(worldPos);
|
||||
return Box2.CenteredAround(localPos, (boxSize, boxSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -19,7 +20,7 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly RenderingTreeSystem _treeSystem = default!;
|
||||
[Dependency] private readonly SpriteTreeSystem _treeSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
|
||||
private readonly Queue<SpriteComponent> _inertUpdateQueue = new();
|
||||
@@ -29,8 +30,10 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
UpdatesAfter.Add(typeof(SpriteTreeSystem));
|
||||
|
||||
_proto.PrototypesReloaded += OnPrototypesReloaded;
|
||||
SubscribeLocalEvent<SpriteUpdateInertEvent>(QueueUpdateInert);
|
||||
SubscribeLocalEvent<SpriteComponent, SpriteUpdateInertEvent>(QueueUpdateInert);
|
||||
_cfg.OnValueChanged(CVars.RenderSpriteDirectionBias, OnBiasChanged, true);
|
||||
}
|
||||
|
||||
@@ -46,9 +49,13 @@ namespace Robust.Client.GameObjects
|
||||
SpriteComponent.DirectionBias = value;
|
||||
}
|
||||
|
||||
private void QueueUpdateInert(SpriteUpdateInertEvent ev)
|
||||
private void QueueUpdateInert(EntityUid uid, SpriteComponent sprite, ref SpriteUpdateInertEvent ev)
|
||||
{
|
||||
_inertUpdateQueue.Enqueue(ev.Sprite);
|
||||
if (sprite._inertUpdateQueued)
|
||||
return;
|
||||
|
||||
sprite._inertUpdateQueued = true;
|
||||
_inertUpdateQueue.Enqueue(sprite);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -76,13 +83,7 @@ namespace Robust.Client.GameObjects
|
||||
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
|
||||
var spriteState = (frameTime, _manualUpdate);
|
||||
|
||||
foreach (var comp in _treeSystem.GetRenderTrees(currentMap, pvsBounds))
|
||||
{
|
||||
var invMatrix = _transform.GetInvWorldMatrix(comp.Owner, xforms);
|
||||
var bounds = invMatrix.TransformBox(pvsBounds);
|
||||
|
||||
comp.SpriteTree.QueryAabb(ref spriteState, static (ref (
|
||||
float frameTime,
|
||||
_treeSystem.QueryAabb( ref spriteState, static (ref (float frameTime,
|
||||
HashSet<ISpriteComponent> _manualUpdate) tuple, in ComponentTreeEntry<SpriteComponent> value) =>
|
||||
{
|
||||
if (value.Component.IsInert)
|
||||
@@ -92,8 +93,7 @@ namespace Robust.Client.GameObjects
|
||||
value.Component.FrameUpdate(tuple.frameTime);
|
||||
|
||||
return true;
|
||||
}, bounds, true);
|
||||
}
|
||||
}, currentMap, pvsBounds, true);
|
||||
|
||||
_manualUpdate.Clear();
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
using static Robust.Client.GameObjects.ClientOccluderComponent;
|
||||
using OGLTextureWrapMode = OpenToolkit.Graphics.OpenGL.TextureWrapMode;
|
||||
using TKStencilOp = OpenToolkit.Graphics.OpenGL4.StencilOp;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Client.ComponentTrees;
|
||||
using static Robust.Shared.GameObjects.OccluderComponent;
|
||||
|
||||
namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
@@ -538,19 +539,18 @@ namespace Robust.Client.Graphics.Clyde
|
||||
expandedBounds)
|
||||
GetLightsToRender(MapId map, in Box2Rotated worldBounds, in Box2 worldAABB)
|
||||
{
|
||||
var renderingTreeSystem = _entitySystemManager.GetEntitySystem<RenderingTreeSystem>();
|
||||
var lightTreeSys = _entitySystemManager.GetEntitySystem<LightTreeSystem>();
|
||||
var xformSystem = _entitySystemManager.GetEntitySystem<TransformSystem>();
|
||||
var enlargedBounds = worldAABB.Enlarged(renderingTreeSystem.MaxLightRadius);
|
||||
|
||||
// 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, xformSystem, xforms, worldAABB);
|
||||
|
||||
foreach (var comp in renderingTreeSystem.GetRenderTrees(map, enlargedBounds))
|
||||
foreach (var comp in lightTreeSys.GetIntersectingTrees(map, worldAABB))
|
||||
{
|
||||
var bounds = xformSystem.GetInvWorldMatrix(comp.Owner, xforms).TransformBox(worldBounds);
|
||||
|
||||
comp.LightTree.QueryAabb(ref state, static (ref (
|
||||
comp.Tree.QueryAabb(ref state, static (ref (
|
||||
Clyde clyde,
|
||||
int count,
|
||||
int shadowCastingCount,
|
||||
@@ -933,7 +933,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
var xforms = _entityManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var comp in occluderSystem.GetOccluderTrees(map, expandedBounds))
|
||||
foreach (var comp in occluderSystem.GetIntersectingTrees(map, expandedBounds))
|
||||
{
|
||||
var treeBounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(expandedBounds);
|
||||
|
||||
@@ -945,7 +945,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return true;
|
||||
}
|
||||
|
||||
var occluder = (ClientOccluderComponent)sOccluder;
|
||||
var occluder = (OccluderComponent)sOccluder;
|
||||
|
||||
var worldTransform = xformSystem.GetWorldMatrix(transform, xforms);
|
||||
var box = sOccluder.BoundingBox;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
@@ -38,7 +39,6 @@ internal partial class Clyde
|
||||
Array.Sort(indexList, 0, _drawingSpriteList.Count, new SpriteDrawingOrderComparer(_drawingSpriteList));
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void ProcessSpriteEntities(MapId map, Viewport view, IEye eye, Box2Rotated worldBounds, RefList<SpriteData> list)
|
||||
{
|
||||
@@ -59,7 +59,7 @@ internal partial class Clyde
|
||||
var index = 0;
|
||||
var added = 0;
|
||||
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parMan.ParallelProcessCount };
|
||||
foreach (var comp in _entitySystemManager.GetEntitySystem<RenderingTreeSystem>().GetRenderTrees(map, worldBounds))
|
||||
foreach (var comp in _entitySystemManager.GetEntitySystem<SpriteTreeSystem>().GetIntersectingTrees(map, worldBounds))
|
||||
{
|
||||
var treeOwner = comp.Owner;
|
||||
var treeXform = query.GetComponent(comp.Owner);
|
||||
@@ -75,7 +75,7 @@ internal partial class Clyde
|
||||
Cos = MathF.Cos((float)treeXform.LocalRotation),
|
||||
};
|
||||
|
||||
comp.SpriteTree.QueryAabb(ref list,
|
||||
comp.Tree.QueryAabb(ref list,
|
||||
static (ref RefList<SpriteData> state, in ComponentTreeEntry<SpriteComponent> value) =>
|
||||
{
|
||||
ref var entry = ref state.AllocAdd();
|
||||
@@ -125,7 +125,7 @@ internal partial class Clyde
|
||||
// var spriteWorldBB = data.Sprite.CalculateRotatedBoundingBox(data.WorldPos, data.WorldRot, batch.ViewRotation);
|
||||
// data.SpriteScreenBB = Viewport.GetWorldToLocalMatrix().TransformBox(spriteWorldBB);
|
||||
|
||||
var (pos, rot) = batch.Sys.GetParentRelativePositionRotation(data.Xform, batch.TreeOwner, batch.Query);
|
||||
var (pos, rot) = batch.Sys.GetRelativePositionRotation(data.Xform, batch.TreeOwner, batch.Query);
|
||||
pos = new Vector2(
|
||||
batch.TreePos.X + batch.Cos * pos.X - batch.Sin * pos.Y,
|
||||
batch.TreePos.Y + batch.Sin * pos.X + batch.Cos * pos.Y);
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Server.GameObjects
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class ServerOccluderSystem : OccluderSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
namespace Robust.Server.GameObjects;
|
||||
|
||||
UpdatesAfter.Add(typeof(PhysicsSystem));
|
||||
}
|
||||
}
|
||||
[UsedImplicitly]
|
||||
public sealed class ServerOccluderSystem : OccluderSystem
|
||||
{
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ namespace Robust.Server.GameObjects
|
||||
RegisterClass<PhysicsComponent>();
|
||||
RegisterClass<CollisionWakeComponent>();
|
||||
RegisterClass<ContainerManagerComponent>();
|
||||
RegisterClass<OccluderComponent>();
|
||||
RegisterClass<OccluderTreeComponent>();
|
||||
RegisterClass<SpriteComponent>();
|
||||
RegisterClass<ServerUserInterfaceComponent>();
|
||||
RegisterClass<TimerComponent>();
|
||||
|
||||
@@ -74,12 +74,21 @@ namespace Robust.Shared.Maths
|
||||
{
|
||||
var ang = Theta % (2 * Math.PI);
|
||||
|
||||
if (ang < 0.0f) // convert -PI > PI to 0 > 2PI
|
||||
ang += 2 * (float) Math.PI;
|
||||
if (ang < 0) // convert -PI > PI to 0 > 2PI
|
||||
ang += 2 * Math.PI;
|
||||
|
||||
return (Direction) (Math.Floor((ang + Offset) / Segment) % 8);
|
||||
}
|
||||
|
||||
public Direction RotateDir(Direction dir)
|
||||
{
|
||||
var ang = (Theta + Segment * (int)dir) % (2 * Math.PI);
|
||||
if (ang < 0)
|
||||
ang += 2 * Math.PI;
|
||||
|
||||
return (Direction)(Math.Floor((ang + Offset) / Segment) % 8);
|
||||
}
|
||||
|
||||
private const double CardinalSegment = 2 * Math.PI / 4.0; // Cut the circle into 4 pieces
|
||||
private const double CardinalOffset = CardinalSegment / 2.0; // offset the pieces by 1/2 their size
|
||||
|
||||
|
||||
351
Robust.Shared/ComponentTrees/ComponentTreeSystem.cs
Normal file
351
Robust.Shared/ComponentTrees/ComponentTreeSystem.cs
Normal file
@@ -0,0 +1,351 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Robust.Shared.ComponentTrees;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps track of <see cref="DynamicTree{T}"/>s for various rendering-related components.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
|
||||
where TTreeComp : Component, IComponentTreeComponent<TComp>, new()
|
||||
where TComp : Component, IComponentTreeEntry<TComp>, new()
|
||||
{
|
||||
[Dependency] private readonly RecursiveMoveSystem _recursiveMoveSys = default!;
|
||||
[Dependency] protected readonly SharedTransformSystem XformSystem = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
private readonly Queue<ComponentTreeEntry<TComp>> _updateQueue = new();
|
||||
private readonly HashSet<EntityUid> _updated = new();
|
||||
|
||||
/// <summary>
|
||||
/// If true, this system will update the tree positions every frame update. See also <see cref="DoTickUpdate"/>. Some systems may need to do both.
|
||||
/// </summary>
|
||||
protected abstract bool DoFrameUpdate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, this system will update the tree positions every tick update. See also <see cref="DoFrameUpdate"/>. Some systems may need to do both.
|
||||
/// </summary>
|
||||
protected abstract bool DoTickUpdate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initial tree capacity. Note that client-side trees will remove entities as they leave PVS range.
|
||||
/// </summary>
|
||||
protected virtual int InitialCapacity { get; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// If true, this tree requires all children to be recursively updated whenever ANY entity moves. If false, this
|
||||
/// will only update when an entity with the given component moves.
|
||||
/// </summary>
|
||||
protected abstract bool Recursive { get; }
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
UpdatesOutsidePrediction = DoTickUpdate;
|
||||
UpdatesAfter.Add(typeof(SharedTransformSystem));
|
||||
UpdatesAfter.Add(typeof(SharedPhysicsSystem));
|
||||
|
||||
SubscribeLocalEvent<MapChangedEvent>(MapManagerOnMapCreated);
|
||||
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
|
||||
|
||||
SubscribeLocalEvent<TComp, ComponentStartup>(OnCompStartup);
|
||||
SubscribeLocalEvent<TComp, ComponentRemove>(OnCompRemoved);
|
||||
|
||||
if (Recursive)
|
||||
{
|
||||
SubscribeLocalEvent<TComp, TreeRecursiveMoveEvent>(HandleRecursiveMove);
|
||||
_recursiveMoveSys.AddSubscription();
|
||||
}
|
||||
else
|
||||
{
|
||||
SubscribeLocalEvent<TComp, MoveEvent>(HandleMove);
|
||||
}
|
||||
|
||||
SubscribeLocalEvent<TTreeComp, EntityTerminatingEvent>(OnTerminating);
|
||||
SubscribeLocalEvent<TTreeComp, ComponentAdd>(OnTreeAdd);
|
||||
SubscribeLocalEvent<TTreeComp, ComponentRemove>(OnTreeRemove);
|
||||
}
|
||||
|
||||
#region Queue Update
|
||||
private void HandleRecursiveMove(EntityUid uid, TComp component, ref TreeRecursiveMoveEvent args)
|
||||
=> QueueTreeUpdate(uid, component, args.Xform);
|
||||
|
||||
private void HandleMove(EntityUid uid, TComp component, ref MoveEvent args)
|
||||
=> QueueTreeUpdate(uid, component, args.Component);
|
||||
|
||||
public void QueueTreeUpdate(EntityUid uid, TComp component, TransformComponent? xform = null)
|
||||
{
|
||||
if (component.TreeUpdateQueued || !Resolve(uid, ref xform))
|
||||
return;
|
||||
|
||||
component.TreeUpdateQueued = true;
|
||||
_updateQueue.Enqueue((component, xform));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Component Management
|
||||
protected virtual void OnCompStartup(EntityUid uid, TComp component, ComponentStartup args)
|
||||
=> QueueTreeUpdate(uid, component);
|
||||
|
||||
protected virtual void OnCompRemoved(EntityUid uid, TComp component, ComponentRemove args)
|
||||
=> RemoveFromTree(component);
|
||||
|
||||
protected virtual void OnTreeAdd(EntityUid uid, TTreeComp component, ComponentAdd args)
|
||||
{
|
||||
component.Tree = new(ExtractAabb, capacity: InitialCapacity);
|
||||
}
|
||||
|
||||
protected virtual void OnTreeRemove(EntityUid uid, TTreeComp component, ComponentRemove args)
|
||||
{
|
||||
if (Terminating(uid))
|
||||
return;
|
||||
|
||||
foreach (var entry in component.Tree)
|
||||
{
|
||||
entry.Component.TreeUid = null;
|
||||
}
|
||||
|
||||
component.Tree.Clear();
|
||||
}
|
||||
|
||||
protected virtual void OnTerminating(EntityUid uid, TTreeComp component, ref EntityTerminatingEvent args)
|
||||
{
|
||||
RemComp(uid, component);
|
||||
}
|
||||
|
||||
private void MapManagerOnMapCreated(MapChangedEvent e)
|
||||
{
|
||||
if (e.Destroyed || e.Map == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
EnsureComp<TTreeComp>(e.Uid);
|
||||
}
|
||||
|
||||
private void MapManagerOnGridCreated(GridInitializeEvent ev)
|
||||
{
|
||||
EnsureComp<TTreeComp>(ev.EntityUid);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Update Trees
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
if (DoTickUpdate)
|
||||
UpdateTreePositions();
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
if (DoFrameUpdate)
|
||||
UpdateTreePositions();
|
||||
}
|
||||
|
||||
private void UpdateTreePositions()
|
||||
{
|
||||
var xforms = GetEntityQuery<TransformComponent>();
|
||||
var trees = GetEntityQuery<TTreeComp>();
|
||||
|
||||
while (_updateQueue.TryDequeue(out var entry))
|
||||
{
|
||||
var (comp, xform) = entry;
|
||||
|
||||
comp.TreeUpdateQueued = false;
|
||||
if (!_updated.Add(comp.Owner))
|
||||
continue;
|
||||
|
||||
if (!comp.AddToTree || comp.Deleted || xform.MapUid == null)
|
||||
{
|
||||
RemoveFromTree(comp);
|
||||
continue;
|
||||
}
|
||||
|
||||
var newTree = xform.GridUid ?? xform.MapUid;
|
||||
if (!trees.TryGetComponent(newTree, out var newTreeComp) && comp.TreeUid == null)
|
||||
continue;
|
||||
|
||||
Vector2 pos;
|
||||
Angle rot;
|
||||
if (comp.TreeUid == newTree)
|
||||
{
|
||||
(pos, rot) = XformSystem.GetRelativePositionRotation(
|
||||
entry.Transform,
|
||||
newTree!.Value,
|
||||
xforms);
|
||||
|
||||
newTreeComp!.Tree.Update(entry, ExtractAabb(entry, pos, rot));
|
||||
continue;
|
||||
}
|
||||
|
||||
RemoveFromTree(comp);
|
||||
|
||||
if (newTreeComp == null)
|
||||
return;
|
||||
|
||||
comp.TreeUid = newTree;
|
||||
comp.Tree = newTreeComp.Tree;
|
||||
|
||||
(pos, rot) = XformSystem.GetRelativePositionRotation(
|
||||
entry.Transform,
|
||||
newTree.Value,
|
||||
xforms);
|
||||
|
||||
newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot));
|
||||
}
|
||||
|
||||
_updated.Clear();
|
||||
}
|
||||
|
||||
private void RemoveFromTree(TComp component)
|
||||
{
|
||||
component.Tree?.Remove(new() { Component = component });
|
||||
component.Tree = null;
|
||||
component.TreeUid = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region AABBs
|
||||
protected virtual Box2 ExtractAabb(in ComponentTreeEntry<TComp> entry)
|
||||
{
|
||||
if (entry.Component.TreeUid == null)
|
||||
return default;
|
||||
|
||||
var (pos, rot) = XformSystem.GetRelativePositionRotation(
|
||||
entry.Transform,
|
||||
entry.Component.TreeUid.Value,
|
||||
GetEntityQuery<TransformComponent>());
|
||||
|
||||
return ExtractAabb(in entry, pos, rot);
|
||||
}
|
||||
|
||||
protected abstract Box2 ExtractAabb(in ComponentTreeEntry<TComp> entry, Vector2 pos, Angle rot);
|
||||
#endregion
|
||||
|
||||
#region Queries
|
||||
public IEnumerable<TTreeComp> GetIntersectingTrees(MapId mapId, Box2Rotated worldBounds)
|
||||
=> GetIntersectingTrees(mapId, worldBounds.CalcBoundingBox());
|
||||
|
||||
public IEnumerable<TTreeComp> GetIntersectingTrees(MapId mapId, Box2 worldAABB)
|
||||
{
|
||||
if (mapId == MapId.Nullspace) yield break;
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
|
||||
{
|
||||
if (TryComp(grid.Owner, out TTreeComp? treeComp))
|
||||
yield return treeComp;
|
||||
}
|
||||
|
||||
if (TryComp(_mapManager.GetMapEntityId(mapId), out TTreeComp? mapTreeComp))
|
||||
yield return mapTreeComp;
|
||||
}
|
||||
|
||||
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2 worldBounds, bool approx = true)
|
||||
=> QueryAabb(mapId, new Box2Rotated(worldBounds, default, default), approx);
|
||||
|
||||
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2Rotated worldBounds, bool approx = true)
|
||||
{
|
||||
var state = new HashSet<ComponentTreeEntry<TComp>>();
|
||||
foreach (var treeComp in GetIntersectingTrees(mapId, worldBounds))
|
||||
{
|
||||
var bounds = Transform(treeComp.Owner).InvWorldMatrix.TransformBox(worldBounds);
|
||||
|
||||
treeComp.Tree.QueryAabb(ref state, static (ref HashSet<ComponentTreeEntry<TComp>> state, in ComponentTreeEntry<TComp> value) =>
|
||||
{
|
||||
state.Add(value);
|
||||
return true;
|
||||
},
|
||||
bounds, approx);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
public void QueryAabb<TState>(
|
||||
ref TState state,
|
||||
DynamicTree<ComponentTreeEntry<TComp>>.QueryCallbackDelegate<TState> callback,
|
||||
MapId mapId,
|
||||
Box2 worldBounds,
|
||||
bool approx = true)
|
||||
{
|
||||
QueryAabb(ref state, callback, mapId, new Box2Rotated(worldBounds, default, default), approx);
|
||||
}
|
||||
|
||||
public void QueryAabb<TState>(
|
||||
ref TState state,
|
||||
DynamicTree<ComponentTreeEntry<TComp>>.QueryCallbackDelegate<TState> callback,
|
||||
MapId mapId,
|
||||
Box2Rotated worldBounds,
|
||||
bool approx = true)
|
||||
{
|
||||
foreach (var treeComp in GetIntersectingTrees(mapId, worldBounds))
|
||||
{
|
||||
var bounds = Transform(treeComp.Owner).InvWorldMatrix.TransformBox(worldBounds);
|
||||
treeComp.Tree.QueryAabb(ref state, callback, bounds, approx);
|
||||
}
|
||||
}
|
||||
|
||||
public List<RayCastResults> IntersectRayWithPredicate<TState>(MapId mapId, in Ray ray, float maxLength,
|
||||
TState state, Func<EntityUid, TState, bool> predicate, bool returnOnFirstHit = true)
|
||||
{
|
||||
if (mapId == MapId.Nullspace)
|
||||
return new ();
|
||||
|
||||
var queryState = new QueryState<TState>(maxLength, returnOnFirstHit, state, predicate);
|
||||
|
||||
var endPoint = ray.Position + ray.Direction * maxLength;
|
||||
var worldBox = new Box2(Vector2.ComponentMin(ray.Position, endPoint), Vector2.ComponentMax(ray.Position, endPoint));
|
||||
var xforms = GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var comp in GetIntersectingTrees(mapId, worldBox))
|
||||
{
|
||||
var transform = xforms.GetComponent(comp.Owner);
|
||||
var (_, treeRot, matrix) = transform.GetWorldPositionRotationInvMatrix(xforms);
|
||||
var relativeAngle = new Angle(-treeRot.Theta).RotateVec(ray.Direction);
|
||||
var treeRay = new Ray(matrix.Transform(ray.Position), relativeAngle);
|
||||
comp.Tree.QueryRay(ref queryState, QueryCallback, treeRay);
|
||||
if (returnOnFirstHit && queryState.List.Count > 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return queryState.List;
|
||||
|
||||
static bool QueryCallback(
|
||||
ref QueryState<TState> state,
|
||||
in ComponentTreeEntry<TComp> value,
|
||||
in Vector2 point,
|
||||
float distFromOrigin)
|
||||
{
|
||||
if (distFromOrigin > state.MaxLength || state.Predicate.Invoke(value.Uid, state.State))
|
||||
return true;
|
||||
|
||||
state.List.Add(new RayCastResults(distFromOrigin, point, value.Uid));
|
||||
return !state.ReturnOnFirstHit;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct QueryState<TState>
|
||||
{
|
||||
public readonly float MaxLength;
|
||||
public readonly bool ReturnOnFirstHit;
|
||||
public readonly List<RayCastResults> List = new();
|
||||
public readonly TState State;
|
||||
public readonly Func<EntityUid, TState, bool> Predicate;
|
||||
|
||||
public QueryState(float maxLength, bool returnOnFirstHit, TState state, Func<EntityUid, TState, bool> predictate)
|
||||
{
|
||||
MaxLength = maxLength;
|
||||
ReturnOnFirstHit = returnOnFirstHit;
|
||||
State = state;
|
||||
Predicate = predictate;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
32
Robust.Shared/ComponentTrees/IComponentTreeComponent.cs
Normal file
32
Robust.Shared/ComponentTrees/IComponentTreeComponent.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Robust.Shared.ComponentTrees;
|
||||
|
||||
public interface IComponentTreeComponent<TComp> where TComp : Component, IComponentTreeEntry<TComp>
|
||||
{
|
||||
public DynamicTree<ComponentTreeEntry<TComp>> Tree { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface that must be implemented by components that can be stored on component trees.
|
||||
/// </summary>
|
||||
public interface IComponentTreeEntry<TComp> where TComp : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The tree that the component is currently stored on.
|
||||
/// </summary>
|
||||
public EntityUid? TreeUid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tree that the component is currently stored on.
|
||||
/// </summary>
|
||||
public DynamicTree<ComponentTreeEntry<TComp>>? Tree { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the component should currently be added to a tree.
|
||||
/// </summary>
|
||||
public bool AddToTree { get; }
|
||||
|
||||
public bool TreeUpdateQueued { get; set; }
|
||||
}
|
||||
60
Robust.Shared/ComponentTrees/RecursiveMoveSystem.cs
Normal file
60
Robust.Shared/ComponentTrees/RecursiveMoveSystem.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.ComponentTrees;
|
||||
|
||||
/// <summary>
|
||||
/// This system will recursively raise events to update component tree positions any time any entity moves.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used by some client-side systems (e.g., sprites, lights, etc). However this can be quite expensive and if possible should not be used by the server.
|
||||
/// </remarks>
|
||||
internal sealed class RecursiveMoveSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
bool Subscribed = false;
|
||||
|
||||
internal void AddSubscription()
|
||||
{
|
||||
if (Subscribed)
|
||||
return;
|
||||
|
||||
Subscribed = true;
|
||||
SubscribeLocalEvent<MoveEvent>(AnythingMoved);
|
||||
}
|
||||
|
||||
private void AnythingMoved(ref MoveEvent args)
|
||||
{
|
||||
if (args.Component.MapUid == args.Sender || args.Component.GridUid == args.Sender)
|
||||
return;
|
||||
|
||||
DebugTools.Assert(!_mapManager.IsMap(args.Sender));
|
||||
DebugTools.Assert(!_mapManager.IsGrid(args.Sender));
|
||||
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
AnythingMovedSubHandler(args.Sender, args.Component, xformQuery);
|
||||
}
|
||||
|
||||
private void AnythingMovedSubHandler(
|
||||
EntityUid uid,
|
||||
TransformComponent xform,
|
||||
EntityQuery<TransformComponent> xformQuery)
|
||||
{
|
||||
var ev = new TreeRecursiveMoveEvent(xform);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
// TODO only enumerate over entities in containers if necessary?
|
||||
// annoyingly, containers aren't guaranteed to occlude sprites & lights
|
||||
// but AFAIK thats currently unused???
|
||||
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
while (childEnumerator.MoveNext(out var child))
|
||||
{
|
||||
if (xformQuery.TryGetComponent(child.Value, out var childXform))
|
||||
AnythingMovedSubHandler(child.Value, childXform, xformQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Robust.Shared/ComponentTrees/TreeRecursiveMoveEvent.cs
Normal file
13
Robust.Shared/ComponentTrees/TreeRecursiveMoveEvent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Robust.Shared.ComponentTrees;
|
||||
|
||||
[ByRefEvent]
|
||||
internal readonly struct TreeRecursiveMoveEvent
|
||||
{
|
||||
public readonly TransformComponent Xform;
|
||||
public TreeRecursiveMoveEvent(TransformComponent xform)
|
||||
{
|
||||
Xform = xform;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +1,54 @@
|
||||
using System;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
[RegisterComponent]
|
||||
[NetworkedComponent()]
|
||||
[Access(typeof(OccluderSystem))]
|
||||
public sealed class OccluderComponent : Component, IComponentTreeEntry<OccluderComponent>
|
||||
{
|
||||
[NetworkedComponent()]
|
||||
[Virtual]
|
||||
public class OccluderComponent : Component
|
||||
[DataField("enabled")]
|
||||
public bool Enabled = true;
|
||||
|
||||
[DataField("boundingBox")]
|
||||
public Box2 BoundingBox = new(-0.5f, -0.5f, 0.5f, 0.5f);
|
||||
|
||||
public EntityUid? TreeUid { get; set; }
|
||||
public DynamicTree<ComponentTreeEntry<OccluderComponent>>? Tree { get; set; }
|
||||
|
||||
public bool AddToTree => Enabled;
|
||||
public bool TreeUpdateQueued { get; set; } = false;
|
||||
|
||||
[ViewVariables] public (EntityUid Grid, Vector2i Tile)? LastPosition;
|
||||
[ViewVariables] public OccluderDir Occluding;
|
||||
|
||||
[Flags]
|
||||
public enum OccluderDir : byte
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
None = 0,
|
||||
North = 1,
|
||||
East = 1 << 1,
|
||||
South = 1 << 2,
|
||||
West = 1 << 3,
|
||||
}
|
||||
|
||||
[DataField("enabled")]
|
||||
private bool _enabled = true;
|
||||
[DataField("boundingBox")]
|
||||
private Box2 _boundingBox = new(-0.5f, -0.5f, 0.5f, 0.5f);
|
||||
[NetSerializable, Serializable]
|
||||
public sealed class OccluderComponentState : ComponentState
|
||||
{
|
||||
public bool Enabled { get; }
|
||||
public Box2 BoundingBox { get; }
|
||||
|
||||
internal OccluderTreeComponent? Tree = null;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Box2 BoundingBox
|
||||
public OccluderComponentState(bool enabled, Box2 boundingBox)
|
||||
{
|
||||
get => _boundingBox;
|
||||
set
|
||||
{
|
||||
_boundingBox = value;
|
||||
Dirty();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, new OccluderUpdateEvent(this), true);
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public virtual bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
if (_enabled == value)
|
||||
return;
|
||||
|
||||
_enabled = value;
|
||||
if (_enabled)
|
||||
{
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, new OccluderAddEvent(this), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, new OccluderRemoveEvent(this), true);
|
||||
}
|
||||
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new OccluderComponentState(Enabled, BoundingBox);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
if (curState == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cast = (OccluderComponentState) curState;
|
||||
|
||||
Enabled = cast.Enabled;
|
||||
BoundingBox = cast.BoundingBox;
|
||||
}
|
||||
|
||||
[NetSerializable, Serializable]
|
||||
private sealed class OccluderComponentState : ComponentState
|
||||
{
|
||||
public bool Enabled { get; }
|
||||
public Box2 BoundingBox { get; }
|
||||
|
||||
public OccluderComponentState(bool enabled, Box2 boundingBox)
|
||||
{
|
||||
Enabled = enabled;
|
||||
BoundingBox = boundingBox;
|
||||
}
|
||||
Enabled = enabled;
|
||||
BoundingBox = boundingBox;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the relevant occluder children of this entity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class OccluderTreeComponent : Component, IComponentTreeComponent<OccluderComponent>
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the relevant occluder children of this entity.
|
||||
/// </summary>
|
||||
public sealed class OccluderTreeComponent : Component
|
||||
{
|
||||
internal DynamicTree<ComponentTreeEntry<OccluderComponent>> Tree { get; set; } = default!;
|
||||
}
|
||||
public DynamicTree<ComponentTreeEntry<OccluderComponent>> Tree { get; set; } = default!;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
using System;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
[NetworkedComponent]
|
||||
public abstract class SharedPointLightComponent : Component
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
[DataField("enabled")]
|
||||
protected bool _enabled = true;
|
||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
||||
|
||||
[DataField("color")]
|
||||
protected Color _color = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// How far the light projects.
|
||||
/// </summary>
|
||||
[DataField("radius")]
|
||||
protected float _radius = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Offset from the center of the entity.
|
||||
/// </summary>
|
||||
@@ -43,17 +33,17 @@ namespace Robust.Shared.GameObjects
|
||||
[DataField("castShadows")]
|
||||
public bool CastShadows = true;
|
||||
|
||||
[Access(typeof(SharedPointLightSystem))]
|
||||
[DataField("enabled")]
|
||||
public bool _enabled = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public virtual bool Enabled
|
||||
[Animatable] // please somebody ECS animations
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
if (_enabled == value) return;
|
||||
_enabled = value;
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, new PointLightToggleEvent(_enabled), true);
|
||||
Dirty();
|
||||
}
|
||||
[Obsolete("Use the system's setter")]
|
||||
set => _sysMan.GetEntitySystem<SharedPointLightSystem>().SetEnabled(Owner, value, this);
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
@@ -68,16 +58,20 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How far the light projects.
|
||||
/// </summary>
|
||||
[DataField("radius")]
|
||||
[Access(typeof(SharedPointLightSystem))]
|
||||
public float _radius = 5f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public virtual float Radius
|
||||
[Animatable] // please somebody ECS animations
|
||||
public float Radius
|
||||
{
|
||||
get => _radius;
|
||||
set
|
||||
{
|
||||
if (MathHelper.CloseToPercent(_radius, value)) return;
|
||||
_radius = MathF.Max(value, 0.01f); // setting radius to 0 causes exceptions, so just use a value close enough to zero that it's unnoticeable.
|
||||
Dirty();
|
||||
}
|
||||
[Obsolete("Use the system's setter")]
|
||||
set => _sysMan.GetEntitySystem<SharedPointLightSystem>().SetRadius(Owner, value, this);
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
|
||||
@@ -749,18 +749,20 @@ namespace Robust.Shared.GameObjects
|
||||
public readonly EntityUid Entity;
|
||||
public readonly EntityUid OldGrid;
|
||||
public readonly EntityUid Grid;
|
||||
public readonly TransformComponent Xform;
|
||||
|
||||
/// <summary>
|
||||
/// Tile on both the old and new grid being re-anchored.
|
||||
/// </summary>
|
||||
public readonly Vector2i TilePos;
|
||||
|
||||
public ReAnchorEvent(EntityUid uid, EntityUid oldGrid, EntityUid grid, Vector2i tilePos)
|
||||
public ReAnchorEvent(EntityUid uid, EntityUid oldGrid, EntityUid grid, Vector2i tilePos, TransformComponent xform)
|
||||
{
|
||||
Entity = uid;
|
||||
OldGrid = oldGrid;
|
||||
Grid = grid;
|
||||
TilePos = tilePos;
|
||||
Xform = xform;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,261 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.ComponentTrees;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
namespace Robust.Shared.GameObjects;
|
||||
public abstract class OccluderSystem : ComponentTreeSystem<OccluderTreeComponent, OccluderComponent>
|
||||
{
|
||||
public abstract class OccluderSystem : EntitySystem
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly IMapManagerInternal _mapManager = default!;
|
||||
|
||||
private const float TreeGrowthRate = 256;
|
||||
|
||||
private Queue<OccluderEvent> _updates = new(64);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
UpdatesOutsidePrediction = true;
|
||||
|
||||
SubscribeLocalEvent<MapChangedEvent>(ev =>
|
||||
{
|
||||
if (ev.Created)
|
||||
OnMapCreated(ev);
|
||||
});
|
||||
|
||||
SubscribeLocalEvent<GridInitializeEvent>(HandleGridInit);
|
||||
SubscribeLocalEvent<OccluderTreeComponent, ComponentInit>(HandleOccluderTreeInit);
|
||||
SubscribeLocalEvent<OccluderComponent, ComponentInit>(HandleOccluderInit);
|
||||
SubscribeLocalEvent<OccluderComponent, ComponentShutdown>(HandleOccluderShutdown);
|
||||
|
||||
SubscribeLocalEvent<OccluderComponent, MoveEvent>(EntMoved);
|
||||
SubscribeLocalEvent<OccluderComponent, EntParentChangedMessage>(EntParentChanged);
|
||||
SubscribeLocalEvent<OccluderEvent>(ev => _updates.Enqueue(ev));
|
||||
}
|
||||
|
||||
internal IEnumerable<OccluderTreeComponent> GetOccluderTrees(MapId mapId, Box2 worldAABB)
|
||||
{
|
||||
if (mapId == MapId.Nullspace) yield break;
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
|
||||
{
|
||||
yield return EntityManager.GetComponent<OccluderTreeComponent>(grid.Owner);
|
||||
}
|
||||
|
||||
yield return EntityManager.GetComponent<OccluderTreeComponent>(_mapManager.GetMapEntityId(mapId));
|
||||
}
|
||||
|
||||
private void HandleOccluderInit(EntityUid uid, OccluderComponent component, ComponentInit args)
|
||||
{
|
||||
if (!component.Enabled) return;
|
||||
_updates.Enqueue(new OccluderAddEvent(component));
|
||||
}
|
||||
|
||||
private void HandleOccluderShutdown(EntityUid uid, OccluderComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (!component.Enabled) return;
|
||||
_updates.Enqueue(new OccluderRemoveEvent(component));
|
||||
}
|
||||
|
||||
private void HandleOccluderTreeInit(EntityUid uid, OccluderTreeComponent component, ComponentInit args)
|
||||
{
|
||||
var capacity = (int) Math.Min(256, Math.Ceiling(EntityManager.GetComponent<TransformComponent>(component.Owner).ChildCount / TreeGrowthRate) * TreeGrowthRate);
|
||||
|
||||
component.Tree = new(ExtractAabbFunc, capacity: capacity);
|
||||
}
|
||||
|
||||
private void HandleGridInit(GridInitializeEvent ev)
|
||||
{
|
||||
EntityManager.EnsureComponent<OccluderTreeComponent>(ev.EntityUid);
|
||||
}
|
||||
|
||||
private OccluderTreeComponent? GetOccluderTree(OccluderComponent component)
|
||||
{
|
||||
var entity = component.Owner;
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
if (!xformQuery.TryGetComponent(entity, out var xform) || xform.MapID == MapId.Nullspace)
|
||||
return null;
|
||||
|
||||
var query = GetEntityQuery<OccluderTreeComponent>();
|
||||
while (xform.ParentUid.IsValid())
|
||||
{
|
||||
if (query.TryGetComponent(xform.ParentUid, out var comp))
|
||||
return comp;
|
||||
|
||||
xform = xformQuery.GetComponent(xform.ParentUid);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_updates.Clear();
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
UpdateTrees();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
UpdateTrees();
|
||||
}
|
||||
|
||||
private void UpdateTrees()
|
||||
{
|
||||
var query = GetEntityQuery<TransformComponent>();
|
||||
while (_updates.TryDequeue(out var occluderUpdate))
|
||||
{
|
||||
OccluderTreeComponent? tree;
|
||||
var component = occluderUpdate.Component;
|
||||
|
||||
switch (occluderUpdate)
|
||||
{
|
||||
case OccluderAddEvent:
|
||||
if (component.Tree != null || component.Deleted) break;
|
||||
tree = GetOccluderTree(component);
|
||||
if (tree == null) break;
|
||||
component.Tree = tree;
|
||||
tree.Tree.Add(new()
|
||||
{
|
||||
Component = component,
|
||||
Transform = query.GetComponent(component.Owner)
|
||||
});
|
||||
break;
|
||||
case OccluderUpdateEvent:
|
||||
if (component.Deleted) break;
|
||||
var oldTree = component.Tree;
|
||||
tree = GetOccluderTree(component);
|
||||
var entry = new ComponentTreeEntry<OccluderComponent>()
|
||||
{
|
||||
Component = component,
|
||||
Transform = query.GetComponent(component.Owner)
|
||||
};
|
||||
if (oldTree != tree)
|
||||
{
|
||||
oldTree?.Tree.Remove(entry);
|
||||
tree?.Tree.Add(entry);
|
||||
component.Tree = tree;
|
||||
break;
|
||||
}
|
||||
|
||||
tree?.Tree.Update(entry);
|
||||
|
||||
break;
|
||||
case OccluderRemoveEvent:
|
||||
tree = component.Tree;
|
||||
tree?.Tree.Remove(new() { Component = component });
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException($"No implemented occluder update for {occluderUpdate.GetType()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EntMoved(EntityUid uid, OccluderComponent component, ref MoveEvent args)
|
||||
{
|
||||
_updates.Enqueue(new OccluderUpdateEvent(component));
|
||||
}
|
||||
|
||||
private void EntParentChanged(EntityUid uid, OccluderComponent component, ref EntParentChangedMessage args)
|
||||
{
|
||||
_updates.Enqueue(new OccluderUpdateEvent(component));
|
||||
}
|
||||
|
||||
private void OnMapCreated(MapChangedEvent e)
|
||||
{
|
||||
if (e.Map == MapId.Nullspace) return;
|
||||
|
||||
EnsureComp<OccluderTreeComponent>(e.Uid);
|
||||
}
|
||||
|
||||
private Box2 ExtractAabbFunc(in ComponentTreeEntry<OccluderComponent> entry)
|
||||
{
|
||||
return entry.Component.BoundingBox.Translated(entry.Transform.LocalPosition);
|
||||
}
|
||||
|
||||
public IEnumerable<RayCastResults> IntersectRayWithPredicate(MapId mapId, in Ray ray, float maxLength,
|
||||
Func<EntityUid, bool>? predicate = null, bool returnOnFirstHit = true)
|
||||
{
|
||||
// ReSharper disable once ConvertToLocalFunction
|
||||
var wrapper = (EntityUid uid, Func<EntityUid, bool>? wrapped)
|
||||
=> wrapped != null && wrapped(uid);
|
||||
|
||||
return IntersectRayWithPredicate(mapId, in ray, maxLength, predicate, wrapper, returnOnFirstHit);
|
||||
}
|
||||
|
||||
public IEnumerable<RayCastResults> IntersectRayWithPredicate<TState>(MapId mapId, in Ray ray, float maxLength,
|
||||
TState state, Func<EntityUid, TState, bool> predicate, bool returnOnFirstHit = true)
|
||||
{
|
||||
if (mapId == MapId.Nullspace) return Enumerable.Empty<RayCastResults>();
|
||||
var list = new List<RayCastResults>();
|
||||
|
||||
var endPoint = ray.Position + ray.Direction * maxLength;
|
||||
var worldBox = new Box2(Vector2.ComponentMin(ray.Position, endPoint), Vector2.ComponentMax(ray.Position, endPoint));
|
||||
var xforms = EntityManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var comp in GetOccluderTrees(mapId, worldBox))
|
||||
{
|
||||
var transform = xforms.GetComponent(comp.Owner);
|
||||
var (_, treeRot, matrix) = transform.GetWorldPositionRotationInvMatrix(xforms);
|
||||
|
||||
var relativeAngle = new Angle(-treeRot.Theta).RotateVec(ray.Direction);
|
||||
|
||||
var treeRay = new Ray(matrix.Transform(ray.Position), relativeAngle);
|
||||
|
||||
comp.Tree.QueryRay(ref list,
|
||||
(ref List<RayCastResults> listState, in ComponentTreeEntry<OccluderComponent> value, in Vector2 point, float distFromOrigin) =>
|
||||
{
|
||||
if (distFromOrigin > maxLength)
|
||||
return true;
|
||||
|
||||
if (!value.Component.Enabled)
|
||||
return true;
|
||||
|
||||
if (predicate.Invoke(value.Uid, state))
|
||||
return true;
|
||||
|
||||
var result = new RayCastResults(distFromOrigin, point, value.Uid);
|
||||
listState.Add(result);
|
||||
return !returnOnFirstHit;
|
||||
}, treeRay);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<OccluderComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<OccluderComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
internal sealed class OccluderAddEvent : OccluderEvent
|
||||
private void OnGetState(EntityUid uid, OccluderComponent comp, ref ComponentGetState args)
|
||||
{
|
||||
public OccluderAddEvent(OccluderComponent component) : base(component) {}
|
||||
args.State = new OccluderComponent.OccluderComponentState(comp.Enabled, comp.BoundingBox);
|
||||
}
|
||||
private void OnHandleState(EntityUid uid, OccluderComponent comp, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not OccluderComponent.OccluderComponentState state)
|
||||
return;
|
||||
|
||||
SetEnabled(uid, state.Enabled, comp);
|
||||
SetBoundingBox(uid, state.BoundingBox, comp);
|
||||
}
|
||||
|
||||
internal sealed class OccluderUpdateEvent : OccluderEvent
|
||||
#region Component Tree Overrides
|
||||
protected override bool DoFrameUpdate => true;
|
||||
protected override bool DoTickUpdate => true;
|
||||
|
||||
// this system relies on the assumption that all occluders are parented directly to a grid or map.
|
||||
// if this ever changes, this will make server move events very expensive.
|
||||
protected override bool Recursive => false;
|
||||
|
||||
protected override Box2 ExtractAabb(in ComponentTreeEntry<OccluderComponent> entry)
|
||||
{
|
||||
public OccluderUpdateEvent(OccluderComponent component) : base(component) {}
|
||||
DebugTools.Assert(entry.Transform.ParentUid == entry.Component.TreeUid);
|
||||
return entry.Component.BoundingBox.Translated(entry.Transform.LocalPosition);
|
||||
}
|
||||
|
||||
internal sealed class OccluderRemoveEvent : OccluderEvent
|
||||
protected override Box2 ExtractAabb(in ComponentTreeEntry<OccluderComponent> entry, Vector2 pos, Angle rot)
|
||||
=> ExtractAabb(in entry);
|
||||
#endregion
|
||||
|
||||
#region Setters
|
||||
public void SetBoundingBox(EntityUid uid, Box2 box, OccluderComponent? comp = null)
|
||||
{
|
||||
public OccluderRemoveEvent(OccluderComponent component) : base(component) {}
|
||||
if (!Resolve(uid, ref comp))
|
||||
return;
|
||||
|
||||
comp.BoundingBox = box;
|
||||
Dirty(comp);
|
||||
|
||||
if (comp.TreeUid != null)
|
||||
QueueTreeUpdate(uid, comp);
|
||||
}
|
||||
|
||||
internal abstract class OccluderEvent : EntityEventArgs
|
||||
public virtual void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null)
|
||||
{
|
||||
public OccluderComponent Component { get; }
|
||||
if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled)
|
||||
return;
|
||||
|
||||
public OccluderEvent(OccluderComponent component)
|
||||
{
|
||||
Component = component;
|
||||
}
|
||||
comp.Enabled = enabled;
|
||||
Dirty(comp);
|
||||
QueueTreeUpdate(uid, comp);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
@@ -19,13 +20,33 @@ namespace Robust.Shared.GameObjects
|
||||
private void HandleCompState(EntityUid uid, SharedPointLightComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not PointLightComponentState newState) return;
|
||||
component.Enabled = newState.Enabled;
|
||||
component.Radius = newState.Radius;
|
||||
|
||||
SetEnabled(uid, newState.Enabled, component);
|
||||
SetRadius(uid, newState.Radius, component);
|
||||
component.Offset = newState.Offset;
|
||||
component.Color = newState.Color;
|
||||
component.Energy = newState.Energy;
|
||||
component.Softness = newState.Softness;
|
||||
component.CastShadows = newState.CastShadows;
|
||||
}
|
||||
|
||||
public virtual void SetEnabled(EntityUid uid, bool enabled, SharedPointLightComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp) || enabled == comp.Enabled)
|
||||
return;
|
||||
|
||||
comp._enabled = enabled;
|
||||
RaiseLocalEvent(uid, new PointLightToggleEvent(comp.Enabled));
|
||||
Dirty(comp);
|
||||
}
|
||||
|
||||
public virtual void SetRadius(EntityUid uid, float radius, SharedPointLightComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp) || MathHelper.CloseToPercent(comp.Radius, radius))
|
||||
return;
|
||||
|
||||
comp._radius = radius;
|
||||
Dirty(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public abstract partial class SharedTransformSystem
|
||||
DebugTools.Assert(xform._anchored);
|
||||
|
||||
Dirty(xform);
|
||||
var ev = new ReAnchorEvent(xform.Owner, ((Component) oldGrid).Owner, ((Component) newGrid).Owner, tilePos);
|
||||
var ev = new ReAnchorEvent(xform.Owner, ((Component) oldGrid).Owner, ((Component) newGrid).Owner, tilePos, xform);
|
||||
RaiseLocalEvent(xform.Owner, ref ev);
|
||||
}
|
||||
|
||||
@@ -731,7 +731,7 @@ public abstract partial class SharedTransformSystem
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal (Vector2 Position, Angle Rotation) GetParentRelativePositionRotation(
|
||||
public (Vector2 Position, Angle Rotation) GetRelativePositionRotation(
|
||||
TransformComponent component,
|
||||
EntityUid relative,
|
||||
EntityQuery<TransformComponent> query)
|
||||
@@ -749,7 +749,7 @@ public abstract partial class SharedTransformSystem
|
||||
}
|
||||
|
||||
// Entity was not actually in the transform hierarchy. This is probably a sign that something is wrong, or that the function is being misused.
|
||||
Logger.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetParentRelativePositionRotation)}.");
|
||||
Logger.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetRelativePositionRotation)}.");
|
||||
var relXform = query.GetComponent(relative);
|
||||
pos = relXform.InvWorldMatrix.Transform(pos);
|
||||
rot = rot - relXform.WorldRotation;
|
||||
@@ -759,6 +759,36 @@ public abstract partial class SharedTransformSystem
|
||||
return (pos, rot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the position and rotation relative to some entity higher up in the component's transform hierarchy.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vector2 GetRelativePosition(
|
||||
TransformComponent component,
|
||||
EntityUid relative,
|
||||
EntityQuery<TransformComponent> query)
|
||||
{
|
||||
var pos = component._localPosition;
|
||||
var xform = component;
|
||||
while (xform.ParentUid != relative)
|
||||
{
|
||||
if (xform.ParentUid.IsValid() && query.TryGetComponent(xform.ParentUid, out xform))
|
||||
{
|
||||
pos = xform._localRotation.RotateVec(pos) + xform._localPosition;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Entity was not actually in the transform hierarchy. This is probably a sign that something is wrong, or that the function is being misused.
|
||||
Logger.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetRelativePositionRotation)}.");
|
||||
var relXform = query.GetComponent(relative);
|
||||
pos = relXform.InvWorldMatrix.Transform(pos);
|
||||
break;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetWorldPosition(EntityUid uid, Vector2 worldPos)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.Debugging;
|
||||
@@ -120,6 +121,7 @@ namespace Robust.UnitTesting
|
||||
var mapMan = deps.Resolve<IMapManager>();
|
||||
|
||||
// Required components for the engine to work
|
||||
// Why are we still here? Just to suffer? Why can't we just use [RegisterComponent] magic?
|
||||
var compFactory = deps.Resolve<IComponentFactory>();
|
||||
|
||||
if (!compFactory.AllRegisteredTypes.Contains(typeof(MapComponent)))
|
||||
@@ -157,6 +159,26 @@ namespace Robust.UnitTesting
|
||||
compFactory.RegisterClass<JointComponent>();
|
||||
}
|
||||
|
||||
if (!compFactory.AllRegisteredTypes.Contains(typeof(OccluderComponent)))
|
||||
{
|
||||
compFactory.RegisterClass<OccluderComponent>();
|
||||
}
|
||||
|
||||
if (!compFactory.AllRegisteredTypes.Contains(typeof(OccluderTreeComponent)))
|
||||
{
|
||||
compFactory.RegisterClass<OccluderTreeComponent>();
|
||||
}
|
||||
|
||||
if (!compFactory.AllRegisteredTypes.Contains(typeof(SpriteTreeComponent)))
|
||||
{
|
||||
compFactory.RegisterClass<SpriteTreeComponent>();
|
||||
}
|
||||
|
||||
if (!compFactory.AllRegisteredTypes.Contains(typeof(LightTreeComponent)))
|
||||
{
|
||||
compFactory.RegisterClass<LightTreeComponent>();
|
||||
}
|
||||
|
||||
// So by default EntityManager does its own EntitySystemManager initialize during Startup.
|
||||
// We want to bypass this and load our own systems hence we will manually initialize it here.
|
||||
entMan.Initialize();
|
||||
|
||||
@@ -257,6 +257,7 @@ namespace Robust.UnitTesting.Server
|
||||
|
||||
var compFactory = container.Resolve<IComponentFactory>();
|
||||
|
||||
// if only we had some sort of attribute for autmatically registering components.
|
||||
compFactory.RegisterClass<MetaDataComponent>();
|
||||
compFactory.RegisterClass<TransformComponent>();
|
||||
compFactory.RegisterClass<MapGridComponent>();
|
||||
@@ -269,6 +270,8 @@ namespace Robust.UnitTesting.Server
|
||||
compFactory.RegisterClass<PhysicsMapComponent>();
|
||||
compFactory.RegisterClass<FixturesComponent>();
|
||||
compFactory.RegisterClass<CollisionWakeComponent>();
|
||||
compFactory.RegisterClass<OccluderComponent>();
|
||||
compFactory.RegisterClass<OccluderTreeComponent>();
|
||||
|
||||
_regDelegate?.Invoke(compFactory);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user