canto/Assets/Scripts/Entity/Motor.cs
2025-10-02 15:28:03 -04:00

233 lines
6.8 KiB
C#

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<float> currentMaxSpeed = new();
public float MaxSpeed => currentMaxSpeed.Value;
public ReactiveProperty<float> 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<Rigidbody>();
}
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<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()
{
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<Quaternion> ObserveRotation(Observable<Vector3> directionSource, FrameProvider provider) =>
ObserveRotation(directionSource, transform, provider);
private static Observable<Quaternion> ObserveRotation(Observable<Vector3> 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<Vector3> ObserveVelocity(Observable<Vector3> directionSource, Observable<float> 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<Vector3> 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));
}
}
}