using UnityEngine; using System.Linq; using R3; using R3.Triggers; using ObservableCollections; using KitsuneCafe.Interaction; using KitsuneCafe.Extension; using KitsuneCafe.SOAP; namespace KitsuneCafe.Player { public class Interactor : MonoBehaviour, IInteractor { [SerializeField] private new Collider collider; [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; [SerializeField] private Color outlineColor = Color.orangeRed; [SerializeField] private int outlineWidth = 3; private readonly ObservableHashSet interactables = new ObservableHashSet(); public ReactiveProperty CurrentHighlightedInteractable { get; private set; } private Vector3 lastPosition; void Reset() { collider = GetComponent(); } private void Awake() { CurrentHighlightedInteractable = new ReactiveProperty(null); lastPosition = transform.position; sqrtMovementDelta = Mathf.Sqrt(minimumChangeDelta); var d = Disposable.CreateBuilder(); collider.OnTriggerEnterAsObservable() .Subscribe(other => { if (other.TryGetComponent(out IInteractable interactable)) { interactables.Add(interactable); } }) .AddTo(ref d); collider.OnTriggerExitAsObservable() .Subscribe(other => { if (other.TryGetComponent(out IInteractable interactable)) { interactables.Remove(interactable); } }) .AddTo(ref d); Observable.Merge(InteractablesChanged(), PlayerMoved()) .ThrottleFirstFrame(updateFrequency) .DefaultIfEmpty() .Select(_ => { var closest = interactables .Where(x => x.IsInteractable && x.gameObject != null) .MinBy(x => Vector3.SqrMagnitude(gameObject.transform.position - x.gameObject.transform.position)); return closest; }) .DistinctUntilChanged() .Subscribe(highlighted => { CurrentHighlightedInteractable.Value = highlighted; }) .AddTo(ref d); interactables.ObserveChanged() .ThrottleFirstFrame(updateFrequency, UnityFrameProvider.FixedUpdate) .Subscribe(_ => { var highlighted = interactables .Where(x => x.IsInteractable) .MinBy(x => gameObject.transform.position.SqrDistance(x.gameObject.transform.position)); if (CurrentHighlightedInteractable.Value != highlighted) { CurrentHighlightedInteractable.Value = highlighted; } }) .AddTo(ref d); var highlightedInteractable = CurrentHighlightedInteractable .Scan((prev, cur) => { UnhighlightInteractable(prev); HighlightInteractable(cur); return cur; }); interactSource .AsObservable() .WithLatestFrom(highlightedInteractable, (_, highlighted) => highlighted) .WhereNotNull() .Subscribe(interactable => { var result = interactable.Interact(this); if (result.IsOk) { interactables.Remove(interactable); } else { Debug.Log($"interaction failed: {result}"); } }) .AddTo(ref d); d.RegisterTo(destroyCancellationToken); } private Observable InteractablesChanged() { return interactables.ObserveChanged().Select(_ => Unit.Default); } private Observable PlayerMoved() { return Observable.EveryUpdate(UnityFrameProvider.FixedUpdate) .Select(_ => transform.position) .Where(pos => pos.SqrDistance(lastPosition) > sqrtMovementDelta) .Do(pos => lastPosition = pos) .Select(_ => Unit.Default); } private void HighlightInteractable(IInteractable interactable) { if (interactable == null) { return; } Outline outline; if (!interactable.TryGetComponent(out outline)) { outline = interactable.gameObject.AddComponent(); outline.OutlineColor = outlineColor; outline.OutlineWidth = outlineWidth; outline.OutlineMode = Outline.Mode.OutlineVisible; } outline.enabled = true; } private void UnhighlightInteractable(IInteractable interactable) { if (interactable == null) { return; } if (interactable.TryGetComponent(out Outline outline)) { outline.enabled = false; } } } }