UniTask and Asynchronous Processing
What Is UniTask?
UniTask is a library for Unity and used to handle asynchronous processing efficiently. It is provided with the MIT license, so you can use it even for commercial purposes for free as long as you follow the license.
Asynchronous processing is frequently used to implement dialogue scenes, and ScenarioFlow uses UniTask to handle asynchronous processing. Therefore, you have to learn about UniTask in order to make use of ScenarioFlow.
What Is Asynchronous Processing?
Asynchronous processing means that you operate a process independently of completion of other processes. Such a processing is called asynchronous processing.
Basically, programs are executed one by one, which is called synchronous processing. In synchronous processing, the next process will never start until the previous one finishes. On the other hand, if an asynchronous processing starts, the next processing will start immediately without waiting for its completion.
A good example for asynchronous operation is loading scene. A process for loading some data is often executed in games. Then, if such a process is executed synchronously, the screen will appear to freeze because other processes can’t be executed until the loading process finishes.
If the loading process is executed asynchronously to resolve the problem, it will be possible to start other processes regardless of the completion of its process. So a progress bar for the loading can be shown to the player to reduce their stress, for example.
Eventually, you can think of asynchronous processing as what is applied to processes which take some time to complete.
Why UniTask?
You can choose Coroutine or Task (C# standard) as an option other than UniTask to handle asynchronous processing.
We have the two main reasons to choose UniTask from the options. Firstly UniTask was developed by optimizing the performance of Task (standard function in C#) for Unity, secondly it provides many useful functions for game developments.
Import UniTask
We are going to learn how to use UniTask by writing codes later. But first of all, let’s import UniTask to a project to be able to use it.
The easiest way to install UniTask is to import a .unitypackage file to your project. You can download a .unitypackage file at the Releases section on the GitHub page which you can access with the following URL.
UniTasks with No Return Values
Delayed Message Program
As our first asynchronous programming with UniTask, we are going to develop an asynchronous method which displays a delayed message on the console, where we specify the waiting time and the message.
Follow the steps below to run a sample code.
- Add a new empty object on the hierarchy window
- Create the following C# script, and attach it to the object
- Run it
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
Debug.Log("Program started...");
LogDelayedMessageAsync(3.0f, "Hello, UniTask1!").Forget();
LogDelayedMessageAsync(3.0f, "Hello, UniTask2!").Forget();
LogDelayedMessageAsync(3.0f, "Hello, UniTask3!").Forget();
}
private async UniTask LogDelayedMessageAsync(float waitTime, string message)
{
await UniTask.Delay(TimeSpan.FromSeconds(waitTime));
Debug.Log(message);
}
}
C#After you follow the steps above to run the program, the three messages will be output on the console after 3 seconds.
Let’s make sure that the timing of message showing will change if we specify the different waiting times.
private void Start()
{
Debug.Log("Program started...");
LogDelayedMessageAsync(3.0f, "Hello, UniTask1!").Forget();
LogDelayedMessageAsync(2.0f, "Hello, UniTask2!").Forget();
LogDelayedMessageAsync(1.0f, "Hello, UniTask3!").Forget();
}
C#When an asynchronous process is executed, the next process can start without waiting for its completion.
The 3 messages are shown simultaneously in the first example, and the 3 messages are shown in the reverse program order in the second example. It indicates that the following processes are executed without waiting for the completion of the previous programs.
The name of asynchronous methods
It is an understandable convention that an asynchronous method name ends with “Async” in order to clarify that it is an asynchronous method.
The UniTask Struct
When we create an asynchronous method with UniTask, basically the return type of that method is UniTask
. The UniTask
struct was developed by optimizing the C# standard Task
for Unity, and it is defined in the Cysharp.Threading.Tasks
namespace.
One feature of an asynchronous method, in other words an asynchronous process, is to be able start the next process without waiting for its completion. In addition to that, it is possible to wait for the completion of an asynchronous process by specifying the UniTask
struct as the return type of the asynchronous method. In this context, we call an asynchronous method whose return type is the UniTask
struct simply UniTask.
At this point, when we say “Unitask”, it has one meaning of the following three depending on the context:
- UniTask as a library
UniTask
as a return type- UniTask as a asynchronous process/method
2 UniTasks, LogDelayedMessageAsync
and UniTask.Delay
, are called in the sample code. The latter is a UniTask provided by the UniTask library, which inserts a specified time delay. As mentioned earlier, when we call a UniTask, we can choose to wait for its completion before starting the next process and can also choose not to wait. In the sample code, completion of the former is not awaited while completion of the latter is awaited.
UniTask is not equivalent to asynchronous method
UniTask is a kind of asynchronous method, however, an asynchronous method is not always a UniTask. For example, a method whose return type is the Task
class in which the UniTask
struct originates is also a kind of asynchronous method. Furthermore, a method whose return type is void
can also be a asynchronous method.
By the way, we can’t wait for completion of a asynchronous method with void
return type unlike that with Task
or UniTask
return type.
NOT Wait for Completion of UniTasks
You have to attach the Forget
method calling if you call an UniTask and don’t wait for its completion; otherwise an warning message is given.
You can also choose to assign an UniTask to an underscore instead of the Forget
method calling.
private void Start()
{
Debug.Log("Program started...");
_ = LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
LogDelayedMessageAsync(2.0f, "Hello, UniTask2!").Forget();
LogDelayedMessageAsync(1.0f, "Hello, UniTask3!").Forget();
}
C#Wait for completion of UniTasks
We use the await
operator to wait for the completion of an UniTask.
In the LogDelayedMessageAsync
method, the Debug.Log
method is called after the UniTask.Delay
method is called. Then, the Debug.Log
method will not called until the process of the UniTask.Delay
method finishes because the UniTask.Delay
is called with the await
operator.
Also, if you want to wait for completion of an asynchronous process in a certain method, you have to attach the async
modifier to that method.
Let’s try to wait for completion of the LogDelayedMessageAsync
method. We change the Start
method in the sample code as follows.
private async void Start()
{
Debug.Log("Program started...");
await LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
await LogDelayedMessageAsync(2.0f, "Hello, UniTask2!");
await LogDelayedMessageAsync(1.0f, "Hello, UniTask3!");
}
C#The async modifier is attached to the Start method since the await operators are used inside the method.
Unlike the first example and the second one, the messages are shown one by one in the program order. That is because the completion is awaited due to the await
operator every time the LogDelayedMessageAsync
method is called.
Awaitable objects
The reason why we can wait for completion of UniTasks with the await
operator is that the GetAwaiter
method, which returns an object called Awaiter, is defined for the UniTask
type. Awaiter
and GetAwaiter
are functions in C#, so not only UniTask objects but also objects that have their GetAwaiter
method are awaitable.
UniTask provides Awaiter
s and GetAwaiter
s for some objects. Therefore you can await some objects other than UniTask objects. For example, you can await objects defined in DOTween, which is a library for creating script-based animations. The cooperation between DOTween and UniTask is so powerful.
Awaiting through Variables
You can wait for the completion of an UniTask after storing it in a variable. Let’s try the following code.
private async void Start()
{
Debug.Log("Program started...");
var task1 = LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
var task2 = LogDelayedMessageAsync(2.0f, "Hello, UniTask2!");
var task3 = LogDelayedMessageAsync(1.0f, "Hello, UniTask3!");
await task1;
await task2;
await task3;
}
C#UniTasks are awaited in this example; nevertheless the messages are not shown in the program order, which is different from the example in the previous section.
The reason is that the process of an UniTask starts right after it is assigned to a variable. Basically, the process of an UniTask doesn’t start when it is awaited, but when it is called. The role of the await
operator is to specify that the completion of the UniTask is awaited.
You should await a method call directly if you want to wait for its completion at the same time you call the method. Or if you don’t want to call a method and wait for its completion simultaneously, you should await the method through a variable.。
UniTasks are awaited only once
Generally one UniTask can be awaited only once. The following code causes an error. If you have any requirement of awaiting the same UniTask two ore more times, you need to use the special method, Preserve
.
var task1 = LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
await task1;
await task1;
C#Start process of UniTasks when they are awaited
You can delay the timing of the execution of an UniTask until it is awaited by using the UniTask.Defer
method. The result of running the following code is the same as the example in the previous section. You can use the UniTask.Lazy
to await an UniTask many times, by the way.
private async void Start()
{
Debug.Log("Program started...");
var task1 = UniTask.Defer(() => LogDelayedMessageAsync(3.0f, "Hello, UniTask1!"));
var task2 = UniTask.Defer(() => LogDelayedMessageAsync(2.0f, "Hello, UniTask2!"));
var task3 = UniTask.Defer(() => LogDelayedMessageAsync(1.0f, "Hello, UniTask3!"));
await task1;
await task2;
await task3;
}
C#UniTaskVoid
If an UniTask will never be awated, you can use the UniTaskVoid
type, which is the lightweight version of the UniTask
type. For example, the following code works, and running it results in the same result as that of the first example. Note that you can’t await UniTasks with the UniTaskVoid
type.
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
Debug.Log("Program started...");
LogDelayedMessageAsync(3.0f, "Hello, UniTask1!").Forget();
LogDelayedMessageAsync(3.0f, "Hello, UniTask2!").Forget();
LogDelayedMessageAsync(3.0f, "Hello, UniTask3!").Forget();
}
private async UniTaskVoid LogDelayedMessageAsync(float waitTime, string message)
{
await UniTask.Delay(TimeSpan.FromSeconds(waitTime));
Debug.Log(message);
}
}
C#If you await any asynchronous operations in the Start
method, you can specify the UniTaskVoid
type as the return type. For example, the following code results in the same result as that of the example in which the return type is UniTask.
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
await LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
await LogDelayedMessageAsync(2.0f, "Hello, UniTask2!");
await LogDelayedMessageAsync(1.0f, "Hello, UniTask3!");
}
C#Now we can say that the UniTaskVoid
type is the UniTask version of the void type.
UniTask with Return Values
Delayed Adder Method
In the previous chapter, we developed the UniTask that shows a message after it waits for the specified number of seconds. We can call this UniTask and wait for its completion, but it returns no value.
In this section, we are going to deal with an UniTask with a return value.
As an example, let’s develop a delayed adder method. Create the following script and run it.
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
var result = await AddNumbersAsync(1, 2);
Debug.Log($"Result: {result}");
}
private async UniTask<int> AddNumbersAsync(int n1, int n2)
{
await UniTask.Delay(TimeSpan.FromSeconds(2.0f));
return n1 + n2;
}
}
C#It takes a few seconds for the AddNumbersAsync
method to finish, and after it finishes, it returns the sum of the two integers passed as the two arguments of the method.
The UniTask<T> Generic Struct
We specify the UniTask<T>
generic struct as the return type of a method to deal with an UniTask with a return value, where T
is the type of the return value.
We can treat the async
modifier and the await
operator in the same way as UniTasks with no return value. If we use the await
operator in a certain method, we have to attach the async
modifier to the method.
The return value is received by calling an UniTask and assigning it to a variable with the await
operator.
It is not so difficult to deal with UniTasks with a return value if you understand how to deal with UniTasks without any return values. When the process of a generic version of UniTask starts and the limitation regarding the number of awaiting are the same as normal UniTasks. The only difference is whether it provides its return value.
Awaiting through Variables
We can wait for the completion of an UniTask with a return value as well as an UniTask without any return values. The following sample code results in the same result as that of the previous example.
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
var task = AddNumbersAsync(1, 2);
var result = await task;
Debug.Log($"Result: {result}");
}
C#Delay the timing of starting an UniTask with return values
The process of the AddNumberAsync
method starts right after it is assigned to the task
variable. If you want to delay the start of the process until it is awaited, you can use the UniTask.Defer<T>
generic method, which is the generic version of the UniTask.Defer
method. (You can also use the UniTask.Lazy<T>
method to await the process many times.)
Exercise
Named Timer
Develop a timer that counts the specified number of seconds. The timer name and the remaining time have to be shown on the console every second.
Use the template below.
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
var timerA = StartTimerAsync("Timer A", 3);
var timerB = StartTimerAsync("Timer B", 5);
await timerA;
await timerB;
Debug.Log("Program finished!");
}
private async UniTask StartTimerAsync(string name, int seconds)
{
}
}
C#N-Inputs Delayed Adder
Develop an adder that receives any number of decimals and returns the sum of them. Additionally, it requires 1 second for each decimal passed to finish calculation.
Use the template below.
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
var result1 = await SumAsync(new float[] { 1.6f, 5.4f, 3.0f });
Debug.Log($"Result1: {result1}");
var result2 = await SumAsync(new float[] { 2.2f, -5.3f, 2.5f, -7.9f, -6.5f });
Debug.Log($"Result2: {result2}");
Debug.Log("Program finished!");
}
private async UniTask<float> SumAsync(IEnumerable<float> numbers)
{
}
}
C#Example Answers
Named timer
private async UniTask StartTimerAsync(string name, int seconds)
{
Debug.Log($"{name} started...");
while (seconds > 0)
{
Debug.Log($"{name}: {seconds} sec.");
seconds--;
await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
}
Debug.Log($"{name} finished!");
}
C#N-Inputs delayed adder
private async UniTask<float> SumAsync(IEnumerable<float> numbers)
{
await UniTask.Delay(TimeSpan.FromSeconds(1.0f * numbers.Count()));
return numbers.Sum();
}
C#
Comments