canto/Assets/Scripts/Entity/Raycaster.cs
2025-10-02 15:28:03 -04:00

240 lines
6.2 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using KitsuneCafe.Sys.Attributes;
using R3;
using UnityEngine;
using static KitsuneCafe.Extension.R3Extensions;
namespace KitsuneCafe.Sys
{
public class Raycaster : MonoBehaviour, INotifyPropertyChanged
{
private static readonly Lazy<IComparer<RaycastHit>> hitComparer = new(
() => FComparer<RaycastHit>.Create((a, b) => a.distance.CompareTo(b.distance))
);
[SerializeField]
private bool everyFrame = true;
[SerializeField, DrawIf(nameof(everyFrame), true)]
private PlayerLoopTiming updateTiming = PlayerLoopTiming.FixedUpdate;
[SerializeField]
private Vector3 direction = Vector3.forward;
public Vector3 Direction => DirectionFrom(directionSpace, direction, transform);
[SerializeField]
private Space directionSpace = Space.World;
[SerializeField]
private float maxDistance = 1f;
public float MaxDistance => maxDistance;
[SerializeField]
private LayerMask layerMask = default;
public LayerMask LayerMask => LayerMask;
[SerializeField]
private QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal;
public QueryTriggerInteraction QueryTriggerInteraction => queryTriggerInteraction;
[SerializeField]
private int bufferSize = 1;
public int BufferSize => bufferSize;
private int hitCount = 0;
public int HitCount
{
get => hitCount;
private set
{
if (hitCount != value)
{
var isCollidingChanged = value == 0 || hitCount == 0;
hitCount = value;
Notify();
if (isCollidingChanged)
{
Notify(nameof(IsColliding));
}
}
}
}
public bool IsColliding => hitCount > 0;
private RaycastHit[] hits;
private HashSet<int> ids = new();
public ReadOnlyMemory<RaycastHit> Collisions => new(hits, 0, hitCount);
public event PropertyChangedEventHandler PropertyChanged;
private CancellationTokenSource disableCancellationTokenSource;
private CancellationToken disableCancellationToken => disableCancellationTokenSource.Token;
private void Reset()
{
layerMask = LayerMask.NameToLayer("Default");
}
private void OnEnable()
{
disableCancellationTokenSource = new();
hits = new RaycastHit[bufferSize];
if (everyFrame)
{
var source = bufferSize > 1 ?
RaycastMany() :
RaycastSingle();
source.RegisterTo(disableCancellationToken);
}
}
private IDisposable RaycastMany() =>
Observable.EveryUpdate(updateTiming.GetFrameProvider())
.WithLatestFrom(DirectionStreamFrom(directionSpace, direction, transform), (_, dir) => dir)
.Subscribe(dir => RaycastAll(transform.position, dir));
private IDisposable RaycastSingle() =>
Observable.EveryUpdate(updateTiming.GetFrameProvider())
.WithLatestFrom(DirectionStreamFrom(directionSpace, direction, transform), (_, dir) => dir)
.Subscribe(dir => hitCount = Raycast(transform.position, dir, out hits[0]) ? 1 : 0);
public bool TryGetClosestHit(out RaycastHit hit)
{
if (hitCount > 0)
{
SortRaycastHits();
hit = bufferSize switch
{
> 1 => hits[0],
_ => default
};
return true;
}
hit = default;
return false;
}
private void SortRaycastHits()
{
Array.Sort(hits, hitComparer.Value);
}
private static Observable<Vector3> DirectionStreamFrom(Space space, Vector3 direction, Transform target) =>
space switch
{
Space.World => Observable.Return(direction),
Space.Self => Observable.EveryUpdate(UnityFrameProvider.FixedUpdate)
.Select(_ => target.TransformDirection(direction)),
_ => throw new ArgumentOutOfRangeException(space.ToString()),
};
private static Vector3 DirectionFrom(Space space, Vector3 direction, Transform target) =>
space switch
{
Space.World => direction,
Space.Self => target.TransformDirection(direction),
_ => throw new ArgumentOutOfRangeException(space.ToString()),
};
private void OnDisable()
{
disableCancellationTokenSource.Cancel();
}
public bool Raycast(out RaycastHit hit) => Raycast(
DirectionFrom(directionSpace, direction, transform),
out hit
);
public bool Raycast(Vector3 direction, out RaycastHit hit) => Raycast(
transform.position,
direction,
out hit
);
private bool Raycast(Vector3 position, Vector3 direction, out RaycastHit hit) =>
Physics.Raycast(
position,
direction,
out hit,
maxDistance,
layerMask,
queryTriggerInteraction
);
public bool RaycastAll(Vector3 direction) =>
RaycastAll(
transform.position,
direction
);
private bool RaycastAll(Vector3 position, Vector3 direction)
{
HitCount = RaycastAll(position, direction, hits);
NotifyChanges();
return hitCount > 0;
}
private void NotifyChanges()
{
var set = new HashSet<int>();
var notified = false;
foreach (var hit in Collisions.Span)
{
set.Add(hit.colliderInstanceID);
if (!notified && !ids.Contains(hit.colliderInstanceID))
{
notified = true;
Notify(nameof(Collisions));
}
}
ids.Clear();
ids.UnionWith(set);
}
private int RaycastAll(Vector3 position, Vector3 direction, RaycastHit[] hits) =>
Physics.RaycastNonAlloc(
position,
direction,
hits,
maxDistance,
layerMask,
queryTriggerInteraction
);
private void Notify([CallerMemberName] string name = default)
{
PropertyChanged?.Invoke(this, new(name));
}
private void OnDrawGizmos()
{
Gizmos.color = IsColliding ? Color.green : Color.yellow;
Gizmos.DrawRay(
transform.position,
DirectionFrom(
directionSpace,
direction,
transform
) * maxDistance
);
}
}
}