Introduction to Asynchronous Programming in Unity

Dive deeper into the world of asynchronous programming in Unity by creating a fun town-builder game. By Johnny Thompson.

5 (2) · 2 Reviews

Download materials
Save for later
Share

Video games are loop-driven applications, meaning that some code executes before a frame draws to the screen. As a result, games often have many things to process in only a fraction of a second. If anything takes longer than a few milliseconds to complete, the frame rate drops and creates a poor player experience. No one likes choppy gameplay!

But sometimes, it’s unavoidable for tasks to take longer than a few milliseconds to complete. This is where asynchronous programming can really come in handy. You can start processes in parallel, allowing the user to continue with their game.

In this tutorial, you’ll create Wenderlich-Topia, a town-building game that allows users to construct roads and houses asynchronously.

Wenderlich-Topia game in action

In the process of building the game, you’ll learn how to:

  • Build roads with basic asynchronous methods.
  • Trigger actions once the build process is complete.
  • Build houses with multiple asynchronous methods, returning the construction costs once builds complete.
  • Cancel asynchronous build methods.
  • Handle errors and catch exceptions.

You’ll need Unity 2020.3 or later to build the game.

Note: This tutorial assumes you understand the fundamentals of working with C# and Unity. If you need to familiarize yourself with Unity, read the Get Started with Unity tutorial before proceeding. If you need a C# primer, then you should check out Beginning Programming with C#.

Getting Started

Download both the starter and final projects by clicking the Download Materials button at the top or bottom of the tutorial.

Explore both projects and familiarize yourself with the setup. In the true spirit of asynchronous programming, you could also explore the projects in parallel with working on the tutorial. :]

Setting Up the Starter Project

Open the starter project in Unity and navigate to the Assets/RW folder. Here are the subfolders you see and what they contain:

  • Audio: Background music and the cash register sound effect.
  • Prefabs: Complete and partial structures that get built in the project.
  • Scenes: Wenderlich-Topia demo scene.
  • ScriptableObjects: Data for the structures that you’ll build.
  • Scripts: Scripts for all game logic.
  • Sprites: Sprites for all isometric art assets.

Open the demo scene located in the Assets/RW/Scenes/Wenderlich-Topia project folder. Set the aspect ratio of the Game View to 16:9 Aspect so the UI displays correctly.

setting the aspect ratio to 16:9 for the Game View

Click Play to start the game. You’ll see the Welcome UI. Click the “X” icon to close the welcome message and observe the starting scene.

wenderlich-topia game starting screen

Advantages of Asynchronous Programming

The main advantage of asynchronous programming is that you can run multiple tasks simultaneously without blocking the overall execution of the current thread. In Unity, unless you’re getting into the Job System, most gameplay code is synchronous. That is, the code executes sequentially — one line, one instruction at a time. Once a task is finished, another one begins. Generally, this works fine but not always.

For example, in the sample project, a road can take a few seconds to build. If the code were purely synchronous, the player would have to wait for the road to finish building before doing anything else. Building a city would take a lot of time!

This is where asynchronous programming can help.

Writing Basic Asynchronous Code

Using asynchronous code, you’ll allow players to build multiple houses and roads in Wenderlich-Topia asynchronously.

First, you’ll define an entry point to serve as a bridge between synchronous and asynchronous code. Then, you’ll define specific tasks that run asynchronously. And finally, you’ll write sections of code that will run once your tasks are complete.

To summarize the game’s construction loop:

  1. Player selects a type of structure from the menu.
  2. Player clicks on the map where they want to place it.
  3. A temporary construction tile spawns where the structure will go.
  4. The program takes some time to build the structure.
  5. Once time is up, the program replaces the construction tile with the finalized road or house tile.
  6. The program plays a UI effect to display the cost of construction and adds it to the total city cost.
Note: Some of the behaviors listed above already exist in the starter project. You can check out the StructurePlacementManager.cs and UiManger.cs scripts to see how behaviors 1, 2 and 6 work. You will, however, implement behaviors 3-5 later in the tutorial.

Defining the async Keyword

The keyword async, as you might have guessed, defines a method that can run asynchronously. async goes between the access modifier (public, private, etc.) and the return type.

