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

139 lines
4.1 KiB
C#

using System.Linq;
using KitsuneCafe.Extension;
using KitsuneCafe.Interaction;
using ObservableCollections;
using R3;
using R3.Triggers;
using UnityAtoms.BaseAtoms;
using UnityEngine;
namespace KitsuneCafe.Player
{
public class Interactor : MonoBehaviour, IInteractor
{
[SerializeField]
private GameObject root;
public GameObject Root => root;
[SerializeField]
private new Collider collider;
[SerializeField]
private GameObjectReference selectedObject;
[SerializeField]
private FloatEvent interactSource;
[SerializeField, Tooltip("How far an object must move before being considered")]
private float minimumChangeDelta = 0.1f;
private float sqrtMovementDelta;
public new T GetComponent<T>() =>
Root.GetComponent<T>();
public new bool TryGetComponent<T>(out T component) =>
root.TryGetComponent<T>(out component);
private void Reset()
{
root = gameObject;
collider = GetComponent<Collider>();
}
private void Awake()
{
selectedObject.Value = null;
sqrtMovementDelta = Mathf.Sqrt(minimumChangeDelta);
var d = Disposable.CreateBuilder();
var activeInteractables = TrackCollisions<IInteractable>(collider, ref d);
var selectedObjects = ObserveNearestInteractable(
PositionChanged(transform, sqrtMovementDelta),
activeInteractables
)
.DefaultIfEmpty()
.DistinctUntilChanged()
.Do(selected => selectedObject.Value = selected?.gameObject)
.SelectOrDefault(go => go.TryGetComponent(out IInteractable i) ? i : null)
.Scan<IInteractable, IInteractable>(null, SwapSelected);
interactSource
.Observe()
.ToObservable()
.WithLatestFrom(selectedObjects, (_, highlighted) => highlighted)
.WhereNotNull()
.Subscribe(interactable =>
{
var result = interactable.Interact(this);
if (!interactable.IsInteractable)
{
activeInteractables.Remove(interactable);
}
if (!result.IsOk)
{
Debug.Log($"interaction failed: {result}");
}
})
.AddTo(ref d);
d.RegisterTo(destroyCancellationToken);
}
private ObservableHashSet<T> TrackCollisions<T>(Collider collider, ref DisposableBuilder d) where T : IInteractable
{
var interactables = new ObservableHashSet<T>();
collider.OnTriggerEnterAsObservable()
.Subscribe(other =>
{
if (other.TryGetComponent(out T interactable) && interactable.IsInteractable)
{
interactables.Add(interactable);
}
})
.AddTo(ref d);
collider.OnTriggerExitAsObservable()
.Subscribe(other =>
{
if (other.TryGetComponent(out T interactable))
{
interactables.Remove(interactable);
}
})
.AddTo(ref d);
return interactables;
}
private Observable<Vector3> PositionChanged(Transform target, float delta, FrameProvider frameProvider = null)
{
frameProvider ??= UnityFrameProvider.FixedUpdate;
return Observable.EveryValueChanged(target, t => t.position, frameProvider)
.Scan((prev, cur) => cur.SqrDistance(prev) > delta ? cur : prev);
}
private Observable<IInteractable> ObserveNearestInteractable(Observable<Vector3> positionSource, IObservableCollection<IInteractable> interactables) =>
Observable.CombineLatest(
interactables.ObserveChanged(),
positionSource,
(_, position) => Nearest(position, interactables)
);
private IInteractable Nearest(Vector3 origin, IObservableCollection<IInteractable> interactables) =>
interactables
.Where(x => x.IsInteractable)
.MinBy(x => origin.SqrDistance(x.gameObject.transform.position));
private IInteractable SwapSelected(IInteractable previous, IInteractable current)
{
previous?.Deselect(this);
current?.Select(this);
return current;
}
}
}