Use Async/Await to Simplify Asynchronous Logic in Unity

Wait until something is done before executing with async/await

Cover image
by Tommy Leung on February 6th, 2020

Why would you want to use async/await over coroutines to deal with asynchronous code?

It is understandable if you are hesitant.

The async/await syntax is somewhat new and unfamiliar to long-time users of Unity. Coroutines, on the other hand, have been a staple of Unity for as long as anyone can remember.

It is used natively in WWW and UnityWebRequest so odds are you've had to interact with them.

You may have heard that using a Task requires the unnecessary overhead of threads.

We can handle these concerns with the UniTask library created by the maintainer of UniRx--one of my favorite libraries to use with Unity!

With the performance concerns out of the way lets talk about the benefits of async/await.

It is syntactically easier to understand than coroutines. Especially for those new to Unity or are coming from the wider development field like .Net or JavaScript.

You can use async/await outside of a MonoBehaviour and call async methods like other methods.

Even experienced Unity developers will sometimes forget to call StartCoroutine().

And lastly, don't you want to learn new shit?! 💩

Unity is moving to adopt more from the larger C# and .Net ecosystem. We should do the same as Unity developers!

Import UniTask

Normally, you use async/await with C#'s System.Threading.Tasks.Task class which typically performs operations on a background thread.

If you don't need a background thread and plan to use Unity APIs then use UniTask instead.

Download the latest Unitypackage from the releases tab and import it into your project.

You will need to use Unity 2018.3 or newer because UniTask relies on a C# 7 feature that allows using task-like types with async/await.

UniTask also adds support for Unity async types like UnityWebRequestAsyncOperation, IEnumerator (coroutines), and more.

There's really no reason not to switch to async/await if you can use UniTask!

Using async/await

We're going to make a simple MonoBehaviour to demonstrate using async/await with UniTask to continuously spawn enemies as long as the maximum number of enemies have not been reached.

The enemy spawning logic is going to be theoretical to keep the focus on async/await. We will just add and subtract from a count of activeEnemies.

Here's the basic structure of our demo Component:

using UnityEngine;
using UniRx.Async;

public class AsyncAwaitDemo : MonoBehaviour
{
	public int activeEnemies = 0;

	async void Start()
	{
		// implementation below...
	}

	async UniTask<int> SpawnEnemies()
	{
		// implementation below...
	}
}

First, let's tackle async UniTask<int> SpawnEnemies().

async UniTask<int> SpawnEnemies()
{
	// wait until there are less than 5 enemies
	await UniTask.WaitUntil(() => this.activeEnemies < 5);

	int enemiesToSpawn = 5 - this.activeEnemies;

	for (int i = 0; i < enemiesToSpawn; ++i)
	{
		Debug.Log(string.Format("spawn enemy {0}", i));

		++this.activeEnemies;

		// wait for 500 ms before continuing for loop
		await UniTask.Delay(500);
	}

	// return the count of enemies spawned
	return enemiesToSpawn;
}

This method will return the number of enemies spawned to reach 5 max enemies with a half second delay between each enemy spawned.

We use await UniTask.WaitUntil(() => this.activeEnemes < 5) to only continue execution when there are less than 5 active enemies.

The await keyword suspends execution until the logic being awaited (on the right hand side) is completed.

Next, we will call this method from Start().

async void Start()
{
	int count = await this.SpawnEnemies();

	Debug.Log(string.Format("<color=blue>{0} enemies spawned!</color>", count));

	while (true)
	{
		count = await this.SpawnEnemies();

		Debug.Log(string.Format("<color=blue>{0} more enemies spawned!</color>", count));
	}
}

Notice that both Start() and SpawnEnemies() have the async modifier. You can only use await inside async methods.

We start by spawning our first 5 enemies and then logging out the number of enemies spawned. We can do this because async methods return a value unlike coroutines.

Then we continue to spawn enemies whenever the number of active enemies fall below 5 by awaiting this.SpawnEnemies() in a while loop.

We use while (true) as a demonstration. You likely have a better fitting condition for your project.

See it in Action

Attach AsyncAwaitDemo to a GameObject in your Scene and hit play to see console logs as each enemy is created.

To see that await is actually working, you can manually change the number of activeEnemies in the Inspector to cause SpawnEnemies() to continue execution.

Alternatively you can add this Update() method to AsyncAwaitDemo that will kill a random number of enemies when you press Space.

void Update()
{
	if (this.activeEnemies < 5)
	{
		return;
	}

	if (Input.GetKeyDown(KeyCode.Space))
	{
		int killCount = Random.Range(1, 6);
		this.activeEnemies -= killCount;

		Debug.Log(string.Format("<color=red>killed {0} enemies</color>", killCount));
	}
}

That's a basic example for using async/await with UniTask.

There are many more operations that you can await like SceneManager.LoadSceneAsync(), UnityWebRequest.Get(), UniTask.Delay(), and more.

I am continuning to learn more about UniTask so be sure to drop your email in the box below to get more async/await techniques!