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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Introduction to Asynchronous Programming in Unity
35 mins
- Getting Started
- Setting Up the Starter Project
- Advantages of Asynchronous Programming
- Writing Basic Asynchronous Code
- Defining the async Keyword
- Building Structures With Task
- Placing Temporary Construction Tiles
- Adding await to Start Running Asynchronous Code
- Further Asynchronous Programming Concepts
- Returning Values From Tasks
- Awaiting Multiple Tasks
- Building a House by Parts
- Cleaning up Construction
- Improving Your Asynchronous Code
- Cancelling Tasks
- Cancelling a Task Efficiently
- Catching Exceptions
- Choosing Asynchronous Programming vs. Coroutines
- Advantages of async Over Coroutines
- Deciding When to Use Coroutines
- Where to Go From Here?
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.
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.
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.
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.
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:
- Player selects a type of structure from the menu.
- Player clicks on the map where they want to place it.
- A temporary construction tile spawns where the structure will go.
- The program takes some time to build the structure.
- Once time is up, the program replaces the construction tile with the finalized road or house tile.
- The program plays a UI effect to display the cost of construction and adds it to the total city cost.
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.
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.
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:
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.
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. :]
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:
- Place a temporary construction tile where the house will get built.
- Build a house frame.
- Build a roof.
- Build a fence.
- Finalize the house.
- 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.
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.
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 setwage
. - 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.
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.
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. :]
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.
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.
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.
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
.
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.
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!