To define async, open RW/Scripts/Managers/ConstructionManager.cs in your editor of choice. Add async to BuildStructure on line 48. Your BuildStructure method should now look like this:

public async void BuildStructure(GameObject placementStructure, Vector3 buildPosition)
{
    // Method code goes here
}

This method serves as the entry point into the remaining asynchronous code you’ll implement in this project. This entry point can await the completion of other asynchronous methods before proceeding.

Take note of the async method’s return type. (In this method’s case it is void). Methods tagged with async can have one of three return types: void, Task and Task<TResult>. All three return types will be covered in this tutorial.

Avoid using void as a return type for async except for event handlers or as an entry point into asynchronous code. This is because, while async void can wait for asynchronous methods to complete, other methods can’t wait for async void to complete, and will continue executing, leading to unexpected and out-of-order code execution.

Additionally, async void can’t catch exceptions in the same way async Task can. You’ll learn to catch exceptions later in this tutorial. For now, start building some structures in Wenderlich-Topia.

Building Structures With Task

By now you’ve seen the term Task a few times. But what exactly does it mean? In asynchronous programming, Task is simply a class that represents a single operation that can run asynchronously.

An async method that returns Task cannot return any values. However, unlike void, Task does allow you to check an operation’s completion status. You can execute code after the Task method is finished.

So, to allow your BuildStructure method to know when a road is finished building, you’ll need a method that returns a Task. Add a new method called BuildRoadAsync to ConstructionManager.cs, after the existing BuildStructure method:

private async Task BuildRoadAsync(RoadBuildProperties roadProperties, Vector3 buildPosition)
{

}

Here, the RoadBuildProperties argument is a pre-declared ScriptableObject that contains information pertinent to the construction of the road and buildPosition is the position that the road will go on the map.

Note: While not required by the compiler, it’s best practice to add the suffix Async to asynchronous methods, to denote them as asynchronous.

Placing Temporary Construction Tiles

Next, you’ll start adding some functionality to the new BuildRoadAsync method. You do this the same way you would add steps to any regular method.

For starters, you’ll place a construction tile at the location where the road will be. In BuildRoadAsync, add the following line to instantiate constructionTilePrefab at the location the user clicks.

var constructionTile = Instantiate(constructionTilePrefab, buildPosition, Quaternion.identity, levelGeometryContainer);

Now that you’ve defined the first step of building a road, it’s time to call the method. You call asynchronous methods the same way you call any other method. In the BuildStructure method, add the following line after roadProperties gets defined and set:

var buildRoadTask = BuildRoadAsync(roadProperties, buildPosition);

buildRoadTask will now hold a reference to the returned Task from the BuildRoadAsync method. Save your script.

Test if this first step worked. Go to Unity and click Play. In play mode, select one of the roads from the build menu and place it in the game world. If everything is working correctly, you’ll see the construction tile spawn where you placed the road.

Placing construction tiles for roads in Wenderlich-Topia

Your code is still running synchronously. That’s because the program doesn’t know to wait for a task’s completion to run further steps.

Adding await to Start Running Asynchronous Code

The final component for writing basic asynchronous tasks is await. Using it with a Task tells the program to return to executing the synchronous code that called the async method. This synchronous code is therefore not blocked, and can continue running at least until it requires the returned Task code. The async Task will continue running in the background while the rest of the synchronous application code runs. Once the async Task method completes, the code after await will resume executing.

The above might sound confusing at first, so read it a few times. Or read this quote from the Microsoft await operator documentation:

The await operator doesn’t block the thread that evaluates the async method. When the await operator suspends the enclosing async method, the control returns to the caller of the method.

In Wenderlich-Topia, roads will take some time to build, yet the player will expect to be able to continue interacting with the game while the road is under construction. The code will await the road’s completion so that the rest of the game can continue running uninterrupted.

Once the road building task is complete, the asynchronous method resumes and does the following:

  • Removes the temporary construction tile.
  • Spawns the final road tile.
  • Plays a UI effect displaying the cost of the road.

To start running asynchronous code, you’ll await a call to Task.Delay(int millisecondsDelay) in the BuildRoadAsync method to allow a time delay of 2.5 seconds after the constructionTile spawns. Add the following to the end of the BuildRoadAsync method.

await Task.Delay(2500);

