canto/Assets/Scripts/System/Timer.cs

182 lines
4.4 KiB
C#

using System;
using System.Threading;
using KitsuneCafe.Extension;
using R3;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;
using static KitsuneCafe.Extension.R3Extensions;
using TimeUnit = KitsuneCafe.System.TimeUnit;
namespace KitsuneCafe
{
public class Timer : MonoBehaviour
{
[SerializeField]
private float duration = 1;
public float Duration
{
get => duration;
set => duration = value;
}
[SerializeField]
private TimeUnit unit = TimeUnit.Seconds;
public TimeUnit Unit
{
get => unit;
set => unit = value;
}
[SerializeField]
private PlayerLoopTiming timing = PlayerLoopTiming.Update;
[SerializeField]
private TimeKind timeKind = TimeKind.Time;
[SerializeField]
private bool autostart = false;
[SerializeField]
private bool oneShot = false;
[SerializeField]
private UnityEvent onComplete = default;
private readonly ReactiveProperty<bool> started = new(false);
private readonly ReactiveProperty<bool> paused = new(false);
public bool Started => started.Value;
public bool Paused => paused.Value;
public IDisposable source;
public event EventHandler Completed;
private void Awake()
{
SubscribeToTimer();
}
private void SubscribeToTimer()
{
source?.Dispose();
var counting = Observable.Merge(
started.Compose(WhereTrue).Select(_ => true),
paused.Compose(WhereTrue).Select(_ => false)
);
var stopped = started.Where(KMath.Not).Skip(1);
var timeProvider = GetTimeProvider(timing, timeKind);
var totalDuration = GetDuration();
source = CreateTimer(GetDuration(), counting, timeProvider)
.TakeUntil(stopped)
// add onNext handler if remaining time is required
.Do(onCompleted: OnCompleted)
.Subscribe()
.AddTo(this);
}
private void Start()
{
if (autostart)
{
StartTimer();
}
}
private void OnEnable()
{
if (Started && Paused)
{
Resume();
}
}
private void OnDisable()
{
if (Started)
{
Pause();
}
}
public void StartTimer()
{
if (source == null)
{
SubscribeToTimer();
}
paused.Value = false;
started.Value = true;
}
public void StopTimer()
{
source?.Dispose();
source = null;
paused.Value = false;
started.Value = false;
}
public void Pause()
{
paused.Value = true;
}
public void Resume()
{
paused.Value = false;
}
private void OnCompleted(Result result)
{
StopTimer();
if (result.IsSuccess)
{
Finish();
if (!oneShot)
{
StartTimer();
}
}
}
private void Finish()
{
Completed?.Invoke(this, EventArgs.Empty);
onComplete.Invoke();
}
private Observable<TimeSpan> CreateTimer(TimeSpan duration, Observable<bool> isCounting, TimeProvider timeProvider)
{
return Observable.Empty<Unit>()
.SwitchIf(isCounting, Observable.EveryUpdate(timeProvider.GetFrameProvider()))
.Select(_ => DeltaTime(timeProvider))
.Scan(duration, (acc, dt) => acc.Subtract(dt))
.TakeWhile(ts => ts > TimeSpan.Zero)
.DefaultIfEmpty(duration);
}
private Observable<Unit> WhereTrue(Observable<bool> source)
{
return source.WhereTrue().AsUnitObservable();
}
private TimeSpan GetDuration()
{
return System.Duration.From(duration, unit);
}
private TimeSpan DeltaTime(TimeProvider timeProvider)
{
return TimeSpan.FromSeconds(timeProvider.GetDeltaTime());
}
}
}