From 29ccbf872586afc8b131ef6caebef8dd1edd2218 Mon Sep 17 00:00:00 2001 From: Harvey Ball Date: Thu, 2 Jan 2025 17:03:50 +0000 Subject: [PATCH] feat(Utility): provide time source options for countdown timer The CountdownTimer component can now have different time sources for the countdown. Originally it used just the scaled time, but now it can be set to use unscaled time, fixed time or realtime. --- Runtime/Utility/CountdownTimer.cs | 148 +++++++++++++++++-- Tests/Editor/Utility/CountdownTimerTest.cs | 163 ++++++++++++++++++++- 2 files changed, 298 insertions(+), 13 deletions(-) diff --git a/Runtime/Utility/CountdownTimer.cs b/Runtime/Utility/CountdownTimer.cs index 59b668c1..8fbc2ed2 100644 --- a/Runtime/Utility/CountdownTimer.cs +++ b/Runtime/Utility/CountdownTimer.cs @@ -1,6 +1,7 @@ namespace Zinnia.Utility { using System; + using System.Collections; using UnityEngine; using UnityEngine.Events; using Zinnia.Extension; @@ -10,6 +11,33 @@ /// public class CountdownTimer : MonoBehaviour { + /// + /// The source of Time to use. + /// + public enum TimeSourceType + { + /// + /// Taken from . + /// + ScaledTime, + /// + /// Taken from . + /// + UnscaledTime, + /// + /// Taken from . + /// + FixedScaledTime, + /// + /// Taken from . + /// + FixedUnscaledTime, + /// + /// Taken from . + /// + RealTime + } + /// /// Defines the event with the specified . /// @@ -56,6 +84,25 @@ public bool BeginOnEnable beginOnEnable = value; } } + + [Tooltip("The source for the time to be used in the countdown.")] + [SerializeField] + private TimeSourceType timeSource; + /// + /// The source for the time to be used in the countdown. + /// + public TimeSourceType TimeSource + { + get + { + return timeSource; + } + set + { + timeSourceChanged = !timeSource.Equals(value); + timeSource = value; + } + } #endregion #region Timer Events @@ -116,7 +163,7 @@ public float ElapsedTime { if (IsRunning && !IsPaused) { - currentTime = Time.time; + currentTime = actualTime; } return currentTime - beginTime; } @@ -131,18 +178,18 @@ public float RemainingTime { if (IsRunning && !IsPaused) { - currentTime = Time.time; + currentTime = actualTime; } return StartTime + (beginTime - currentTime); } } /// - /// when is called. + /// when is called. /// protected float beginTime; /// - /// of the current frame. + /// of the current frame. /// protected float currentTime; @@ -151,6 +198,55 @@ public float RemainingTime /// protected float remainingAtPauseTime; + /// + /// Whether the has changed. + /// + protected bool timeSourceChanged = true; + + /// + /// The stored function for retrieving the time. + /// + protected Func selectedTimeFunction = () => Time.time; + + /// + /// The actual time value based on the selected . + /// + protected float actualTime + { + get + { + if (timeSourceChanged) + { + switch(TimeSource) + { + case TimeSourceType.ScaledTime: + selectedTimeFunction = () => Time.time; + break; + case TimeSourceType.UnscaledTime: + selectedTimeFunction = () => Time.unscaledTime; + break; + case TimeSourceType.FixedScaledTime: + selectedTimeFunction = () => Time.fixedTime; + break; + case TimeSourceType.FixedUnscaledTime: + selectedTimeFunction = () => Time.fixedUnscaledTime; + break; + case TimeSourceType.RealTime: + selectedTimeFunction = () => Time.realtimeSinceStartup; + break; + } + timeSourceChanged = false; + } + + return selectedTimeFunction(); + } + } + + /// + /// A container to hold the timer coroutine. + /// + protected Coroutine timerRoutine; + /// /// Starts the timer counting down. /// @@ -171,10 +267,11 @@ public virtual void Begin() /// public virtual void Cancel() { - CancelInvoke(nameof(Complete)); + CancelRoutine(); + //CancelInvoke(nameof(Complete)); if (IsRunning) { - currentTime = Time.time; + currentTime = actualTime; Cancelled?.Invoke(); IsRunning = false; IsPaused = false; @@ -194,7 +291,7 @@ public virtual void Pause() remainingAtPauseTime = RemainingTime; IsPaused = true; - CancelInvoke(nameof(Complete)); + CancelRoutine(); Paused?.Invoke(); } @@ -280,7 +377,36 @@ protected virtual void OnDisable() protected virtual void StartTimer(float invokeTime) { SetInternalStates(); - Invoke(nameof(Complete), invokeTime); + CancelRoutine(); + timerRoutine = StartCoroutine(StartRoutine(invokeTime)); + } + + /// + /// Starts the timer routine. + /// + /// The time to wait until completion. + /// The enumerator for the coroutine. + protected virtual IEnumerator StartRoutine(float invokeTime) + { + float targetTime = actualTime + invokeTime; + while (actualTime < targetTime) + { + yield return null; + } + Complete(); + } + + /// + /// Cancels the timer routine. + /// + protected virtual void CancelRoutine() + { + if (timerRoutine != null) + { + StopCoroutine(timerRoutine); + } + + timerRoutine = null; } /// @@ -294,12 +420,12 @@ protected virtual void Complete() } /// - /// Stores current for calculations. + /// Stores current for calculations. /// protected virtual void SetInternalStates() { - beginTime = Time.time; - currentTime = Time.time; + beginTime = actualTime; + currentTime = actualTime; } /// diff --git a/Tests/Editor/Utility/CountdownTimerTest.cs b/Tests/Editor/Utility/CountdownTimerTest.cs index 1c343c83..e990e0cb 100644 --- a/Tests/Editor/Utility/CountdownTimerTest.cs +++ b/Tests/Editor/Utility/CountdownTimerTest.cs @@ -17,6 +17,7 @@ public class CountdownTimerTest [SetUp] public void SetUp() { + Time.timeScale = 1f; containingObject = new GameObject("CountdownTimerTest"); containingObject.SetActive(false); subject = containingObject.AddComponent(); @@ -29,7 +30,7 @@ public void TearDown() } [UnityTest] - public IEnumerator TimerComplete() + public IEnumerator ScaledTimerComplete() { containingObject.SetActive(true); UnityEventListenerMock timerStartedMock = new UnityEventListenerMock(); @@ -44,6 +45,8 @@ public IEnumerator TimerComplete() subject.StillRunning.AddListener(timerStillRunningMock.Listen); subject.NotRunning.AddListener(timerNotRunningMock.Listen); + subject.TimeSource = CountdownTimer.TimeSourceType.ScaledTime; + subject.StartTime = 0.1f; Assert.IsFalse(timerStartedMock.Received); @@ -63,7 +66,163 @@ public IEnumerator TimerComplete() Assert.IsTrue(timerStillRunningMock.Received); Assert.IsFalse(timerNotRunningMock.Received); - yield return new WaitForSeconds(0.1f); + yield return new WaitForSecondsRealtime(0.1f); + + Assert.IsFalse(timerCancelledMock.Received); + Assert.IsTrue(timerCompleteMock.Received); + + timerStillRunningMock.Reset(); + timerNotRunningMock.Reset(); + + subject.EmitStatus(); + + Assert.IsFalse(timerStillRunningMock.Received); + Assert.IsTrue(timerNotRunningMock.Received); + } + + [UnityTest] + public IEnumerator ScaledTimerCompleteReducedTimeScale() + { + Time.timeScale = 0.5f; + + containingObject.SetActive(true); + UnityEventListenerMock timerStartedMock = new UnityEventListenerMock(); + UnityEventListenerMock timerCancelledMock = new UnityEventListenerMock(); + UnityEventListenerMock timerCompleteMock = new UnityEventListenerMock(); + UnityEventListenerMock timerStillRunningMock = new UnityEventListenerMock(); + UnityEventListenerMock timerNotRunningMock = new UnityEventListenerMock(); + + subject.Started.AddListener(timerStartedMock.Listen); + subject.Cancelled.AddListener(timerCancelledMock.Listen); + subject.Completed.AddListener(timerCompleteMock.Listen); + subject.StillRunning.AddListener(timerStillRunningMock.Listen); + subject.NotRunning.AddListener(timerNotRunningMock.Listen); + + subject.TimeSource = CountdownTimer.TimeSourceType.ScaledTime; + + subject.StartTime = 0.1f; + + Assert.IsFalse(timerStartedMock.Received); + Assert.IsFalse(timerCancelledMock.Received); + Assert.IsFalse(timerCompleteMock.Received); + Assert.IsFalse(timerStillRunningMock.Received); + Assert.IsFalse(timerNotRunningMock.Received); + + subject.Begin(); + + Assert.IsTrue(timerStartedMock.Received); + Assert.IsFalse(timerCancelledMock.Received); + Assert.IsFalse(timerCompleteMock.Received); + + subject.EmitStatus(); + + Assert.IsTrue(timerStillRunningMock.Received); + Assert.IsFalse(timerNotRunningMock.Received); + + yield return new WaitForSecondsRealtime(0.1f); + + Assert.IsFalse(timerCancelledMock.Received); + Assert.IsFalse(timerCompleteMock.Received); + + timerStillRunningMock.Reset(); + timerNotRunningMock.Reset(); + + subject.EmitStatus(); + + Assert.IsTrue(timerStillRunningMock.Received); + Assert.IsFalse(timerNotRunningMock.Received); + } + + [UnityTest] + public IEnumerator UnscaledTimerComplete() + { + subject.TimeSource = CountdownTimer.TimeSourceType.UnscaledTime; + + containingObject.SetActive(true); + UnityEventListenerMock timerStartedMock = new UnityEventListenerMock(); + UnityEventListenerMock timerCancelledMock = new UnityEventListenerMock(); + UnityEventListenerMock timerCompleteMock = new UnityEventListenerMock(); + UnityEventListenerMock timerStillRunningMock = new UnityEventListenerMock(); + UnityEventListenerMock timerNotRunningMock = new UnityEventListenerMock(); + + subject.Started.AddListener(timerStartedMock.Listen); + subject.Cancelled.AddListener(timerCancelledMock.Listen); + subject.Completed.AddListener(timerCompleteMock.Listen); + subject.StillRunning.AddListener(timerStillRunningMock.Listen); + subject.NotRunning.AddListener(timerNotRunningMock.Listen); + + subject.StartTime = 0.1f; + + Assert.IsFalse(timerStartedMock.Received); + Assert.IsFalse(timerCancelledMock.Received); + Assert.IsFalse(timerCompleteMock.Received); + Assert.IsFalse(timerStillRunningMock.Received); + Assert.IsFalse(timerNotRunningMock.Received); + + subject.Begin(); + + Assert.IsTrue(timerStartedMock.Received); + Assert.IsFalse(timerCancelledMock.Received); + Assert.IsFalse(timerCompleteMock.Received); + + subject.EmitStatus(); + + Assert.IsTrue(timerStillRunningMock.Received); + Assert.IsFalse(timerNotRunningMock.Received); + + yield return new WaitForSecondsRealtime(0.1f); + + Assert.IsFalse(timerCancelledMock.Received); + Assert.IsTrue(timerCompleteMock.Received); + + timerStillRunningMock.Reset(); + timerNotRunningMock.Reset(); + + subject.EmitStatus(); + + Assert.IsFalse(timerStillRunningMock.Received); + Assert.IsTrue(timerNotRunningMock.Received); + } + + [UnityTest] + public IEnumerator UnscaledTimerCompleteReduceTimeScale() + { + Time.timeScale = 0.5f; + subject.TimeSource = CountdownTimer.TimeSourceType.UnscaledTime; + + containingObject.SetActive(true); + UnityEventListenerMock timerStartedMock = new UnityEventListenerMock(); + UnityEventListenerMock timerCancelledMock = new UnityEventListenerMock(); + UnityEventListenerMock timerCompleteMock = new UnityEventListenerMock(); + UnityEventListenerMock timerStillRunningMock = new UnityEventListenerMock(); + UnityEventListenerMock timerNotRunningMock = new UnityEventListenerMock(); + + subject.Started.AddListener(timerStartedMock.Listen); + subject.Cancelled.AddListener(timerCancelledMock.Listen); + subject.Completed.AddListener(timerCompleteMock.Listen); + subject.StillRunning.AddListener(timerStillRunningMock.Listen); + subject.NotRunning.AddListener(timerNotRunningMock.Listen); + + subject.StartTime = 0.1f; + + Assert.IsFalse(timerStartedMock.Received); + Assert.IsFalse(timerCancelledMock.Received); + Assert.IsFalse(timerCompleteMock.Received); + Assert.IsFalse(timerStillRunningMock.Received); + Assert.IsFalse(timerNotRunningMock.Received); + + subject.Begin(); + + Assert.IsTrue(timerStartedMock.Received); + Assert.IsFalse(timerCancelledMock.Received); + Assert.IsFalse(timerCompleteMock.Received); + + subject.EmitStatus(); + + Assert.IsTrue(timerStillRunningMock.Received); + Assert.IsFalse(timerNotRunningMock.Received); + + yield return new WaitForSecondsRealtime(0.1f); Assert.IsFalse(timerCancelledMock.Received); Assert.IsTrue(timerCompleteMock.Received);