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);