Now, any subsequent code in BuildRoadAsync will not run until the 2.5 second timer is up.

Next, remove the temporary construction tile and spawn the final road tile. To do this, add the code below after the await Task.Delay(2500); line in the BuildRoadAsync method:

Destroy(constructionTile);
Instantiate(roadProperties.completedRoadPrefab, buildPosition, Quaternion.identity, levelGeometryContainer);

The full road construction cycle is now in place. Next, you need to ensure that BuildStructure knows when the road is finished so it can show the relevant UI effects. The UI effect for showing the total build cost is already included in the Starter project. The UI Manager handles this responsibility for you.

Add the code below to the BuildStructure method, after the call to BuildRoadAsync, which you added previously:

await buildRoadTask;
uiManager.NewStructureComplete(roadProperties.roadCost, buildPosition);

The game will now wait for the BuildRoadAsync to complete and then show the cost of the road as a UI effect.

Go to Unity, enter play mode, and place some roads in the game world.

Building roads in Wenderlich-Topia

You’ll see the construction tile appear. Then, after 2.5 seconds, the final road will appear, followed by the UI effect showing the cost. Notice that while the road is being built, you can still interact with the game and even build multiple roads at once if you’re fast enough. :]

Quickly building roads in Wenderlich-Topia

Further Asynchronous Programming Concepts

In the following two sections, you’ll learn some more asynchronous programming features by building houses. In the end, residents of Wenderlich-Topia will have a place to live.

To build a house, implement the following steps:

  1. Place a temporary construction tile where the house will get built.
  2. Build a house frame.
  3. Build a roof.
  4. Build a fence.
  5. Finalize the house.
  6. Play UI effect to display final cost.

Steps 2-4 are individual tasks, but they won’t happen in sequence. Once you construct the frame, you can also construct the roof and fence at the same time. Once both the roof and fence are complete, you can finalize the house.

Returning Values From Tasks

Remember the three types of return values you learned about earlier? In the following section, you’ll learn how to use the third type, Task<TResult> to return values from Tasks.

One key difference between building houses and roads in Wenderlich-Topia is the cost. The player won’t know the final cost of building a house at the time of construction, but they will know the estimated cost shown on the Build House UI button.

In ConstructionManager.cs, define a BuildHouseAsync method to schedule individual tasks. Each task calculates the task cost by multiplying the task completion time by a wage value. At the end of BuildHouseAsync, you’ll:

  • Sum the cost of all tasks.
  • Return the total cost to BuildStructure.
  • Display the total cost UI effect.
Note: For this project, each task will take a random amount of time between 1.5 and 3.25 seconds. The wage is set at $30.

You’ll now implement the method that returns Task<TResult>. This return type has the same features as a standard Task, with the important benefit of being able to return a defined value within the angle brackets. Like a standard non-void method, async Task<TResult> must return a value of the defined type.

Now, define the BuildHouseAsync method that returns a Task<int>, as the total house cost will be represented as an integer type:

private async Task<int> BuildHouseAsync(HouseBuildProperties houseBuildProperties, Vector3 buildPosition)
{
    var constructionTile = Instantiate(constructionTilePrefab, buildPosition, Quaternion.identity, levelGeometryContainer);

    return 100;
}

The method spawns the temporary construction tile and returns a value of $100. The value is temporary. You’ll calculate the actual value of the house once it’s finished. Too bad because a $100 house would definitely be a steal.

Next, in BuildStructure, make a call to BuildHouseAsync after you set houseProperties. Store the result as a Task, then wait until it’s complete:

var buildHouseTask = BuildHouseAsync(houseProperties, buildPosition);
await buildHouseTask;

Next, in the same method, calculate the cost of the house by accessing Result on buildHouseTask. You’ll use that value to display the UI effect on the screen.

var houseCost = buildHouseTask.Result;
uiManager.NewStructureComplete(houseCost, buildPosition);

Return to Unity, enter play mode, and attempt to build a new house in the game world. First, you’ll see the temporary construction tile placed on the map, immediately followed by the UI effect displaying $100, the placeholder cost.

Placing temporary tiles for houses in Wenderlich-Topia

Awaiting Multiple Tasks

Next, you’ll create tasks for building each piece of the house. Luckily, the steps to build the house are all the same, so you can define one task to use for all of them, passing in a different prefab for each type.

