Introduction
Until Unity version 2023.1 there were a few options for asynchronous code in Unity, but each one had its own drawbacks. We always had coroutines that can be used to code asynchronously, but they had their limitations like being tied to the Monobehaviour that launches the coroutine and their inability to return results directly.
We also had the option to use the Task from .NET, but because it is managed from the .NET environment had its own problems when being used in Unity, like having to manually control its lifetime.
There was also the option to use an external library like UniTask but that comes with the usual problems of having to depend on code that is not managed by Unity or our team.
From Unity version 2023.1, we have the Awaitable class from Unity that finally gives a good solution for using asynchronous code. Although the Awaitable can be compared to the Task from .NET, in this post, I will avoid any comparisons as I believe that using the mechanisms a framework has, is always preferable because of the compatibility it provides with its whole ecosystem.
What Is The Unity Awaitable
As I mentioned, the Awaitable is a mechanism provided by Unity to help with the implementation of asynchronous operations. Asynchronous doesn’t necessarily mean parallel. We can have a part of our code, that will wait for another part of our code to execute before continuing, but won’t block any code that is running at that moment.
For example without asynchronous code, a while loop that repeats until a method returns true inside one of our Update methods, will execute the other method’s code until it returns true and then continue with the rest of the code after the while, but will also block the execution of every other Update method that we have.
A coroutine, can be used to avoid that, but as mentioned had its own limitations.
The Awaitable class, can solve this problem, but can also be used for executing code in a background thread, if we have expensive operations that we don’t want to affect the performance of the Main thread. Code examples are always better for explaining code, so let’s start exploring the new Awaitable class in Unity.
Simple Awaitable Methods Usage
The EndOfFrameAsync
, FixedUpdateAsync
, NextFrameAsync
and WaitForSecondsAsync
static methods of the Awaitable, allow us to execute a piece of code and then transfer the execution to the rest of our program for a predetermined amount of time. Here is an example of their usage:
Suppose that we have the following code:
public class AwaitableExample : MonoBehaviour
{
private void Start()
{
for (int i = 0; i < 10; i++)
{
Debug.Log($"i Equals to: {i}, on frame: {Time.frameCount}");
}
Debug.Log("Outside the for loop");
}
private void Update() => Debug.Log($"Inside the Update, frame: {Time.frameCount}");
}
The output will be:
i Equals to: 0, on frame: 1
i Equals to: 1, on frame: 1
.
.
.
i Equals to: 9, on frame: 1
Outside the for loop
Inside the Update, frame: 1
Inside the Update, frame: 2
.
.
.
The for loop will execute before any other code and then the code in Update will start executing. This might be a problem for us. We want the code in the for loop to execute, but we don’t want our game to wait for the code to finish, instead we would like the code to execute while our Update runs, so that our game doesn’t seem to freeze every time a new game object is instantiated. With the Awaitable we can do this:
public class AwaitableExample : MonoBehaviour
{
private async void Start()
{
for (int i = 0; i < 10; i++)
{
Debug.Log($"i Equals to: {i}, on frame: {Time.frameCount}");
await UnityEngine.Awaitable.EndOfFrameAsync();
}
Debug.Log("Outside the for loop");
}
private void Update() => Debug.Log($"Inside the Update, frame: {Time.frameCount}");
}
Now the output will be:
i Equals to: 0, on frame: 1
Inside the Update, frame: 1
i Equals to: 1, on frame: 1
Inside the Update, frame: 2
i Equals to: 2, on frame: 2
.
.
.
Inside the Update, frame: 9
i Equals to: 9, on frame: 9
Inside the Update, frame: 10
Outside the for loop
Inside the Update, frame: 11
Inside the Update, frame: 12
.
.
.
Take note that on the first frame, we will have two executions of the for loop. This happens, because the first operation executes at the time the Start
method has to run and after that, the execution resumes at the end of each frame because of the EndOfFrameAsync
method. If instead we had used the NextFrameAsync
the output would be:
i Equals to: 0, on frame: 1
Inside the Update, frame: 1
Inside the Update, frame: 2
i Equals to: 1, on frame: 2
Inside the Update, frame: 3
i Equals to: 2, on frame: 3
.
.
.
Inside the Update, frame: 10
i Equals to: 9, on frame: 10
Inside the Update, frame: 11
Outside the for loop
Inside the Update, frame: 12
.
.
.
And with the FixedUpdateAsync
the output could be:
i Equals to: 0, on frame: 1
i Equals to: 1, on frame: 1
Inside the Update, frame: 1
i Equals to: 2, on frame: 2
Inside the Update, frame: 2
i Equals to: 3, on frame: 3
i Equals to: 4, on frame: 3
i Equals to: 5, on frame: 3
i Equals to: 6, on frame: 3
i Equals to: 7, on frame: 3
Inside the Update, frame: 3
.
.
.
I say could be, because this depends on how often our fixed update runs compared to how often our update runs. With the FixedUpdateAsync
, we delay our execution until the next fixed update.
The WaitForSecondsAsync
delays the execution until the provided argument that is a number that represents seconds has passed.
All those methods can take an optional argument, a cancellationToken that is used to cancel the execution, for example:
public class AwaitableExample : MonoBehaviour
{
private CancellationTokenSource cancellationTokenSource = new();
private async void Start()
{
while(!cancellationTokenSource.IsCancellationRequested)
{
Debug.Log($"Inside the while loop");
try
{
await UnityEngine.Awaitable.EndOfFrameAsync(cancellationTokenSource.Token);
}
catch (Exception e)
{
Debug.Log(e);
Debug.Log("cancellation requested");
}
}
Debug.Log("operation ended");
}
private void Update()
{
Debug.Log("Inside Update");
if(Keyboard.current.spaceKey.wasPressedThisFrame)
cancellationTokenSource.Cancel();
}
}
The async/await
We can of course use the async/await in any method we create. In the previous examples, I use it with the Start
method to show that it works with Unity’s event methods normally.
On a more theoretical note, the async/await keywords always go together for historical reasons. They were added from C# version 5. Back then, because a mechanism was needed to keep backwards compatibility so code that used the await word as a variable could still function, the async word was added, so that the compiler would treat the await as a keyword only when a method was declared as async.
Your IDE may show errors, but something like this actually compiles and executes, although it will be confusing for any of the readers nowadays:
public class AwaitableExample : MonoBehaviour
{
private int await = 0;
private void Update()
{
await ++;
Debug.Log($"{await}");
}
}
Executing Code In A Background Thread
We can use the Awaitable
to execute performance heavy code in a background thread, by using the BackgroundThreadAsync
and MainThreadAsync
methods. We have to take care that this code will not be using the Unity API, because Unity’s methods need to be executed in the main thread, but any performance heavy calculations that don’t use Unity’s API can be executed. Here is an example:
public class AwaitableExample : MonoBehaviour
{
private async void Start()
{
var result = await ExecutesInTheBackground(1_000_000);
Debug.Log($"Found the result in frame: {Time.frameCount}, the result is: {result}");
}
private async Awaitable<BigInteger> ExecutesInTheBackground(int n)
{
BigInteger a = 0;
BigInteger b = 1;
BigInteger c = 0;
if (n == 0)
return a;
await UnityEngine.Awaitable.BackgroundThreadAsync();
for (int i = 2; i <=n; i++)
{
c = a + b;
a = b;
b = c;
}
await UnityEngine.Awaitable.MainThreadAsync();
return b;
}
private void Update() => Debug.Log("Inside the Update Method");
}
This method finds the 1000000th Fibonacci number (something that is useful for every game :P) while the Update methods still execute and the calculation is done in a background thread, so that the main thread is not hindered by the calculation.
This example also shows the usage of the Awaitable<T>
class. With the generic version, we can return a result, something that we could not do with coroutines.
Using FromAsyncOperation
The static FromAsyncOperation
method of the Awaitable
returns an Awaitable
from an existing AsyncOperation
object.
The following example gets an Awaitable
from an AsyncInstantiateOperation
. For more information about asynchronously instantiating game objects, you can read my previous post Asynchronously Instantiate Objects with InstantiateAsync In Unity
public class AwaitableExample : MonoBehaviour
{
[SerializeField] private ToSpawn objectToSpawn;
private ToSpawn[] _spawnedObjects;
private AsyncInstantiateOperation<ToSpawn> result;
private async void Start()
{
var spawn = UnityEngine.Awaitable.FromAsyncOperation(AsyncInstantiation());
await spawn;
Debug.Log("At the end of the Start method");
}
private AsyncInstantiateOperation AsyncInstantiation()
{
Span<Vector3> positions = stackalloc Vector3[3];
Span<Quaternion> rotations = stackalloc Quaternion[3];
positions[0] = new Vector2(1, 1);
positions[1] = new Vector2(2, 1);
positions[2] = new Vector2(1, 3);
rotations.Fill(Quaternion.identity);
result = InstantiateAsync(objectToSpawn, 3, positions, rotations);
result.completed += Message;
return result;
}
private void Update() => Debug.Log("Inside the Update Method");
private void Message(AsyncOperation _) => Debug.Log("All objects Instantiated! ");
}
The important thing for this example is the Start
method, the spawn
variable is an Awaitable
that can be awaited.
The Awaitable Is Pooled
The Awaitables
in Unity are pooled objects, so that there are not too many allocations. They are reference types so that they can be referenced from different threads, but because they are pooled we have to be careful.
After their use they return to the pool and that means we cannot use them multiple times. Being pooled means that something like this:
while(true)
{
await Awaitable.NextFrameAsync();
}
will not allocate memory each frame, but being pooled also means that something like the following is not possible and has undefined behavior:
public class AwaitableExample : MonoBehaviour
{
private async void Start()
{
var myFoo = Foo1();
int firstAwait = await myFoo;
Debug.Log($"After first Await, firstAwait value {firstAwait}");
int secondAwait = await myFoo;
Debug.Log($"After second Await, secondAwait value: {secondAwait}");
}
private async Awaitable<int> Foo1()
{
Debug.Log("End of Foo1");
await UnityEngine.Awaitable.EndOfFrameAsync();
return 1;
}
}
After myFoo
has been awaited, it returns to the pool, it cannot be awaited a second time. For this reason, the above example will throw a null reference exception.
The AwaitableCompletionSource
The AwaitableCompletionSource
allows us to control the completion of an Awaitable. Here’s an example of its usage:
public class AwaitableExample : MonoBehaviour
{
private AwaitableCompletionSource<int> bar = new();
private async void Start()
{
Debug.Log("Beginning of Start");
int result = await Keypress();
Debug.Log($"Result at frame {Time.frameCount} with value: {result}");
}
private void Update()
{
if (Keyboard.current.aKey.wasPressedThisFrame)
{
bar?.SetResult(1);
bar = null;
}
if (Keyboard.current.sKey.wasPressedThisFrame)
{
bar?.SetResult(2);
bar = null;
}
}
private async Awaitable<int> Keypress()
{
int result = await bar.Awaitable;
return result;
}
}
The Awaitable here does not complete until we set the result with AwaitableCompletionSource.SetResult
. After that we can return the result normally.
What is important here, is the assignment of null to the bar
object, after setting the result. As before, Unity’s Awaitables are pooled, trying to set the result a second time will cause an error.
Creating WaitUntil with Awaitable
Finishing this post, I would like to show a small but useful script using Awaitables. This script, will behave like the WaitUntil of the coroutines.
private static async UnityEngine.Awaitable WaitUntil(Func<bool> condition, CancellationToken cancellationToken = default)
{
while(!condition())
await UnityEngine.Awaitable.EndOfFrameAsync(cancellationToken);
}
Use it to wait until something is true.
Conclusion
The Awaitable in Unity, can simplify the code by giving us a way to deal with asynchronous code in a better way than the coroutines, while also allowing us to execute computationally expensive code in a background thread. It is preferable than the Task of the .NET, as we don’t have to deal with the cancellation of the Tasks when we stop playing in the editor, because it is managed from the Unity ecosystem.
If you want to learn how you can await any type for writing asynchronous code synchronously with await, instead of manually subscribing to events, you can read my blog post: Await Anything In Unity With AwaitableCompletionSource And Custom Awaiters.
Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.