/* * Unity Timer * * Version: 1.0 * By: Alexander Biggs + Adam Robinson-Yu */ using UnityEngine; using System; using System.Linq; using System.Collections.Generic; using JetBrains.Annotations; using Object = UnityEngine.Object; /// /// Allows you to run events on a delay without the use of s /// or s. /// /// To create and start a Timer, use the method. /// namespace UnityTimer { public class Timer { #region Public Properties/Fields /// /// How long the timer takes to complete from start to finish. /// public float duration { get; private set; } /// /// Whether the timer will run again after completion. /// public bool isLooped { get; set; } /// /// Whether or not the timer completed running. This is false if the timer was cancelled. /// public bool isCompleted { get; private set; } /// /// Whether the timer uses real-time or game-time. Real time is unaffected by changes to the timescale /// of the game(e.g. pausing, slow-mo), while game time is affected. /// public bool usesRealTime { get; private set; } /// /// Whether the timer is currently paused. /// public bool isPaused { get { return this._timeElapsedBeforePause.HasValue; } } /// /// Whether or not the timer was cancelled. /// public bool isCancelled { get { return this._timeElapsedBeforeCancel.HasValue; } } /// /// Get whether or not the timer has finished running for any reason. /// public bool isDone { get { return this.isCompleted || this.isCancelled || this.isOwnerDestroyed; } } #endregion #region Public Static Methods /// /// Register a new timer that should fire an event after a certain amount of time /// has elapsed. /// /// Registered timers are destroyed when the scene changes. /// /// The time to wait before the timer should fire, in seconds. /// An action to fire when the timer completes. /// An action that should fire each time the timer is updated. Takes the amount /// of time passed in seconds since the start of the timer's current loop. /// Whether the timer should repeat after executing. /// Whether the timer uses real-time(i.e. not affected by pauses, /// slow/fast motion) or game-time(will be affected by pauses and slow/fast-motion). /// An object to attach this timer to. After the object is destroyed, /// the timer will expire and not execute. This allows you to avoid annoying s /// by preventing the timer from running and accessessing its parents' components /// after the parent has been destroyed. /// A timer object that allows you to examine stats and stop/resume progress. public static Timer Register(float duration, Action onComplete, Action onUpdate = null, bool isLooped = false, bool useRealTime = false, MonoBehaviour autoDestroyOwner = null) { // create a manager object to update all the timers if one does not already exist. if (Timer._manager == null) { TimerManager managerInScene = Object.FindObjectOfType(); if (managerInScene != null) { Timer._manager = managerInScene; } else { GameObject managerObject = new GameObject { name = "TimerManager" }; GameObject.DontDestroyOnLoad(managerObject); Timer._manager = managerObject.AddComponent(); } } Timer timer = new Timer(duration, onComplete, onUpdate, isLooped, useRealTime, autoDestroyOwner); Timer._manager.RegisterTimer(timer); return timer; } /// /// Cancels a timer. The main benefit of this over the method on the instance is that you will not get /// a if the timer is null. /// /// The timer to cancel. public static void Cancel(Timer timer) { if (timer != null) { timer.Cancel(); } } /// /// Pause a timer. The main benefit of this over the method on the instance is that you will not get /// a if the timer is null. /// /// The timer to pause. public static void Pause(Timer timer) { if (timer != null) { timer.Pause(); } } /// /// Resume a timer. The main benefit of this over the method on the instance is that you will not get /// a if the timer is null. /// /// The timer to resume. public static void Resume(Timer timer) { if (timer != null) { timer.Resume(); } } public static void CancelAllRegisteredTimers() { if (Timer._manager != null) { Timer._manager.CancelAllTimers(); } // if the manager doesn't exist, we don't have any registered timers yet, so don't // need to do anything in this case } public static void PauseAllRegisteredTimers() { if (Timer._manager != null) { Timer._manager.PauseAllTimers(); } // if the manager doesn't exist, we don't have any registered timers yet, so don't // need to do anything in this case } public static void ResumeAllRegisteredTimers() { if (Timer._manager != null) { Timer._manager.ResumeAllTimers(); } // if the manager doesn't exist, we don't have any registered timers yet, so don't // need to do anything in this case } #endregion #region Public Methods /// /// Stop a timer that is in-progress or paused. The timer's on completion callback will not be called. /// public void Cancel() { if (this.isDone) { return; } this._timeElapsedBeforeCancel = this.GetTimeElapsed(); this._timeElapsedBeforePause = null; } /// /// Pause a running timer. A paused timer can be resumed from the same point it was paused. /// public void Pause() { if (this.isPaused || this.isDone) { return; } this._timeElapsedBeforePause = this.GetTimeElapsed(); } /// /// Continue a paused timer. Does nothing if the timer has not been paused. /// public void Resume() { if (!this.isPaused || this.isDone) { return; } this._timeElapsedBeforePause = null; } /// /// Get how many seconds have elapsed since the start of this timer's current cycle. /// /// The number of seconds that have elapsed since the start of this timer's current cycle, i.e. /// the current loop if the timer is looped, or the start if it isn't. /// /// If the timer has finished running, this is equal to the duration. /// /// If the timer was cancelled/paused, this is equal to the number of seconds that passed between the timer /// starting and when it was cancelled/paused. public float GetTimeElapsed() { if (this.isCompleted || this.GetWorldTime() >= this.GetFireTime()) { return this.duration; } return this._timeElapsedBeforeCancel ?? this._timeElapsedBeforePause ?? this.GetWorldTime() - this._startTime; } /// /// Get how many seconds remain before the timer completes. /// /// The number of seconds that remain to be elapsed until the timer is completed. A timer /// is only elapsing time if it is not paused, cancelled, or completed. This will be equal to zero /// if the timer completed. public float GetTimeRemaining() { return this.duration - this.GetTimeElapsed(); } /// /// Get how much progress the timer has made from start to finish as a ratio. /// /// A value from 0 to 1 indicating how much of the timer's duration has been elapsed. public float GetRatioComplete() { return this.GetTimeElapsed() / this.duration; } /// /// Get how much progress the timer has left to make as a ratio. /// /// A value from 0 to 1 indicating how much of the timer's duration remains to be elapsed. public float GetRatioRemaining() { return this.GetTimeRemaining() / this.duration; } #endregion #region Private Static Properties/Fields // responsible for updating all registered timers private static TimerManager _manager; #endregion #region Private Properties/Fields private bool isOwnerDestroyed { get { return this._hasAutoDestroyOwner && this._autoDestroyOwner == null; } } private readonly Action _onComplete; private readonly Action _onUpdate; private float _startTime; private float _lastUpdateTime; // for pausing, we push the start time forward by the amount of time that has passed. // this will mess with the amount of time that elapsed when we're cancelled or paused if we just // check the start time versus the current world time, so we need to cache the time that was elapsed // before we paused/cancelled private float? _timeElapsedBeforeCancel; private float? _timeElapsedBeforePause; // after the auto destroy owner is destroyed, the timer will expire // this way you don't run into any annoying bugs with timers running and accessing objects // after they have been destroyed private readonly MonoBehaviour _autoDestroyOwner; private readonly bool _hasAutoDestroyOwner; #endregion #region Private Constructor (use static Register method to create new timer) private Timer(float duration, Action onComplete, Action onUpdate, bool isLooped, bool usesRealTime, MonoBehaviour autoDestroyOwner) { this.duration = duration; this._onComplete = onComplete; this._onUpdate = onUpdate; this.isLooped = isLooped; this.usesRealTime = usesRealTime; this._autoDestroyOwner = autoDestroyOwner; this._hasAutoDestroyOwner = autoDestroyOwner != null; this._startTime = this.GetWorldTime(); this._lastUpdateTime = this._startTime; } #endregion #region Private Methods private float GetWorldTime() { return this.usesRealTime ? Time.realtimeSinceStartup : Time.time; } private float GetFireTime() { return this._startTime + this.duration; } private float GetTimeDelta() { return this.GetWorldTime() - this._lastUpdateTime; } private void Update() { if (this.isDone) { return; } if (this.isPaused) { this._startTime += this.GetTimeDelta(); this._lastUpdateTime = this.GetWorldTime(); return; } this._lastUpdateTime = this.GetWorldTime(); if (this._onUpdate != null) { this._onUpdate(this.GetTimeElapsed()); } if (this.GetWorldTime() >= this.GetFireTime()) { if (this._onComplete != null) { this._onComplete(); } if (this.isLooped) { this._startTime = this.GetWorldTime(); } else { this.isCompleted = true; } } } #endregion #region Manager Class (implementation detail, spawned automatically and updates all registered timers) /// /// Manages updating all the s that are running in the application. /// This will be instantiated the first time you create a timer -- you do not need to add it into the /// scene manually. /// private class TimerManager : MonoBehaviour { private List _timers = new List(); // buffer adding timers so we don't edit a collection during iteration private List _timersToAdd = new List(); public void RegisterTimer(Timer timer) { this._timersToAdd.Add(timer); } public void CancelAllTimers() { foreach (Timer timer in this._timers) { timer.Cancel(); } this._timers = new List(); this._timersToAdd = new List(); } public void PauseAllTimers() { foreach (Timer timer in this._timers) { timer.Pause(); } } public void ResumeAllTimers() { foreach (Timer timer in this._timers) { timer.Resume(); } } // update all the registered timers on every frame [UsedImplicitly] private void Update() { this.UpdateAllTimers(); } private void UpdateAllTimers() { if (this._timersToAdd.Count > 0) { this._timers.AddRange(this._timersToAdd); this._timersToAdd.Clear(); } foreach (Timer timer in this._timers) { timer.Update(); } this._timers.RemoveAll(t => t.isDone); } } #endregion } }