mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-01 09:37:03 +02:00
239 lines
7.8 KiB
C#
239 lines
7.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using Robust.Shared.ComponentTrees;
|
|
using Robust.Shared.GameStates;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Physics.Shapes;
|
|
using Robust.Shared.Physics.Systems;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Shared.GameObjects;
|
|
public abstract partial class OccluderSystem : ComponentTreeSystem<OccluderTreeComponent, OccluderComponent>
|
|
{
|
|
public const float MaxRaycastRange = 100f;
|
|
|
|
[Dependency] private FixtureSystem _fixtureSystem = default!;
|
|
|
|
[Dependency] private EntityQuery<OccluderComponent> _occluderQuery;
|
|
[Dependency] private EntityQuery<TransformComponent> _xformQuery;
|
|
|
|
private readonly List<RayCastResults> _raycastResults = new();
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
SubscribeLocalEvent<OccluderComponent, ComponentInit>(OnCompInit);
|
|
SubscribeLocalEvent<OccluderComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
|
|
}
|
|
|
|
private void OnCompInit(EntityUid uid, OccluderComponent comp, ComponentInit args)
|
|
{
|
|
UpdatePolygonCache(comp);
|
|
}
|
|
|
|
private void OnAfterAutoHandleState(EntityUid uid, OccluderComponent comp, ref AfterAutoHandleStateEvent args)
|
|
{
|
|
UpdatePolygonCache(comp);
|
|
QueueTreeUpdate(uid, comp);
|
|
OnOccluderAfterAutoHandleState(uid, comp, ref args);
|
|
}
|
|
|
|
protected virtual void OnOccluderAfterAutoHandleState(EntityUid uid, OccluderComponent comp, ref AfterAutoHandleStateEvent args)
|
|
{
|
|
}
|
|
|
|
#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)
|
|
{
|
|
DebugTools.Assert(entry.Transform.ParentUid == entry.Component.TreeUid);
|
|
var position = entry.Transform.LocalPosition;
|
|
return new Box2Rotated(
|
|
entry.Component.LocalBounds.Translated(position),
|
|
entry.Transform.LocalRotation,
|
|
position).CalcBoundingBox();
|
|
}
|
|
|
|
protected override Box2 ExtractAabb(in ComponentTreeEntry<OccluderComponent> entry, Vector2 pos, Angle rot)
|
|
=> new Box2Rotated(entry.Component.LocalBounds.Translated(pos), rot, pos).CalcBoundingBox();
|
|
#endregion
|
|
|
|
#region Setters
|
|
public virtual void SetPolygon(EntityUid uid, Vector2[]? polygon, OccluderComponent? comp = null)
|
|
{
|
|
if (!Resolve(uid, ref comp))
|
|
return;
|
|
|
|
comp.PolygonArray = polygon ??
|
|
[
|
|
new(-0.5f, 0.5f),
|
|
new(0.5f, 0.5f),
|
|
new(0.5f, -0.5f),
|
|
new(-0.5f, -0.5f),
|
|
];
|
|
UpdatePolygonCache(comp);
|
|
Dirty(uid, comp);
|
|
|
|
if (comp.TreeUid != null)
|
|
QueueTreeUpdate(uid, comp);
|
|
}
|
|
|
|
public virtual void SetEnabled(EntityUid uid, bool enabled, OccluderComponent? comp = null, MetaDataComponent? meta = null)
|
|
{
|
|
if (!Resolve(uid, ref comp, false) || enabled == comp.Enabled)
|
|
return;
|
|
|
|
comp.Enabled = enabled;
|
|
Dirty(uid, comp, meta);
|
|
QueueTreeUpdate(uid, comp);
|
|
}
|
|
#endregion
|
|
|
|
protected override void OnCompStartup(EntityUid uid, OccluderComponent component, ComponentStartup args)
|
|
{
|
|
UpdatePolygonCache(component);
|
|
base.OnCompStartup(uid, component, args);
|
|
}
|
|
|
|
private static void UpdatePolygonCache(OccluderComponent occluder)
|
|
{
|
|
occluder.LocalBounds = CalculateLocalBounds(occluder.Polygon);
|
|
}
|
|
|
|
#region InRangeUnoccluded
|
|
|
|
/// <summary>
|
|
/// Returns true if two points are within the specified range and there are no occluders between them that aren't
|
|
/// ignored by the predicate.
|
|
/// </summary>
|
|
public bool InRangeUnoccluded<TState>(
|
|
MapCoordinates origin,
|
|
MapCoordinates other,
|
|
float range,
|
|
TState state,
|
|
Func<Entity<OccluderComponent, TransformComponent>, TState, bool> ignore)
|
|
{
|
|
if (!GetRay(origin, other, range, out var length, out var ray, out var result))
|
|
return result;
|
|
|
|
IntersectRay(_raycastResults, origin.MapId, ray, length);
|
|
foreach (var rayResult in _raycastResults)
|
|
{
|
|
if (!_occluderQuery.TryComp(rayResult.HitEntity, out var occluder) ||
|
|
!_xformQuery.TryComp(rayResult.HitEntity, out var xform))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!ignore(new Entity<OccluderComponent, TransformComponent>(rayResult.HitEntity, occluder, xform), state))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if two points are within the specified range and there are no occluders between them.
|
|
/// </summary>
|
|
/// <param name="ignoreTouching">If true, this will ignore occluders that contain the start or end point.</param>
|
|
public bool InRangeUnoccluded(MapCoordinates origin, MapCoordinates other, float range, bool ignoreTouching)
|
|
{
|
|
if (!GetRay(origin, other, range, out var length, out var ray, out var result))
|
|
return result;
|
|
|
|
IntersectRay(_raycastResults, origin.MapId, ray, length);
|
|
foreach (var rayResult in _raycastResults)
|
|
{
|
|
if (!ignoreTouching)
|
|
return false;
|
|
|
|
if (!_occluderQuery.TryComp(rayResult.HitEntity, out var occluder) ||
|
|
!_xformQuery.TryComp(rayResult.HitEntity, out var xform))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ContainsPoint(occluder, xform, origin.Position) ||
|
|
ContainsPoint(occluder, xform, other.Position))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool GetRay(MapCoordinates origin, MapCoordinates other, float range, out float length, out Ray ray, out bool result)
|
|
{
|
|
ray = default;
|
|
length = default;
|
|
result = false;
|
|
if (other.MapId != origin.MapId || other.MapId == MapId.Nullspace)
|
|
return false;
|
|
|
|
var dir = other.Position - origin.Position;
|
|
length = dir.Length();
|
|
if (MathHelper.CloseTo(length, 0))
|
|
{
|
|
result = true;
|
|
return false;
|
|
}
|
|
|
|
var normalized = dir / length;
|
|
|
|
if (range > 0f && length > range + 0.01f)
|
|
return false;
|
|
|
|
if (length > MaxRaycastRange)
|
|
{
|
|
Log.Warning($"{nameof(InRangeUnoccluded)} check performed over extreme range. Limiting range.");
|
|
length = MaxRaycastRange;
|
|
}
|
|
|
|
ray = new Ray(origin.Position, normalized);
|
|
return true;
|
|
}
|
|
|
|
public bool ContainsPoint(OccluderComponent occluder, TransformComponent xform, Vector2 point)
|
|
{
|
|
// Broadphase check
|
|
var (worldPosition, worldRotation) = XformSystem.GetWorldPositionRotation(xform);
|
|
var worldBounds = new Box2Rotated(
|
|
occluder.LocalBounds.Translated(worldPosition),
|
|
worldRotation,
|
|
worldPosition).CalcBoundingBox();
|
|
|
|
if (!worldBounds.Contains(point))
|
|
return false;
|
|
|
|
// Narrowphase check
|
|
var polygon = new Polygon(occluder.PolygonArray);
|
|
return polygon.VertexCount >= 3 &&
|
|
_fixtureSystem.TestPoint(polygon, new Transform(worldPosition, worldRotation), point);
|
|
}
|
|
|
|
private static Box2 CalculateLocalBounds(ReadOnlySpan<Vector2> polygon)
|
|
{
|
|
var bounds = new Box2(polygon[0], polygon[0]);
|
|
for (var i = 1; i < polygon.Length; i++)
|
|
{
|
|
bounds = bounds.ExtendToContain(polygon[i]);
|
|
}
|
|
|
|
return bounds;
|
|
}
|
|
#endregion
|
|
}
|