Unity: Leveling up with Async / Await / Tasks

John Tucker
4 min readMay 24, 2018

Using async / await / Tasks greatly simplifies writing asynchronous code in Unity.

In this article, the examples are all focused on fetching data from two REST API endpoints (users and todos); sample APIs provided by JSONPlaceholder.

The examples all use Unity’s WWW utility module to retrieve the JSON data and JsonUtility (and a small helper class) to parse it into an array of classes.

Once the data from both endpoints are fetched and parsed, the examples do something with both arrays of classes; in this case simply output the data using Debug.Log.

The variation between the examples comes in the form of how the asynchronous code is organized.

Coroutine

Let us first see how we would implement fetching users and todos using the familiar approach; coroutines.

A coroutine is like a function that has the ability to pause execution and return control to Unity but then to continue where it left off on the following frame.

— Unity Documentation — Coroutines

using System.Collections;
using System.Linq;
using UnityEngine;
public class DataController : MonoBehaviour
{
readonly string USERS_URL = "https://jsonplaceholder.typicode.com/users";
readonly string TODOS_URL = "https://jsonplaceholder.typicode.com/todos";
IEnumerator FetchData()
{
Todo[] todos;
User[] users;
// USERS
var www = new WWW(USERS_URL);
yield return www;
if (!string.IsNullOrEmpty(www.error))
{
Debug.Log("An error occurred");
yield break;
}
var json = www.text;
try
{
var userRaws = JsonHelper.getJsonArray<UserRaw>(json);
users = userRaws.Select(userRaw => new User(userRaw)).ToArray();
}
catch
{
Debug.Log("An error occurred");
yield break;
}
// TODOS
www = new WWW(TODOS_URL);
yield return www;
if (!string.IsNullOrEmpty(www.error))
{
Debug.Log("An error occurred");
yield break;
}
json = www.text;
try
{
var todoRaws = JsonHelper.getJsonArray<TodoRaw>(json);
todos = todoRaws.Select(todoRaw => new Todo(todoRaw)).ToArray();
}
catch
{
Debug.Log("An error occurred");
yield break;
}
// OUTPUT
foreach (User user in users)
{
Debug.Log(user.Name);
}
foreach (Todo todo in todos)
{
Debug.Log(todo.Title);
}
}
void Start()
{
StartCoroutine(FetchData());
}
}

Observations:

  • The use of coroutines (and yield statements) make handling asynchronous calls (using WWW) flow like synchronous code.
  • However, because one cannot put a yield inside a try-catch block, we have to create a complicated mix of asynchronous (inspecting www.error) and synchronous (try-catch) error handlers.
  • Because coroutines cannot return values, we have to create a huge monolithic coroutine (FetchData).
  • We are required to stack the requests; i.e., fetching users completes and then fetching todos starts.

Async / Await / Tasks

This approach draws heavily from the article Async-Await instead of coroutines in Unity 2017.

To enable this approach, one needs to change the project’s scripting runtime version using the menu choices (Unity 2018):

Edit > Project Settings > Player > Configuration > Scripting Runtime Version > .NET 4.x Equivalent

Also, one needs to install a plugin using:

Asset Store > Async Await Support

Much like coroutines and the yield statement, async methods and the await statement allow methods to be paused (waiting for result from an asynchronous call) and then resumed. The key difference, however, is async methods can return data.

note: If you have experience with modern JavaScript, this approach is just like async / await / Promises; where Tasks are just like Promises.

using System;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
public class DataAsyncController : MonoBehaviour
{
readonly string USERS_URL = "https://jsonplaceholder.typicode.com/users";
readonly string TODOS_URL = "https://jsonplaceholder.typicode.com/todos";
async Task<User[]> FetchUsers()
{
var www = await new WWW(USERS_URL);
if (!string.IsNullOrEmpty(www.error))
{
throw new Exception();
}
var json = www.text;
var userRaws = JsonHelper.getJsonArray<UserRaw>(json);
return userRaws.Select(userRaw => new User(userRaw)).ToArray();
}
async Task<Todo[]> FetchTodos()
{
var www = await new WWW(TODOS_URL);
if (!string.IsNullOrEmpty(www.error))
{
throw new Exception();
}
var json = www.text;
var todosRaws = JsonHelper.getJsonArray<TodoRaw>(json);
return todosRaws.Select(todoRaw => new Todo(todoRaw)).ToArray();
}
async void Start()
{
try
{
var users = await FetchUsers();
var todos = await FetchTodos();
foreach (User user in users)
{
Debug.Log(user.Name);
}
foreach (Todo todo in todos)
{
Debug.Log(todo.Title);
}
}
catch
{
Debug.Log("An error occurred");
}
}
}

Observations:

  • Because async methods return data, we break out the code for fetching users and todos; FetchUsers and FetchTodos.
  • In addition to returning data, async methods return errors through the returned tasks. This allows one to centralize error-handling through a top-level try-catch block.
  • Like the previous example, this example stacks fetching the data.

Task.WhenAll

The Task class has some utility methods for managing Tasks; in particular WhenAll that returns a new Task when all of the tasks in the provided array of Tasks complete.

A simple change in the previous code enables fetching users and todos to happen simultaneously.

...
try
{
var usersTask = FetchUsers();
var todosTask = FetchTodos();
await Task.WhenAll(usersTask, todosTask);
var users = await usersTask;
var todos = await todosTask;
...

Conclusion

Using the modern C# async / await / Task features greatly simplifies writing asynchronous code in Unity. And in particular, the approach is similar to modern JavaScript (making it particularly easy to learn — at least for me).

--

--

John Tucker

Broad infrastructure, development, and soft-skill background