In the ConstructionManager.cs script, define a new method, BuildHousePartAsync, which will return a Task<int> (the integer value of the cost):

private async Task<int> BuildHousePartAsync(HouseBuildProperties houseBuildProperties, GameObject housePartPrefab, Vector3 buildPosition)
{
    var constructionTime = houseBuildProperties.GetConstructionTime();
    await Task.Delay(constructionTime);
}

This method gets the time it takes to build a certain part, then waits for the specified duration. After the delay, that part of the house is complete. You may be curious how the delay is calculated. Well, that is a helper property that is defined on the HouseBuildProperties.cs ScriptableObject which is included in the project files.

public int GetConstructionTime() => Random.Range(minConstructionTime, maxConstructionTime);

After defining the BuildHousePartAsync method you may notice an error in your IDE. (Not all code paths return a value). That’s because the BuildHousePartAsync method isn’t returning a value yet but you have declared it with a return type of Task<int>. You’ll add this next.

In the same method after await Task.Delay(constructionTime);, spawn the housePartPrefab and calculate the cost of the task. Then, return that cost as an integer.

Instantiate(housePartPrefab, buildPosition, Quaternion.identity, levelGeometryContainer);
var taskCost = constructionTime * houseBuildProperties.wage;
return taskCost;

In the code block above, you’re:

  • Calling Instantiate to instantiate a new GameObject based on the relevant house part prefab and placing it as the correct location.
  • Calculating the cost by multiplying the constructionTime by the set wage.
  • Returning the calculated cost as an integer.

Building a House by Parts

Now that you have sufficient logic to build each part of the house, you need to define the tasks for each of these parts: frame, roof and fence. Call await for the tasks within BuildHouseAsync after the construction tile spawns, and before the return call.

Start with the frame of the house. You can’t start other tasks until the frame is complete, so any subsequent code will await the completion of this task:

Task<int> buildFrame = BuildHousePartAsync(houseBuildProperties, houseBuildProperties.completedFramePrefab, buildPosition);
await buildFrame;

Next, you can begin building the roof and fence. These tasks can take place at the same time, so don’t await anything just yet. Add this code immediately after the previous block you just added (in the same BuildHouseAsync method).

Task<int> buildRoof = BuildHousePartAsync(houseBuildProperties, houseBuildProperties.completedRoofPrefab, buildPosition);
Task<int> buildFence = BuildHousePartAsync(houseBuildProperties, houseBuildProperties.completedFencePrefab, buildPosition);

The final step of building the house is to finalize the house. However, you can’t start this step until both the roof and fence are done. This is where you’ll learn a new technique – how to await multiple Tasks.

Add this line after the definition of the previous two tasks you just added in BuildHouseAsync:

await Task.WhenAll(buildRoof, buildFence);

Task.WhenAll(Task[] tasks) will wait until all defined tasks are complete. In this case, the code will continue executing once both the roof and fence are up.

Now, in the same method, call and await a task to finalize the house. This task will place down the final, completed house prefab.

Task<int> finalizeHouse = BuildHousePartAsync(houseBuildProperties, houseBuildProperties.completedHousePrefab, buildPosition);
await finalizeHouse;

Cleaning up Construction

After the previously added code runs, the house is complete. The next steps are to destroy the temporary construction tile, calculate the total house cost, return the result and remove that temporary return value of $100. You’ll use Task.Result to get the cost of each task. Add the below code to BuildHouseAsync by replacing the existing return 100; line with:

Destroy(constructionTile);

var totalHouseCost = buildFrame.Result + buildRoof.Result + buildFence.Result + finalizeHouse.Result;
return totalHouseCost;

And that’s all it takes to build a house. In this game, at least… :]

Now, go back to Unity, enter play mode, and test building some houses. You’ll know everything is working if you have a house at the end with a roof, a frame and a fence around it. You can refer to the code in the final project files in case you are stuck.

Building houses in Wenderlich-Topia

Look closely, and you’ll see that sometimes the fence finishes before the roof, and other times, it’s the other way around. If you’re really lucky, the ‘builders’ (a.k.a. random time returning Tasks!) are synchronized, and they complete at the same time.

Improving Your Asynchronous Code

