canto/Assets/Scripts/Interaction/Interactor.cs
2025-08-06 12:51:11 -04:00

148 lines
4.7 KiB
C#

using System.Linq;
using KitsuneCafe.Extension;
using KitsuneCafe.Interaction;
using KitsuneCafe.SOAP;
using ObservableCollections;
using R3;
using R3.Triggers;
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 ReactiveValue<GameObject> selectedObject;
[SerializeField]
private ReactiveEvent<float> interactSource;
// [SerializeField]
// private int updateFrequency = 10;
[SerializeField, Tooltip("How far an object must move before being considered")]
private float minimumChangeDelta = 0.1f;
private float sqrtMovementDelta;
public new T GetComponent<T>()
{
return Root.GetComponent<T>();
}
public new bool TryGetComponent<T>(out T component)
{
return root.TryGetComponent<T>(out component);
}
private void OnValidate()
{
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
.AsObservable()
.WithLatestFrom(selectedObjects, (_, highlighted) => highlighted)
.WhereNotNull()
.Subscribe(interactable =>
{
var result = interactable.Interact(this);
if (result.IsOk)
{
activeInteractables.Remove(interactable);
}
else
{
Debug.Log($"interaction failed: {result}");
}
})
.AddTo(ref d);
d.RegisterTo(destroyCancellationToken);
}
private ObservableHashSet<T> TrackCollisions<T>(Collider collider, ref DisposableBuilder d)
{
var interactables = new ObservableHashSet<T>();
collider.OnTriggerEnterAsObservable()
.Subscribe(other =>
{
if (other.TryGetComponent(out T interactable))
{
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)
{
return Observable.CombineLatest(
interactables.ObserveChanged(),
positionSource,
(_, position) => Nearest(position, interactables)
);
}
private IInteractable Nearest(Vector3 origin, IObservableCollection<IInteractable> interactables)
{
return 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;
}
}
}