181 lines
5 KiB
C#
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
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|