Now that you know how to write basic asynchronous code, you need to make some considerations to make your asynchronous code safer and more flexible.

Cancelling Tasks

You need to consider how to handle task cancelation because there may be an occasion when you need to stop an active task. For example, you may want to add a feature that allows the player to cancel a building’s construction. Or if you’re running the game in the Unity Editor and you stop the project from running. Spoiler alert: you’ll actually implement both of these features! :]

Replicate this use case by going back to Unity and running the application. Begin building a house, then exit play mode before the house is finished building.

Exiting play mode before house is built still results in the house getting constructed

You’ll notice something strange happens. Even while the game isn’t running, the house is still being built. You’ll even see these new objects in the scene hierarchy, even when it’s not in the play mode. I bet you didn’t think this could ever happen. :]

Scene hierarchy after stopping the game midway through construction

Since you don’t want this to happen to your players, you’ll need to put in a measure that will allow your game to cancel tasks.

Note: If you tried the magical “GameObject spawning whilst Editor is stopped” scenario above, before continuing, delete the spawned objects in this step from the scene Hierarchy. You’ll find the spawned house parts as a child of LevelGeometry, the UI effect as a child of UI > WorldSpaceCanvas and the sound effect as a child of Managers > BackgroundAudio, as highlighted in the screenshot above.

Cancelling a Task Efficiently

To cancel a Task partway through execution, you’ll need a CancellationToken. In the ConstructionManager.cs script, declare a private CancellationTokenSource and then initialize it in Start.

private CancellationTokenSource cancellationTokenSource;

private void Start()
{
    cancellationTokenSource = new CancellationTokenSource();
}

Now that the variable is declared, get a reference to CancellationToken in the beginning of BuildStructure like below:

var cancellationToken = cancellationTokenSource.Token;

Modify the signature of all three async Task methods to take in a CancellationToken as the last parameter. For example, BuildRoadAsync should look like this:

private async Task BuildRoadAsync(RoadBuildProperties roadProperties, Vector3 buildPosition, CancellationToken cancellationToken)

Do the same for BuildHouseAsync and BuildHousePartAsync.

Next, modify the calls to each of these methods to pass in the cancellationToken. There are two such calls in BuildStructure, and four calls in BuildHouseAsync. As an example, for the call to BuildRoadAsync, you’ll pass in the new cancellationToken like this:

var buildRoadTask = BuildRoadAsync(roadProperties, buildPosition, cancellationToken);

Similarly, you need to update the calls for BuildHouseAsync in BuildStructure and BuildHousePartAsync in BuildHouseAsync.

Finally, pass the cancellationToken into the two Task.Delay calls. As an example, here is what you’ll do in the BuildHousePartAsync method:

await Task.Delay(constructionTime, cancellationToken);

Find and update the Task.Delay call in BuildRoadAsync too.

Now that the CancellationToken is everywhere it needs to be, cancellationTokenSource needs to be used when it is time to actually cancel a task(s). Add the following line to the OnDisable() method:

cancellationTokenSource.Cancel();

OnDisable() is always called when the application stops. (for example, when you stop the game in the Unity Editor). Doing this will cancel any currently running task that passed in the CancellationToken reference.

Go back to Unity, enter play mode, and start building a house. Exit play mode before the house is finished, and you’ll see that no extra objects spawn outside of play mode.

Building houses in Wenderlich-Topia without any extra objects spawning outside of play mode

But what if the user wants to cancel a task halfway through? Let them! Using CancellationToken, a user will be able to cancel any Task while the application is still running. As the welcome UI says, the user can press the Escape key to cancel placing a structure. You can achieve this functionality by adding the following code to the Update() method:

if (Input.GetKeyDown(KeyCode.Escape))
{
    cancellationTokenSource.Cancel();
    cancellationTokenSource.Dispose();
    cancellationTokenSource = new CancellationTokenSource();
}

Now, when the player presses Escape:

  • CancellationTokenSource cancels any tasks that references its token.
  • CancellationTokenSource is disposed of.
  • A new CancellationTokenSource is created.

Return to Unity and run the game. Place a house or a road on the map, then press Escape. Construction stops immediately.

Building houses in Wenderlich-Topia

Catching Exceptions

