canto/Assets/Scripts/Entity/AirMotor.cs

181 lines
5 KiB
C#

using System;
using System.ComponentModel;
using System.Threading;
using KitsuneCafe.Extension;
using KitsuneCafe.Sys;
using R3;
using Unity.AppUI.MVVM;
using Unity.Properties;
using UnityEngine;
namespace KitsuneCafe.Entities
{
public enum LandingForce : byte
{
None,
Soft,
Hard
}
[ObservableObject]
public partial class AirMotor : MonoBehaviour, INotifyPropertyChanged
{
[Header("Dependencies")]
[SerializeField]
private new Rigidbody rigidbody;
[SerializeField]
private Spring spring;
[SerializeField]
private Motor motor;
[Space]
[SerializeField]
private Duration landingPredictionTime = TimeSpan.FromSeconds(0.1);
[SerializeField]
private Duration landingRecoveryTime = TimeSpan.FromSeconds(1);
[SerializeField]
private Duration hardLandingRecoveryTime = TimeSpan.FromSeconds(2);
[SerializeField]
private float hardLandingThreshold = Physics.gravity.y;
[SerializeField]
private LayerMask layerMask;
[ObservableProperty]
[AlsoNotifyChangeFor(nameof(IsFalling))]
private bool isGrounded = false;
[CreateProperty(ReadOnly = true)]
public bool IsFalling => !IsGrounded && !IsLanding;
[ObservableProperty]
[AlsoNotifyChangeFor(nameof(IsFalling))]
private bool isLanding = false;
public event EventHandler<LandingForce> Landed = delegate { };
private CancellationTokenSource disableCancellationSource;
private void OnValidate()
{
this.TryGetComponentIfNull(ref rigidbody);
this.TryGetComponentIfNull(ref spring);
this.TryGetComponentIfNull(ref motor);
}
private void OnEnable()
{
disableCancellationSource = new();
var d = Disposable.CreateBuilder();
var groundedSource = spring.ObservePropertyChanged(x => x.IsColliding);
var willLandSource = groundedSource.SwitchIfElse(
Observable.Empty<(bool, LandingForce)>(),
Observable.EveryUpdate(UnityFrameProvider.FixedUpdate)
.Select(_ => WillLandWithin(landingPredictionTime, Time.fixedDeltaTime))
)
.DistinctUntilChanged();
var landingSource = ObserveLanding(willLandSource);
willLandSource.Where(v => v.Item1).Subscribe(v => OnLanding(v.Item2)).AddTo(ref d);
groundedSource.Subscribe(grounded => IsGrounded = grounded).AddTo(ref d);
landingSource.Subscribe(landing =>
{
motor.CanMove = !landing;
IsLanding = landing;
}).AddTo(ref d);
d.RegisterTo(disableCancellationSource.Token);
}
private void OnDisable()
{
disableCancellationSource.Cancel();
}
private (bool, LandingForce) WillLandWithin(TimeSpan timeSpan, float deltaTime)
{
var currentPosition = rigidbody.position;
var velocity = rigidbody.linearVelocity;
for (float i = 0; i < timeSpan.TotalSeconds; i += deltaTime)
{
velocity += Physics.gravity * deltaTime;
var nextPosition = currentPosition + velocity * deltaTime;
if (Raycast(currentPosition, nextPosition - currentPosition, out var hit))
{
return (true, GetLandingForce(velocity));
}
currentPosition = nextPosition;
}
return (false, LandingForce.None);
}
private bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hit)
{
return Physics.Raycast(origin, direction.normalized, out hit, direction.magnitude, layerMask);
}
private void OnLanding(LandingForce force)
{
Landed?.Invoke(this, force);
}
private Observable<bool> ObserveLanding(Observable<(bool, LandingForce)> landingSource)
{
return Observable.Merge(
landingSource.Select(v => v.Item1).WhereTrue(),
landingSource.Select(v => GetLandingRecoveryTime(v.Item2))
.SelectSwitch(duration => Observable.Timer(duration, UnityTimeProvider.FixedUpdate))
.Select(_ => false)
);
}
private LandingForce GetLandingForce(Vector3 velocity)
{
return (velocity.y <= hardLandingThreshold) switch
{
true => LandingForce.Hard,
_ => LandingForce.Soft
};
}
private Duration GetLandingRecoveryTime(LandingForce force)
{
return force switch
{
LandingForce.Soft => landingRecoveryTime,
LandingForce.Hard => hardLandingRecoveryTime,
_ => TimeSpan.Zero
};
}
private Observable<LandingForce> ObserveLandingForce(Observable<bool> groundedSource)
{
var landingForceSource = Observable.Merge(
Observable.Return(LandingForce.Soft),
Observable.EveryUpdate(UnityFrameProvider.FixedUpdate)
.SkipWhile(_ => rigidbody.linearVelocity.y > hardLandingThreshold)
.Select(_ => LandingForce.Hard)
.Take(1)
);
return groundedSource.WhereFalse()
.SelectSwitch(_ =>
Observable.ZipLatest(
groundedSource.WhereTrue(),
landingForceSource,
(_, landing) => landing
)
);
}
}
}