Avoiding Unity Update Loops

The Update loop is the most commonly used function to implement any kind of game behaviour, according to Unity's documentation. As such it's important to address the performance impact it has on your game since logic in the Update function runs every frame. Update is convenient and time-saving because it requires little planning to use and provides immediate results, but this allure can be an issue as your codebase grows. It's especially problematic if you have many characters or smart objects within your game world constantly checking for conditions or executing complex logic. I'll demonstrate how to avoid the Update loop using either Events, Callbacks, Coroutines, or the Invoke function.

Before diving into programming techniques you should first understand when it's appropriate to use the Update function. For me to maintain a performance-based mindset, I generally consider the Update function a bad thing and avoid it when possible. Logic in any of the Update functions should be simple; if something does not need to be calculated every frame then do it elsewhere.

  • Use Update for Input and Movement
  • Use FixedUpdate for continuous physics related logic
  • Use LateUpdate for camera movement/rotation

Usually anything else doesn't need to be in the Update loops and comes down to being handled by an appropriate technique, at an appropriate time. The techniques listed below cover most cases I've encountered where an Update loop could be optimized.

Events

Events are my go-to solution when an action occurs and something (often many) needs to know about it. For example if the player gains money then the UI should be updated to reflect the new total. If the money is gained while browsing a store, some items may need to change state (not purchasable => purchasable). Rather than having the UI constantly check the player's money in an Update loop, or having messy logic where a bunch of methods are called on interested objects, you can instead use an event.

In your IDE (MonoDevelop, Visual Studio, etc) an event is typically associated with an icon of the letter 'E' or a sweet lightning bolt. An event is essentially a list of methods who have subscribed to it, and when the event is raised it iterates through the list, calling each subscribed method. Creating and using an event is accomplished in a few short steps as shown in the below code with comments.

Defining and Raising an Event

using UnityEngine;

public class Inventory
{
	// The total amount of money this inventory contains
	int money;

	// Defines the function and parameters your event requires
	// (optional) Define parameters that provide information related to the event
	public delegate void OnMoneyUpdatedHandler(int currentMoney, int amountChanged);

	// The event which other objects can subscribe to
	// Uses the function defined above as its type
	public event OnMoneyUpdatedHandler OnMoneyUpdated;

	/// <summary>
	/// Updates the current total money by Amount.
	/// </summary>
	/// <param name="amount">Amount to alter the money total by.</param>
	public void UpdateMoney (int amount)
	{
		// Redundancy check
		if (amount != 0) {
			// Tally up the new money total
			money += amount;

			// Raise OnMoneyUpdated event if it contains subscriptions.
			// An event with no subscribers is null, always check.
			if (OnMoneyUpdated != null) {
				OnMoneyUpdated(money, amount);
			}
		}
	}
}

Subscribing to Events

using UnityEngine;
using UnityEngine.UI;

public class HUD : MonoBehaviour
{
	// The UI Text on the HUD which displays the current money in the inventory
	[SerializeField]
	Text moneyLabel;

	// Reference to the player's inventory, assume it has been assigned
	Inventory inventory;

	void Start ()
	{
		// Subscribes OnMoneyUpdated to the inventory's OnMoneyUpdated event
		// You only need to subscribe once.
		// You can unsubscribe the same way but using a minus (-) sign.
		inventory.OnMoneyUpdated += OnMoneyUpdated;
	}

	/// <summary>
	/// Matches the function defined by OnMoneyUpdatedHandler in Inventory.cs
	/// It must match in order to subscribe to the event.
	/// Subscribes to the OnMoneyUpdated event in the Start method above.
	/// Called when the OnMoneyUpdated event is raised in Inventory.cs
	/// </summary>
	/// <param name="totalMoney">New amount of money in the inventory.</param>
	/// <param name="amountChanged">Amount changed during this event.</param>
	void OnMoneyUpdated (int totalMoney, int amountChanged)
	{
		moneyLabel.text = totalMoney.ToString();

		// TODO: Effect displaying the amount changed (ie. +/- amountChanged)
	}
}

