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> hitComparer = new( () => FComparer.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 ids = new(); public ReadOnlyMemory 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 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(); 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 ); } } }