/* * Unity Timer * * Version: 1.0 * By: Alexander Biggs + Adam Robinson-Yu */ using UnityEngine; using System; using System.Collections.Generic; using Object = UnityEngine.Object; namespace UnityTimer { [System.Serializable] 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 public static void Init() { if (Timer._manager == null) { GameObject managerObject = new GameObject { name = "[TimerManager]" }; Timer._manager = managerObject.AddComponent(); GameObject.DontDestroyOnLoad(managerObject); } } /// /// 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) { GameObject managerObject = new GameObject { name = "[TimerManager]" }; Timer._manager = managerObject.AddComponent(); GameObject.DontDestroyOnLoad(managerObject); } Timer timer = new Timer(duration, onComplete, onUpdate, isLooped, useRealTime, autoDestroyOwner); Timer._manager.RegisterTimer(timer); return timer; } public static Timer RegisterRealTimeNoLoop(float duration, Action onComplete, Action onUpdate = null, MonoBehaviour autoDestroyOwner = null) { return Register(duration, onComplete, onUpdate, false, true, autoDestroyOwner); } /// /// 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; } public 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 } }