using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading; using KitsuneCafe.Extension; using R3; using UnityAtoms.BaseAtoms; using UnityEngine; namespace KitsuneCafe.Entities { public interface IMotor { Vector3 LinearVelocity { get; } bool Move(Vector3 direction); }; public class Motor : MonoBehaviour, IMotor, INotifyPropertyChanged { [Header("Dependencies")] [SerializeField] private new Rigidbody rigidbody; [Header("Movement")] [SerializeField] private Vector2Reference 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; private readonly ReactiveProperty currentMaxSpeed = new(); public float MaxSpeed => currentMaxSpeed.Value; public ReactiveProperty MaxSpeedSource => currentMaxSpeed; public Vector3 LinearVelocity => rigidbody.linearVelocity; 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 Reset() { rigidbody = GetComponent(); } private void OnEnable() { disableCancellationSource = new(); var d = Disposable.CreateBuilder(); currentMaxSpeed.Value = maxSpeed; var directionSource = direction.ObserveChange().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.Empty()) .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() { StopMoving(); 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) { rigidbody.MoveRotation( Quaternion.Slerp( transform.rotation, rotation, Time.fixedDeltaTime * rotationSpeed ) ); } private Observable ObserveRotation(Observable directionSource, FrameProvider provider) => ObserveRotation(directionSource, transform, provider); private static Observable ObserveRotation(Observable directionSource, Transform transform, FrameProvider provider) => Observable.EveryUpdate(provider) .WithLatestFrom( directionSource .Select(dir => dir.IsZero() ? transform.rotation : Quaternion.LookRotation(dir.normalized) ) .DistinctUntilChanged(), (_, rotation) => rotation ); private Observable ObserveVelocity(Observable directionSource, Observable maxSpeedSource, FrameProvider provider) => directionSource .DistinctUntilChanged() .CombineLatest(maxSpeedSource, (direction, currentMaxSpeed) => (direction, currentMaxSpeed)) .Select(data => { var (direction, maxSpeed) = data; var start = rigidbody.linearVelocity; var targetMagnitude = !direction.IsZero() ? maxSpeed : 0f; var targetVelocity = direction.normalized * targetMagnitude; if (start.SqrDistance(targetVelocity).IsZero()) { return start.IsZero() && targetVelocity.IsZero() ? Observable.Return(Vector3.zero) : Observable.Return((targetVelocity - start) / Time.fixedDeltaTime); } float lerpDuration; AnimationCurve curve; if (targetMagnitude > float.Epsilon) { var ratio = start.magnitude / maxSpeed; lerpDuration = Mathf.Max(accelerationTime * (1f - ratio), 0.05f); curve = accelerationCurve; } else { lerpDuration = decelerationTime; curve = decelerationCurve; } return Interpolate(start, targetVelocity, lerpDuration, provider, curve) .Select(targetVelocity => (targetVelocity - rigidbody.linearVelocity) / Time.fixedDeltaTime ) .Where(_ => !targetVelocity.IsZero() || !rigidbody.linearVelocity.IsZero()) .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) => Observable.EveryUpdate(provider) .Scan(0f, (elapsedTime, _) => elapsedTime + Time.fixedDeltaTime) .Select(elapsedTime => { var factor = duration > float.Epsilon ? Mathf.Clamp01(elapsedTime / duration) : 1f; return Vector3.Lerp(start, target, curve.Evaluate(factor)); }); public bool Move(Vector2 direction) { this.direction.Value = direction; return true; } public bool Move(Vector3 direction) { this.direction.Value = new Vector3(direction.x, 0, direction.z); return true; } 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)); } } }