using System; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading; using KitsuneCafe.Extension; using KitsuneCafe.SOAP; using R3; using UnityEngine; namespace KitsuneCafe.Entities { public class Motor : MonoBehaviour, INotifyPropertyChanged { [Header("Dependencies")] [SerializeField] private new Rigidbody rigidbody; [Header("Movement")] [SerializeField] private ReactiveSource direction; [SerializeField] private float maxSpeed = 25f; public float DefaultMaxSpeed => maxSpeed; [SerializeField] private float accelerationTime = 1f; public float AccelerationTime => accelerationTime; [SerializeField] private AnimationCurve accelerationCurve = AnimationCurve.EaseInOut(0, 0, 1, 1); [SerializeField] private float decelerationTime = 0.5f; public float Decelerationtime => decelerationTime; [SerializeField] private AnimationCurve decelerationCurve = AnimationCurve.EaseInOut(0, 0, 1, 1); [SerializeField] private float rotationSpeed = 25f; public float RotationSpeed => rotationSpeed; [Space] [SerializeField] private float epsilon = 0.001f; private float sqEpsilon; private readonly ReactiveProperty currentMaxSpeed = new(); public float MaxSpeed => currentMaxSpeed.Value; public ReactiveProperty MaxSpeedSource => currentMaxSpeed; private CancellationTokenSource disableCancellationSource; public event PropertyChangedEventHandler PropertyChanged; private bool canMove = true; public bool CanMove { get => canMove; set { if (canMove != value) { canMove = value; Notify(); } } } private void OnValidate() { if (rigidbody == null) { rigidbody = GetComponent(); } sqEpsilon = epsilon * epsilon; } private void OnEnable() { disableCancellationSource = new(); var d = Disposable.CreateBuilder(); currentMaxSpeed.Value = maxSpeed; var directionSource = direction.AsObservable().Select(v2 => new Vector3(v2.x, 0, v2.y)); var velocitySource = ObserveVelocity(directionSource, currentMaxSpeed, UnityFrameProvider.FixedUpdate); var rotationSource = ObserveRotation(directionSource, UnityFrameProvider.FixedUpdate); var canMove = this.ObservePropertyChanged(x => x.CanMove).DefaultIfEmpty(CanMove); canMove.WhereFalse() .Subscribe(_ => StopMoving()) .AddTo(ref d); canMove .SwitchIfElse(velocitySource, Observable.Never()) .Subscribe(Accelerate) .AddTo(ref d); canMove .SwitchIfElse(rotationSource, Observable.Empty()) .Where(rotation => Quaternion.Angle(transform.rotation, rotation) > 0.1f) .Subscribe(ApplyRotation) .AddTo(ref d); d.RegisterTo(disableCancellationSource.Token); } private void OnDisable() { rigidbody.linearVelocity = Vector3.zero; disableCancellationSource.Cancel(); } private void StopMoving() { rigidbody.linearVelocity = new Vector3( 0, rigidbody.linearVelocity.y, 0 ); } private void Accelerate(Vector3 force) { rigidbody.AddForce(force, ForceMode.Acceleration); } private void ApplyRotation(Quaternion rotation) { transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.fixedDeltaTime * rotationSpeed); } private Observable ObserveRotation(Observable directionSource, FrameProvider provider) { var targetRotationStream = directionSource .Select(inputDir => { if (inputDir.sqrMagnitude > sqEpsilon) { return Quaternion.LookRotation(inputDir.normalized); } else { return transform.rotation; } }) .DistinctUntilChanged(); return Observable.EveryUpdate(provider) .WithLatestFrom(targetRotationStream, (_, targetRotation) => targetRotation); } private Observable ObserveVelocity(Observable directionSource, Observable maxSpeedSource, FrameProvider provider) { return directionSource .DistinctUntilChanged() .CombineLatest(maxSpeedSource, (direction, currentMaxSpeed) => (direction, currentMaxSpeed)) .Select(data => { var (direction, maxSpeed) = data; var start = rigidbody.linearVelocity; var targetMagnitude = direction.sqrMagnitude > sqEpsilon ? maxSpeed : 0f; var targetVelocity = direction.normalized * targetMagnitude; if (start.SqrDistance(targetVelocity) < sqEpsilon) { if (targetVelocity.sqrMagnitude < sqEpsilon && start.sqrMagnitude < sqEpsilon) { return Observable.Return(Vector3.zero); } else { return Observable.Return((targetVelocity - start) / Time.fixedDeltaTime); } } float lerpDuration; AnimationCurve curve; if (targetMagnitude > epsilon) { var ratio = start.magnitude / maxSpeed; lerpDuration = accelerationTime * (1f - ratio); lerpDuration = Mathf.Max(lerpDuration, 0.05f); curve = accelerationCurve; } else { lerpDuration = decelerationTime; curve = decelerationCurve; } return Interpolate(start, targetVelocity, lerpDuration, provider, curve) .Select(targetVelocity => { var currentVelocity = rigidbody.linearVelocity; return (targetVelocity - currentVelocity) / Time.fixedDeltaTime; }) .Where(_ => targetVelocity.sqrMagnitude > sqEpsilon || rigidbody.linearVelocity.sqrMagnitude > sqEpsilon) .Select(v3 => new Vector3(v3.x, 0, v3.z)) .DefaultIfEmpty(Vector3.zero); }) .Switch(); } private static Observable Interpolate(Vector3 start, Vector3 target, float duration, FrameProvider provider, AnimationCurve curve) { return Observable.EveryUpdate(provider) .Scan(0f, (elapsedTime, _) => elapsedTime + Time.fixedDeltaTime) .Select(elapsedTime => { var factor = 0f; if (duration > float.Epsilon) { factor = Mathf.Clamp01(elapsedTime / duration); } else { factor = 1f; } var evaluatedFactor = curve.Evaluate(factor); return Vector3.Lerp(start, target, evaluatedFactor); }); } public void ChangeMaxSpeed(float value) { currentMaxSpeed.Value = value; } public void ResetMaxSpeed() { currentMaxSpeed.Value = maxSpeed; } private void Notify([CallerMemberName] string name = default) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } } }