mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-01 09:37:03 +02:00
647 lines
21 KiB
C#
647 lines
21 KiB
C#
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;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Robust.Shared.Collections;
|
|
using System.Numerics;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Shared.ComponentTrees;
|
|
|
|
/// <summary>
|
|
/// Keeps track of <see cref="DynamicTree{T}"/>s for various rendering-related components.
|
|
/// </summary>
|
|
[UsedImplicitly]
|
|
public abstract partial class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
|
|
where TTreeComp : Component, IComponentTreeComponent<TComp>, new()
|
|
where TComp : Component, IComponentTreeEntry<TComp>
|
|
{
|
|
[Dependency] private RecursiveMoveSystem _recursiveMoveSys = default!;
|
|
[Dependency] protected SharedTransformSystem XformSystem = default!;
|
|
[Dependency] private SharedMapSystem _mapSystem = default!;
|
|
|
|
private readonly Queue<ComponentTreeEntry<TComp>> _updateQueue = new();
|
|
protected EntityQuery<TComp> Query;
|
|
|
|
/// <summary>
|
|
/// Whether this lookup tree should even be enabled.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This can be used to disable some trees if they are not required, which helps improve performance a bit.
|
|
/// </remarks>
|
|
protected virtual bool Enabled => true;
|
|
private bool _initialized;
|
|
|
|
/// <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();
|
|
|
|
if (!Enabled)
|
|
return;
|
|
|
|
_initialized = true;
|
|
UpdatesOutsidePrediction = DoTickUpdate;
|
|
UpdatesAfter.Add(typeof(SharedTransformSystem));
|
|
UpdatesAfter.Add(typeof(SharedPhysicsSystem));
|
|
|
|
SubscribeLocalEvent<MapCreatedEvent>(MapManagerOnMapCreated);
|
|
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
|
|
|
|
// Yipeee point light
|
|
if (!typeof(TComp).IsAbstract)
|
|
{
|
|
SubscribeLocalEvent<TComp, ComponentStartup>(OnCompStartup);
|
|
SubscribeLocalEvent<TComp, ComponentRemove>(OnCompRemoved);
|
|
Query = GetEntityQuery<TComp>();
|
|
}
|
|
|
|
if (Recursive)
|
|
{
|
|
_recursiveMoveSys.OnTreeRecursiveMove += HandleRecursiveMove;
|
|
_recursiveMoveSys.AddSubscription();
|
|
}
|
|
else
|
|
{
|
|
// TODO EXCEPTION TOLERANCE
|
|
// Ensure lookup trees update before content code handles move events.
|
|
SubscribeLocalEvent<TComp, MoveEvent>(HandleMove);
|
|
}
|
|
|
|
SubscribeLocalEvent<TTreeComp, EntityTerminatingEvent>(OnTerminating);
|
|
SubscribeLocalEvent<TTreeComp, ComponentAdd>(OnTreeAdd);
|
|
SubscribeLocalEvent<TTreeComp, ComponentRemove>(OnTreeRemove);
|
|
}
|
|
|
|
public override void Shutdown()
|
|
{
|
|
if (!_initialized)
|
|
return;
|
|
|
|
_initialized = false;
|
|
if (Recursive)
|
|
_recursiveMoveSys.OnTreeRecursiveMove -= HandleRecursiveMove;
|
|
}
|
|
|
|
private bool CheckEnabled()
|
|
{
|
|
if (_initialized)
|
|
return true;
|
|
|
|
Log.Error($"Attempted to use disabled lookup tree");
|
|
return false;
|
|
}
|
|
|
|
#region Queue Update
|
|
|
|
private void HandleRecursiveMove(EntityUid uid, TransformComponent xform)
|
|
{
|
|
if (Query.TryGetComponent(uid, out var component))
|
|
QueueTreeUpdate(uid, component, xform);
|
|
}
|
|
|
|
private void HandleMove(EntityUid uid, TComp component, ref MoveEvent args)
|
|
{
|
|
QueueTreeUpdate(uid, component, args.Component);
|
|
OnComponentMove(uid, component, ref args);
|
|
}
|
|
|
|
protected virtual void OnComponentMove(EntityUid uid, TComp component, ref MoveEvent args)
|
|
{
|
|
}
|
|
|
|
public void QueueTreeUpdate(EntityUid uid, TComp component, TransformComponent? xform = null)
|
|
{
|
|
if (!_initialized)
|
|
return;
|
|
|
|
if (component.TreeUpdateQueued || !Resolve(uid, ref xform))
|
|
return;
|
|
|
|
component.TreeUpdateQueued = true;
|
|
_updateQueue.Enqueue((component, xform));
|
|
}
|
|
|
|
public void QueueTreeUpdate(Entity<TComp> entity, TransformComponent? xform = null)
|
|
{
|
|
QueueTreeUpdate(entity.Owner, entity.Comp, 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)
|
|
{
|
|
foreach (var entry in component.Tree)
|
|
{
|
|
entry.Component.TreeUid = null;
|
|
entry.Component.Tree = null;
|
|
}
|
|
|
|
component.Tree.Clear();
|
|
}
|
|
|
|
protected virtual void OnTerminating(EntityUid uid, TTreeComp component, ref EntityTerminatingEvent args)
|
|
{
|
|
// IIRC, this is to prevent a tree-update spam as each of the entity's children get detached to nullspace.
|
|
RemComp(uid, component);
|
|
}
|
|
|
|
private void MapManagerOnMapCreated(MapCreatedEvent e)
|
|
{
|
|
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 && _initialized)
|
|
UpdateTreePositions();
|
|
}
|
|
|
|
public override void FrameUpdate(float frameTime)
|
|
{
|
|
if (DoFrameUpdate && _initialized)
|
|
UpdateTreePositions();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes any pending position updates. Note that this should generally always get run before directly
|
|
/// querying a tree.
|
|
/// </summary>
|
|
public void UpdateTreePositions()
|
|
{
|
|
try
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
if (_updateQueue.Count == 0)
|
|
return;
|
|
|
|
var trees = GetEntityQuery<TTreeComp>();
|
|
|
|
while (_updateQueue.TryDequeue(out var entry))
|
|
{
|
|
var (comp, xform) = entry;
|
|
|
|
// Was this entity queued multiple times?
|
|
DebugTools.Assert(comp.TreeUpdateQueued, "Entity was queued multiple times?");
|
|
|
|
comp.TreeUpdateQueued = false;
|
|
if (!comp.Running)
|
|
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);
|
|
|
|
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);
|
|
|
|
newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_updateQueue.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);
|
|
|
|
return ExtractAabb(in entry, pos, rot);
|
|
}
|
|
|
|
protected abstract Box2 ExtractAabb(in ComponentTreeEntry<TComp> entry, Vector2 pos, Angle rot);
|
|
#endregion
|
|
|
|
#region Queries
|
|
public ValueList<(EntityUid Uid, TTreeComp Comp)> GetIntersectingTrees(MapId mapId, Box2Rotated worldBounds)
|
|
=> GetIntersectingTrees(mapId, worldBounds.CalcBoundingBox());
|
|
|
|
public ValueList<(EntityUid Uid, TTreeComp Comp)> GetIntersectingTrees(MapId mapId, Box2 worldAABB)
|
|
=> GetIntersectingTreesInternal(mapId, worldAABB);
|
|
|
|
internal ValueList<(EntityUid Uid, TTreeComp Comp)> GetIntersectingTreesInternal(MapId mapId, Box2 worldAABB)
|
|
{
|
|
if (!CheckEnabled())
|
|
return default;
|
|
// Anything that queries these trees should only do so if there are no queued updates, otherwise it can lead to
|
|
// errors. Currently, there is no easy way to enforce this, but this should work as long as nothing queries the
|
|
// trees directly:
|
|
UpdateTreePositions();
|
|
|
|
var trees = new ValueList<(EntityUid Uid, TTreeComp Comp)>();
|
|
|
|
if (mapId == MapId.Nullspace)
|
|
return trees;
|
|
|
|
// TODO LOOKUPS pass in entity query, not entity manager.
|
|
var state = (EntityManager, trees);
|
|
|
|
_mapSystem.FindGridsIntersecting(mapId, worldAABB, ref state,
|
|
(EntityUid uid, MapGridComponent grid,
|
|
ref (EntityManager EntityManager, ValueList<(EntityUid, TTreeComp)> trees) tuple) =>
|
|
{
|
|
if (tuple.EntityManager.TryGetComponent<TTreeComp>(uid, out var treeComp))
|
|
{
|
|
tuple.trees.Add((uid, treeComp));
|
|
}
|
|
|
|
return true;
|
|
}, includeMap: false);
|
|
|
|
if (_mapSystem.TryGetMap(mapId, out var mapUid)
|
|
&& TryComp(mapUid, out TTreeComp? mapTreeComp)
|
|
&& mapTreeComp.Tree.Count != 0) // TODO LOOKUPS why does space have an occluder tree?
|
|
{
|
|
state.trees.Add((mapUid.Value, mapTreeComp));
|
|
}
|
|
|
|
return state.trees;
|
|
}
|
|
|
|
#region HashSet
|
|
|
|
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2 worldBounds, bool approx = true)
|
|
=> QueryAabb(mapId, new Box2Rotated(worldBounds, default, default), approx);
|
|
|
|
public void QueryAabb(HashSet<Entity<TComp, TransformComponent>> results, MapId mapId, Box2 worldBounds, bool approx = true)
|
|
=> QueryAabb(results, 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>>();
|
|
QueryAabb(state, mapId, worldBounds, approx);
|
|
|
|
return state;
|
|
}
|
|
|
|
[Obsolete("Use Entity<T> variant")]
|
|
internal void QueryAabb(
|
|
HashSet<ComponentTreeEntry<TComp>> results,
|
|
MapId mapId,
|
|
Box2Rotated worldBounds,
|
|
bool approx = true)
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
|
|
{
|
|
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
|
|
|
|
treeComp.Tree.QueryAabb(ref results,
|
|
static (ref HashSet<ComponentTreeEntry<TComp>> state, in ComponentTreeEntry<TComp> value) =>
|
|
{
|
|
state.Add(value);
|
|
return true;
|
|
},
|
|
bounds,
|
|
approx);
|
|
}
|
|
}
|
|
|
|
public void QueryAabb(
|
|
HashSet<Entity<TComp, TransformComponent>> results,
|
|
MapId mapId,
|
|
Box2Rotated worldBounds,
|
|
bool approx = true)
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
|
|
{
|
|
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
|
|
|
|
treeComp.Tree.QueryAabb(ref results,
|
|
static (ref HashSet<Entity<TComp, TransformComponent>> state, in ComponentTreeEntry<TComp> value) =>
|
|
{
|
|
state.Add(value);
|
|
return true;
|
|
},
|
|
bounds,
|
|
approx);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region List
|
|
|
|
public void QueryAabb(List<Entity<TComp, TransformComponent>> results, MapId mapId, Box2 worldBounds, bool approx = true)
|
|
=> QueryAabb(results, mapId, new Box2Rotated(worldBounds, default, default), approx);
|
|
|
|
public void QueryAabb(
|
|
List<Entity<TComp, TransformComponent>> results,
|
|
MapId mapId,
|
|
Box2Rotated worldBounds,
|
|
bool approx = true)
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
|
|
{
|
|
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
|
|
|
|
treeComp.Tree.QueryAabb(ref results,
|
|
static (ref List<Entity<TComp, TransformComponent>> state, in ComponentTreeEntry<TComp> value) =>
|
|
{
|
|
state.Add(value);
|
|
return true;
|
|
},
|
|
bounds,
|
|
approx);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
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)
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
|
|
{
|
|
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
|
|
treeComp.Tree.QueryAabb(ref state, callback, bounds, approx);
|
|
}
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region Rays
|
|
|
|
[Obsolete("use IntersectRay")]
|
|
public List<RayCastResults> IntersectRayWithPredicate<TState>(MapId mapId, in Ray ray, float maxLength,
|
|
TState state, Func<EntityUid, TState, bool> predicate, bool returnOnFirstHit = true)
|
|
{
|
|
var list = new List<RayCastResults>();
|
|
|
|
if (!returnOnFirstHit)
|
|
{
|
|
IntersectRay(list, mapId, ray, maxLength, state, (e, s) => predicate(e.Owner, s));
|
|
return list;
|
|
}
|
|
|
|
var result = IntersectRay(mapId, ray, maxLength, state, (e, s) => predicate(e.Owner, s));
|
|
if (result != null)
|
|
list.Add(result.Value);
|
|
return list;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a ray intersection and return on the first hit.
|
|
/// </summary>
|
|
public RayCastResults? IntersectRay(MapId mapId, in Ray ray, float length)
|
|
{
|
|
var state = new QueryState(length);
|
|
IntersectRayInternal(mapId, in ray, length, ref state, QueryCallback);
|
|
return state.Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a ray intersection and populate a provided list of results.
|
|
/// </summary>
|
|
public void IntersectRay(List<RayCastResults> results, MapId mapId, in Ray ray, float maxLength)
|
|
{
|
|
results.Clear();
|
|
var state = new QueryState(maxLength, results);
|
|
IntersectRayInternal(mapId, in ray, maxLength, ref state, QueryCallback);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a ray intersection with a predicate and return on the first hit.
|
|
/// </summary>
|
|
public RayCastResults? IntersectRay<TState>(
|
|
MapId mapId,
|
|
in Ray ray,
|
|
float length,
|
|
TState predicateState,
|
|
Func<Entity<TComp, TransformComponent>, TState, bool> ignore)
|
|
{
|
|
var state = new QueryState<TState>(new(length), predicateState, ignore);
|
|
IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback);
|
|
return state.Inner.Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a ray intersection with a predicate and populate a provided list of results.
|
|
/// </summary>
|
|
public void IntersectRay<TState>(
|
|
List<RayCastResults> results,
|
|
MapId mapId,
|
|
in Ray ray,
|
|
float length,
|
|
TState predicateState,
|
|
Func<Entity<TComp, TransformComponent>, TState, bool> ignore)
|
|
{
|
|
var state = new QueryState<TState>(new(length, results), predicateState, ignore);
|
|
IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback);
|
|
}
|
|
|
|
private void IntersectRayInternal<TState>(
|
|
MapId mapId,
|
|
in Ray ray,
|
|
float maxLength,
|
|
ref TState state,
|
|
DynamicTree<ComponentTreeEntry<TComp>>.RayQueryCallbackDelegate<TState> callback)
|
|
where TState : IDone
|
|
{
|
|
if (mapId == MapId.Nullspace)
|
|
return;
|
|
|
|
if (!CheckEnabled())
|
|
return;
|
|
var endPoint = ray.Position + ray.Direction * maxLength;
|
|
var worldBox = new Box2(Vector2.Min(ray.Position, endPoint), Vector2.Max(ray.Position, endPoint));
|
|
|
|
foreach (var (treeUid, comp) in GetIntersectingTrees(mapId, worldBox))
|
|
{
|
|
var (_, treeRot, matrix) = XformSystem.GetWorldPositionRotationInvMatrix(treeUid);
|
|
var relativeAngle = new Angle(-treeRot.Theta).RotateVec(ray.Direction);
|
|
var treeRay = new Ray(Vector2.Transform(ray.Position, matrix), relativeAngle);
|
|
comp.Tree.QueryRay(ref state, callback, treeRay);
|
|
if (state.Done)
|
|
return;
|
|
}
|
|
}
|
|
|
|
static bool QueryCallback(
|
|
ref QueryState state,
|
|
in ComponentTreeEntry<TComp> value,
|
|
in Vector2 point,
|
|
float dist)
|
|
{
|
|
if (dist > state.MaxLength)
|
|
return true;
|
|
|
|
if (state.ReturnOnFirstHit)
|
|
{
|
|
state.Result = new RayCastResults(dist, point, value.Uid);
|
|
return false;
|
|
}
|
|
|
|
state.List.Add(new RayCastResults(dist, point, value.Uid));
|
|
return true;
|
|
}
|
|
|
|
private static bool PredicateQueryCallback<TState>(
|
|
ref QueryState<TState> state,
|
|
in ComponentTreeEntry<TComp> value,
|
|
in Vector2 point,
|
|
float dist)
|
|
{
|
|
if (dist > state.Inner.MaxLength)
|
|
return true;
|
|
|
|
if (state.Ignore.Invoke(value, state.PredicateState))
|
|
return true;
|
|
|
|
if (state.Inner.ReturnOnFirstHit)
|
|
{
|
|
state.Inner.Result = new RayCastResults(dist, point, value.Uid);
|
|
return false;
|
|
}
|
|
|
|
state.Inner.List.Add(new RayCastResults(dist, point, value.Uid));
|
|
return true;
|
|
}
|
|
|
|
private struct QueryState<TPredicateState>(
|
|
QueryState inner,
|
|
TPredicateState predicateState,
|
|
Func<Entity<TComp, TransformComponent>, TPredicateState, bool> ignore) : IDone
|
|
{
|
|
public readonly TPredicateState PredicateState = predicateState;
|
|
public readonly Func<Entity<TComp, TransformComponent>, TPredicateState, bool> Ignore = ignore;
|
|
public QueryState Inner = inner;
|
|
public bool Done => Inner.Done;
|
|
}
|
|
|
|
private struct QueryState(float maxLength, List<RayCastResults>? list = null) : IDone
|
|
{
|
|
public readonly float MaxLength = maxLength;
|
|
|
|
[MemberNotNullWhen(false, nameof(List))]
|
|
public readonly bool ReturnOnFirstHit => List == null;
|
|
public readonly List<RayCastResults>? List = list;
|
|
public RayCastResults? Result;
|
|
public bool Done => Result != null;
|
|
}
|
|
|
|
private interface IDone
|
|
{
|
|
bool Done { get; }
|
|
}
|
|
|
|
#endregion
|
|
}
|