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.Sys.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 started = new(false); private readonly ReactiveProperty 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 CreateTimer(TimeSpan duration, Observable isCounting, TimeProvider timeProvider) { return Observable.Empty() .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 WhereTrue(Observable source) { return source.WhereTrue().AsUnitObservable(); } private TimeSpan GetDuration() { return Sys.Duration.From(duration, unit); } private TimeSpan DeltaTime(TimeProvider timeProvider) { return TimeSpan.FromSeconds(timeProvider.GetDeltaTime()); } } }