You can unsubscribe from an event using the minus sign (inventory.OnMoneyUpdated -= OnMoneyUpdated). I usually see this with static events since they are not reset with scene loads and can reference a method on a destroyed object. Try to avoid this situation as it unnecessarily complicates your codebase.

Callback Functions

I believe callbacks were first seen in the Unity API when they introduced their Social API. They accomplish a similar task as events, but serve only a single subscription rather than many, and are usually called just once. Those who've used JavaScript and JQuery should be familiar with the concept of callbacks.

When calling FunctionA, you can pass another FunctionB as a parameter, usually with the assumption that FunctionB is called once FunctionA has completed its task. This is especially useful when the task you're waiting on may take multiple frames to complete, like a web request to a server, which is why callbacks are seen throughout examples in the Social API. So rather than continuously checking for a server response in an Update loop, use a callback function.

Callback Example

Here you see a method which accepts a callback parameter and uses it later.

using System.Collections;
using UnityEngine;

public class Timer : MonoBehaviour
{
	/// <summary>
	/// Helper method which starts the DoCallAfterTime coroutine.
	/// Notice System.Action represents a void function with no parameters.
	/// </summary>
	/// <param name="time">The amount of time to wait.</param>
	/// <param name="callback">The callback method called after waiting.</param>
	public void CallbackAfterTime (float time, System.Action callback)
	{
		StartCoroutine(DoCallbackAfterTime(time, callback));
	}

	/// <summary>
	/// Calls the given callback method after waiting a given amount of time.
	/// </summary>
	/// <param name="time">The amount of time to wait.</param>
	/// <param name="callback">The callback method called after waiting.</param>
	IEnumerator DoCallbackAfterTime (float time, System.Action callback)
	{
		yield return new WaitForSeconds(time);
		callback();
	}
}

While in this example you see the callback method defined and passed as a parameter.

using UnityEngine;

public class CallbackExample : MonoBehaviour
{
	// Reference to the timer, assume it's assigned
	Timer timer;

	void Start ()
	{
		Debug.Log("Starting callback flow at: " + Time.realtimeSinceStartup);

		// Our method, DoSomething, is sent as a parameter
		// DoSomething is later called by CallbackAfterTime once it completes
		timer.CallbackAfterTime(3f, this.DoSomething);
	}

	/// <summary>
	/// Callback method
	/// </summary>
	void DoSomething ()
	{
		Debug.Log("Callback flow completed at: " + Time.realtimeSinceStartup);
	}
}

Callback methods can also have parameters.

public void MyMethod (System.Action<int, string[]> callback)
{
	callback(42, new string[] { "callbacks", "are", "cool" });
}

Callbacks via Lambda Expressions

Often times using anonymous functions is the go-to approach for callbacks, as you may have even noticed in Unity's Social API examples. They save the need to define a function in your class while keeping logic contained and easier to follow at a glance. These are unnamed functions with named parameters that automatically match the types defined in the callback. The lamda expression => is used to create a local function, with parameters on the left and the statement block on the right. Here's the first example from above rewritten with a lambda function.

using UnityEngine;

public class CallbackExample : MonoBehaviour
{
	// Reference to the timer, assume it's assigned
	Timer timer;

	void Start ()
	{
		Debug.Log("Starting callback flow at: " + Time.realtimeSinceStartup);

		// The delegate method is represented () => {}
		timer.CallbackAfterTime(3f, () => {
			Debug.Log("completed at: " + Time.realtimeSinceStartup);
		});
	}
}

And here is an example using callback parameters.

using System;
using UnityEngine;

public class CallbackExampleTwo
{
	public void MyMethod (System.Action<int, string[]> callback)
	{
		callback(42, new string[] { "callbacks", "are", "cool" });
	}
}

public class LambdaExample
{
	CallbackExampleTwo example;

