148 lines
4.7 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|