canto/Assets/Scripts/Entity/Motor.cs

246 lines
7.2 KiB
C#

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<Vector2> 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<float> currentMaxSpeed = new();
public float MaxSpeed => currentMaxSpeed.Value;
public ReactiveProperty<float> 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<Rigidbody>();
}
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<Vector3>())
.Subscribe(Accelerate)
.AddTo(ref d);
canMove
.SwitchIfElse(rotationSource, Observable.Empty<Quaternion>())
.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<Quaternion> ObserveRotation(Observable<Vector3> 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<Vector3> ObserveVelocity(Observable<Vector3> directionSource, Observable<float> 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<Vector3> 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));
}
}
}