canto/Assets/Scripts/Interaction/Interactor.cs

194 lines
No EOL
6.1 KiB
C#

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 GameObject root;
public GameObject Root => root;
[SerializeField]
private new Collider collider;
[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;
[SerializeField]
private Color outlineColor = Color.orangeRed;
[SerializeField]
private int outlineWidth = 3;
[SerializeField]
private Outline.Mode outlineMode = Outline.Mode.OutlineAll;
private readonly ObservableHashSet<IInteractable> interactables = new ObservableHashSet<IInteractable>();
public ReactiveProperty<IInteractable> CurrentHighlightedInteractable { get; private set; }
private Vector3 lastPosition;
public new T GetComponent<T>()
{
return Root.GetComponent<T>();
}
public new bool TryGetComponent<T>(out T component)
{
return root.TryGetComponent<T>(out component);
}
void Reset()
{
root = gameObject;
collider = GetComponent<Collider>();
}
private void Awake()
{
CurrentHighlightedInteractable = new ReactiveProperty<IInteractable>(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<Unit> InteractablesChanged()
{
return interactables.ObserveChanged().Select(_ => Unit.Default);
}
private Observable<Unit> 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; }
if (!interactable.TryGetComponent(out Outline outline))
{
outline = interactable.gameObject.AddComponent<Outline>();
outline.OutlineColor = outlineColor;
outline.OutlineWidth = outlineWidth;
outline.OutlineMode = outlineMode;
}
outline.enabled = true;
}
private void UnhighlightInteractable(IInteractable interactable)
{
if (interactable == null) { return; }
if (interactable.TryGetComponent(out Outline outline))
{
outline.enabled = false;
}
}
}
}