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 selectedObject; [SerializeField] private ReactiveEvent 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() { return Root.GetComponent(); } public new bool TryGetComponent(out T component) { return root.TryGetComponent(out component); } private void OnValidate() { root = gameObject; collider = GetComponent(); } private void Awake() { selectedObject.Value = null; sqrtMovementDelta = Mathf.Sqrt(minimumChangeDelta); var d = Disposable.CreateBuilder(); var activeInteractables = TrackCollisions(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(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 TrackCollisions(Collider collider, ref DisposableBuilder d) { var interactables = new ObservableHashSet(); 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 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 ObserveNearestInteractable(Observable positionSource, IObservableCollection interactables) { return Observable.CombineLatest( interactables.ObserveChanged(), positionSource, (_, position) => Nearest(position, interactables) ); } private IInteractable Nearest(Vector3 origin, IObservableCollection 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; } } }