Simple Timer For Unity

Overview

I've updated this post with a new implementation, one that doesn't require the ObjectPool dependency, instead the Timer class contains its own pool of timers - August 2nd, 2017

Let's say you want something to happen 5 seconds from now, or let's say you want something to occur over the next 5 seconds and then stop. To do these sorts of timed actions you'll need some floats to record the current time, the max duration, and you'll need an Update function to increment the timer until it hits the duration. If you have a class that has many timed states, your update function quickly becomes unmaintainable.

I've written a very simple helper class to allow you to start a timer by simply supplying a duration. 

The Timer class is a Singleton which creates a pool of InnerTimers. A client app can request a timer with a duration, update function and a complete function - both of which are optional (but the timer is pointless unless one is supplied).

public class InnerTimer {
	public float m_duration;
	public float m_timer { get; private set; }
	public float m_zeroToOne { get { return m_timer / m_duration; } }
	private float m_throttle;
	private float m_throttleTimer;
	public Timer_UpdateCallback m_updateFunction;
	public Timer_CompleteCallback m_completeFunction;
	public enum TimerState {
		Uninitialized,
		Playing,
		Paused,
	}
	public TimerState m_state;
	public bool m_autodispose;
}

Usage

The Timer class defines two delegate function signatures that the caller can supply. The UpdateCallback is called once per frame (in Unity's regular Update function) and the CompleteCallback is called in the final frame - immediately after the final UpdateCallback. Both callbacks are optional, but supplying neither of them would make the Timer useless. 

protected void GameOver(string message, Color color) {
    GameplayUI.Instance.roundOver.ShowMessage(message, color);
    Timer delayGameOver = ObjectPoolManager.Instance.timers.Get();
    delayGameOver.Initialize(2.0f, null, ChangeGameStateToGameOver);
}

In this example, I use the Timer to delay the GameOver state by 2 seconds. This makes it so in my game, when the final player dies, there is a 2 second delay before the game switches to the GameOver. This small delay has benefitted gameplay by allowing the players to process how they died, and was easily and cleanly implemented with the above pattern.

Garbage

There's one annoying problem with this pattern of implementation.

It allocates 408 bytes of garbage every time Initialize is called. It seems to be related to passing a delegate as a parameter.

To get around this, the caller should cache the delegates in it's Start or Awake methods. This way, no garbage is allocated at runtime. It pains me that the caller has to do this, because the Timer is supposed to eliminate the kind of setup that a basic timer requires, but this still cleans up your Update function greatly. 

Another garbage free implementation would be to have the callbacks be specified by an interface, and have the caller implement TimerUpdate and TimerComplete methods, then the caller could simply supply a "this" reference to the Timer's Initialize method. There's pros and cons to both implementations.

A garbage free usage of the Timer looks like this:

public class Game {
    private Timer.Timer_CompleteCallback _changeGameStateToGameOver;
    private Timer.Timer_UpdateCallback _fadeOutGameOverMessage;
    public UnityEngine.UI.Text gameOverMessage;

void Start() {
    _changeGameStateToGameOver = delegate() {
        GameManager.Instance.ChangeGameState(GameManager.GameState.GameOver);
    };
    
    _fadeOutGameOverMessage = delegate(float zeroToOne){
        gameOverMessage.color = Color.Lerp(Color.red, Color.clear, zeroToOne);
    };
}

protected void GameOver(string message, Color color) {
    gameOverMessage.text = message;
    gameOverMessage.color = color;
    // delay game state change by 2 seconds
    Timer.InnerTimer delayGameOver = Timer.Instance.Get();
    delayGameOver.Initialize(2.0f, _fadeOutGameOverMessage, _changeGameStateToGameOver);
}

Source Code

Below is the full Timer.cs source code.

using System;
using System.Collections.Generic;
using UnityEngine;

// Timer
//  http://www.00jknight.com/blog/unity-timer
//  This object holds a timer and a couple callback functions.
//  To start a timer, a user can grab one of these
//  and Initialize it with a duration, range, UpdateCallback and CompleteCallback.
//  A gameobject will activate, the update will run and your updatecallback will be called.
//  When the timer is complete the complete callback will be called, the gameobject will deactivate
//  and this timer will return to the ObjectPool.
//  The CompleteCallback will be called in the same frame as the final UpdateCallback.



//  If you supply regular instance methods to this class, it will allocate garbage casting the methods to the delegate types.
//  The best way to avoid this is to cache references to the delegate types in your scripts init.
//  eg) 
//   Start() {
//      _timerUpdateCallback = (Timer.Timer_UpdateCallback) SomeInstanceMethod;
//      _timerCompleteCallback = (Timer.Timer_CompleteCallback) SomeOtherInstanceMethod;
//  }
//  
//  ThenLater() {
//     Timer.InnerTimer t = Timer.Instance.Get();
//     t.Initialize(3, _timerUpdateCallback, _timerCompleteCallback);
//  }
//  
//  The above pattern will allocate no garbage.
public class Timer : MonoBehaviour {
	public delegate void Timer_UpdateCallback(float zeroToOneComplete);
	public delegate void Timer_CompleteCallback();
	public static Timer Instance;
	

	private Stack m_inactiveTimers;
	private const int INITIAL_TIMERS = 32;
	private List m_activeTimers;
	private List m_activeTimersToRemoveThisFrame;
	private List m_activeTimersToAddThisFrame;

	private List m_fixedActiveTimers;
	private List m_fixedActiveTimersToRemoveThisFrame;
	private List m_fixedActiveTimersToAddThisFrame;

	void Awake() {
		Instance = this;
		m_inactiveTimers = new Stack();
		for (int i = 0; i < INITIAL_TIMERS; i++) {
			m_inactiveTimers.Push(new Timer.InnerTimer());
		}

		m_activeTimersToRemoveThisFrame = new List();
		m_activeTimersToAddThisFrame	= new List();
		m_activeTimers 					= new List();

		m_fixedActiveTimersToRemoveThisFrame 	= new List();
		m_fixedActiveTimersToAddThisFrame		= new List();
		m_fixedActiveTimers 					= new List();
	}

	void FixedUpdate() {
		// Update
		var iterator = m_fixedActiveTimers.GetEnumerator();
		while (iterator.MoveNext()) {
			iterator.Current._update(Time.deltaTime);
		}

		// Remove
		var removeIterator = m_fixedActiveTimersToRemoveThisFrame.GetEnumerator();
		while (removeIterator.MoveNext()) {
			m_activeTimers.Remove(removeIterator.Current);
		}
		m_fixedActiveTimersToRemoveThisFrame.Clear();

		// Add
		var addIterator = m_fixedActiveTimersToAddThisFrame.GetEnumerator();
		while (addIterator.MoveNext()) {
			m_activeTimers.Add(addIterator.Current);
		}
		m_fixedActiveTimersToAddThisFrame.Clear();
	}


	void Update() {
		// Update
		var iterator = m_activeTimers.GetEnumerator();
		while (iterator.MoveNext()) {
			iterator.Current._update(Time.deltaTime);
		}

		// Remove
		var removeIterator = m_activeTimersToRemoveThisFrame.GetEnumerator();
		while (removeIterator.MoveNext()) {
			m_activeTimers.Remove(removeIterator.Current);
		}
		m_activeTimersToRemoveThisFrame.Clear();

		// Add
		var addIterator = m_activeTimersToAddThisFrame.GetEnumerator();
		while (addIterator.MoveNext()) {
			m_activeTimers.Add(addIterator.Current);
		}
		m_activeTimersToAddThisFrame.Clear();
	}








	public Timer.InnerTimer Get(bool create=true, bool useFixedUpdate=false)
	{
		Timer.InnerTimer toReturn = null;
		if (m_inactiveTimers.Count == 0)
		{
			if (create) {
				toReturn = new Timer.InnerTimer();
            }
			else return null;
		}
		else toReturn = m_inactiveTimers.Pop();

		if (useFixedUpdate)
			m_fixedActiveTimersToAddThisFrame.Add(toReturn);
		else
			m_activeTimersToAddThisFrame.Add(toReturn);

        return toReturn;
	}

	public void Store(Timer.InnerTimer t) {
		m_inactiveTimers.Push(t);
		m_activeTimersToRemoveThisFrame.Add(t);
	}






	public class InnerTimer {
		public float m_duration;
		public float m_timer { get; private set; }
		public float m_zeroToOne { get { return m_timer / m_duration; } }
		private float m_throttle;
		private float m_throttleTimer;
		public Timer_UpdateCallback m_updateFunction;
		public Timer_CompleteCallback m_completeFunction;
		public enum TimerState {
			Uninitialized,
			Playing,
			Paused,
		}
		public TimerState m_state;
		public bool m_autodispose;
		public void _update(float dt) {
			if (m_state == TimerState.Playing) {
				m_timer += Time.deltaTime; // todo: custom time thingy... shouldn't update when game paused.

				// call Update Callback
				if (m_updateFunction != null) {
					if (m_throttle > 0.0f) {
						m_throttleTimer += Time.deltaTime;
						if (m_throttleTimer > m_throttle) {
							m_updateFunction(m_duration > 0.0f ? m_timer / m_duration : m_timer);
							m_throttleTimer = 0.0f;
						}
					} else
						m_updateFunction(m_duration > 0.0f ? m_timer / m_duration : m_timer);
				}

				// call Complete Callback - if update called pause or stop, then dont call complete
				if (m_state == TimerState.Playing) {
					if (m_timer >= m_duration && m_duration >= 0.0f) {
						if (m_completeFunction != null) {
							m_completeFunction();
						}

						if (m_autodispose) {
							if (m_state == TimerState.Playing
							&& m_timer >= m_duration && m_duration >= 0.0f) { // re check "finished condition" to allow complete function to restart or pausetimer
								Dispose();
							}
						}
						else {
							Stop();
						}
					}
				}
			}
		}

		public InnerTimer Initialize(float duration=1.0f, Timer_UpdateCallback update_function=null, Timer_CompleteCallback complete_function=null, float throttle=-1.0f, bool autodispose=true) {
	    	m_updateFunction 	= update_function;
	    	m_completeFunction 	= complete_function;
	    	m_timer 			= 0.0f;
	    	m_duration			= duration;
	    	m_state				= TimerState.Playing;
			m_throttleTimer		= 0.0f;
			m_throttle 			= throttle;
			m_autodispose 		= autodispose;
			return this;
    	}

    	public void Dispose() {
    		m_updateFunction 	= null;
        	m_completeFunction 	= null;
			m_state				= TimerState.Uninitialized;
			Timer.Instance.Store(this);
    	}

    	public void Stop() {
			m_state				= TimerState.Paused;
			m_timer 			= 0.0f;
			m_throttleTimer		= 0.0f;
    	}

    	public void Pause() {
    		m_state				= TimerState.Paused;
    	}

    	public void Resume() {
    		m_state				= TimerState.Playing;
    	}

    	public void Restart() {
    		m_state 			= TimerState.Playing;
    		m_timer 			= 0.0f;
			m_throttleTimer		= 0.0f;
    	}
	}
}
In