	void DoExample ()
	{
		// Gets int & string values returned by CallbackExampleTwo.MyMethod
		example.MyMethod((number, message) => {
			Debug.Log(number); // 42
		});
	}
}

You can also create a class property that is a delegate function and assign it for use as a callback. You can simply assign an appropriate function as the delegate callback to be used until otherwise assigned.

Using Coroutines/IEnumerators as Update Loops

Coroutines are common place in Unity games, so I'll assume you have some experience with them already. What you may not have considered though is creating your own form of Update loop using a coroutine. It's not a common use-case, but it can be useful when wanting something to update frequently, but less frequently than every frame. For example you could create a function that runs every 10 frames, or every half-second, or even at a random interval. This can be useful for AI decisions or something that can afford to be a bit 'choppy' such as retro-style character movement.

using System.Collections;
using UnityEngine;

public class CustomLoop : MonoBehaviour
{
	const float maxTimeInterval = 3f;
	const float minTimeInterval = 0.25f;
	const float defaultTimeInterval = 0.5f;
	const int frameInterval = 10;

	void Start ()
	{
		StartCoroutine(TimedUpdateRoutine());
		StartCoroutine(FrameDelayedUpdateRoutine());
		StartCoroutine(RandomTimedUpdateRoutine());
	}

	void TimedUpdate ()
	{
		Debug.Log("Time update: " + Time.realtimeSinceStartup);
	}

	void FrameDelayedUpdate ()
	{
		Debug.Log("Frame update: " + Time.realtimeSinceStartup);
	}

	void RandomUpdate ()
	{
		// Your logic here
		Debug.Log("Random update: " + Time.realtimeSinceStartup);
	}

	/// <summary>
	/// A loop which executes logic after waiting for an amount of time to pass.
	/// </summary>
	IEnumerator TimedUpdateRoutine ()
	{
		while (true) {
			yield return new WaitForSeconds(defaultTimeInterval);
			TimedUpdate();
		}
	}

	/// <summary>
	/// A loop which executes logic after waiting for an amount of frames to pass.
	/// </summary>
	IEnumerator FrameDelayedUpdateRoutine ()
	{
		// Always run
		while (true) {
			for (int i = 0; i < frameInterval; i++) {
				yield return new WaitForEndOfFrame();
			}

			FrameDelayedUpdate();
		}
	}

	/// <summary>
	/// A loop which executes logic after waiting a random amount of time.
	/// </summary>
	IEnumerator RandomTimedUpdateRoutine ()
	{
		// Always run
		while (true) {
			float randomInterval = Random.Range(minTimeInterval, maxTimeInterval);
			yield return new WaitForSeconds(randomInterval);
			RandomUpdate();
		}
	}
}

You can also stop these coroutine loops using the StopCoroutine function.

Invoke & InvokeRepeating

The Invoke function is neat tool to have at your disposal. You pass a method name as a string argument and it calls that method after a specified amount of time. Think of it as an alternative to using time-based coroutines, with built-in logic to avoid edge cases your coroutines might miss, such as dealing with Time.timeScale. It can also be useful (but potentially insecure!) in online multiplayer development where only primitive types are used as RPC arguments, allowing string methods to be invoked in situations you can't account for, such as user-made mods.

InvokeRepeating is what we'll focus on today though, as it can accomplish the same thing as the Update loop. Basically it's the same solution as explained in the Coroutines section above, except it only deals with time. You specify a function to be called repeatedly with a specified delay between each call. You can also stop it from repeating using the CancelInvoke function.

using UnityEngine;

public class CustomLoop : MonoBehaviour
{
	void Start ()
	{
		// Call TimedUpdate immediately, repeat every 0.1 seconds
		InvokeRepeating("TimedUpdate", 0f, 0.1f);
	}

	void TimedUpdate ()
	{
		Debug.Log("Time update: " + Time.realtimeSinceStartup);
	}
}