When you pressed Escape, you may have noticed that an exception showed up in the debug console. This is because whenever a task gets canceled at runtime, an exception is thrown by CancellationToken.

Async cancellation exception

In this section, you’ll learn how to catch these exceptions and implement your own custom error handling for canceled tasks. This is easy to do using CancellationToken.

In the BuildStructure method, surround the calls to BuildRoadAsync and BuildHouseAsync in a try...catch block. Here’s what the code should look like once wrapped in a try...catch block for the BuildHouseAsync Task. (Don’t forget to do the BuildRoadAsync one too):

var buildHouseTask = BuildHouseAsync(houseProperties, buildPosition, cancellationToken);

try
{
    await buildHouseTask;
    var houseCost = buildHouseTask.Result;
    uiManager.NewStructureComplete(houseCost, buildPosition);
}
catch
{
    Debug.LogWarning("Building House Cancelled");
}

The task is now awaiting inside try. If it completes successfully, the remaining code in try will run, displaying the appropriate UI effect. If Task throws an exception through CancellationToken, the code inside catch will run instead. For simplicity and the purposes of this tutorial, a warning is printed to the console. For your own project, you might want to handle this in a better way.

Go back to Unity, enter play mode, and test your exception catching. Cancel a house halfway through its building process. Rather than a system-generated error, you now see the custom warning you printed through code.

Async cancellation warning

Choosing Asynchronous Programming vs. Coroutines

Now that you’ve learned about C# asynchronous methods, you might have found similarities to coroutines in Unity. However, while they are similar, key differences between the two make each of them work better in certain situations.

Coroutines are a type of method that allows code to be executed over a stretched period of time. Often, programmers use coroutines for moving GameObjects, setting countdown timers or fading textures between colors. Basically, they suit gameplay programming quite well.

You can learn more about how to use coroutines in the official Unity documentation.

Advantages of async Over Coroutines

One of the biggest advantages of async over coroutines is its ability to return values. For example, in Wenderlich-Topia, the cost of building the house is calculated asynchronously while the house is being built. Once the house is complete, the code returns the total cost of the house’s value, and the result is available in the Task result variable. Coroutines can’t return any values.

Async methods can also run on threads that aren’t the main thread. This means they can continue working on complex tasks without impacting performance on the main thread.

While coroutines also run in the background, they run on the main thread. This means they could impact the performance of the game if they’re doing complex operations.

Finally, a major advantage of async is that many external libraries and APIs also use async. This makes it easy to integrate others’ asynchronous methods into your code.

Deciding When to Use Coroutines

Coroutines are best used for set it and forget it scenarios when you need to execute code over time but don’t need any results and don’t want to manage your code. Once the coroutine is complete, it can trigger some additional behaviors to happen. (An example of this being the one described earlier – changing the color of a GameObject over time).

Additionally, because coroutines are a Unity feature, they’ll stop when the program ends. You won’t have the same problem with asynchronous code, which would continue running even after the application stops. Unity handles this cancellation for you. As you saw before, the Unity engine does not handle the cancelation of async Tasks.

Coroutines also integrate well with other Unity features and a game’s update loop. With coroutines, you can simply call yield return break; to halt a method’s execution and return to that point in the method on the next frame. Put a yield inside a for or while loop and you can make things move or change over time.

You can achieve similar behavior with async in Unity, but of course, being a C# language feature, it wasn’t built with this use case in mind, so the code is a bit messier when trying this kind of scenario with Unity.

Where to Go From Here?

You can use the Download Materials button at the top and bottom of this tutorial to download the starter and final projects.

Now, you hopefully understand how to use asynchronous methods in Unity and know when to use them (and when not to).

To increase your skills, practice implementing what you learned in some of your own projects.

As a next step, you could graduate to integrating with other asynchronous features such as Unity’s Addressable Asset System. This asynchronous way of loading assets into your game is preferable to loading assets from the Resources folder. For more information on the Addressable Asset System, check out Introduction to Modding Unity Games with Addressables from raywenderlich.com.

You can also learn about writing multithreaded code using something like Unity’s C# Job System. Check out the raywenderlich.com tutorial Unity Job System and Burst Compiler: Getting Started on how to create a wave generator using Unity’s C# Job System.

We hope you enjoyed this tutorial. If you have any questions or comments, please feel free to join the forum discussion below!