This tutorial shows how to make the most out of coroutines in Unity.
- Introduction
- Part 1. Synchronous Waits
- Part 2. Asynchronous Coroutines
- Part 3. Synchronous Coroutines
- Part 4. Parallel Coroutines
- Conclusion
Introduction
Each Unity script comes with two important functions: Start
and Update
. While the former is invoked when an object is enabled after being created, the latter is called during each frame. By design, the next frame cannot start until Update
has terminated its job. This introduces a strong design limitation: Update
cannot easily model events that last for more than one frame.
To be completely honest, every custom behaviour you can imagine can be implemented using Start
and Update
. However, events that happens over multiple frames (such as animations, dialogues, waits, …) are harder to code. This is because their logic cannot be written in a consistent flow. It has to be fragmented, spread over multiple frames. This often leads to code that is not just harder to write, but also harder to maintain.
What would be perfect is to have something that can be executed in parallel, unconstrained from the short life of a single frame. If you are a programmer, this will probably resonate with the concept of thread. Threads are pieces of code that are executed in parallel. Working with threads, however, is very tricky. This is because when multiple threads are working on a shared variable without any limitation , there can be issues. By design, Unity strongly discourages the use a threads. However, it offers a good compromise: coroutines. Coroutines are functions which can lasts more than one frame. Moreover, they come with expressive constructs to interrupt and resume their executions due to arbitrary conditions.
Coroutines are normal C# functions which return IEnumerator
. To execute such a function like a coroutine (and not like a traditional function), one has to use the StartCoroutine
method (UnityDoc). For instance:
void Start () { // Execute A as a coroutine StartCoroutine( A() ); } IEnumerator A () { ... }
executes A
as a coroutine. The method StartCoroutine
terminates immediately, but spawns a new coroutine that is executed in parallel.
❓ Why coroutines must have IEnumerator has their return type?
Coroutines in Unity are based on C# iterators, which have been covered in the first part of this tutorial: Iterators in C#: yield, IEnumerable and IEnumerator. Iterators are an expressive way to model objects that can be iterated upon, such as lists and other collections. In C#, this is done using the interface IEnumerator
.
Unity treats coroutines as “lists of code”. Asking for a coroutine to produce its next element has the meaning of executing its next step. As such, each function that needs to last more than one frame is required to use IEnumerator
as its return type.
❓ Are coroutines executed in parallel?
No. Unity is generally not thread safe. Which means that running two pieces of code in parallel can potentially break your game. As such, during each frame, Unity executes a bit of each active coroutine, sequentially.
❓ Can I still have threads in Unity?
Yes. Several parts of Unity run in separate threads (audio, Mechanim, Skinning, …). While GameObject
s and MonoBehaviour
s are not designed to be thread safe, this does not mean they cannot be accessed by threads. Each access to shared resource (like a game object or a variable) needs to be properly controlled, to avoid inconsistent results. Threads can indeed be used, but for the vast majority of your everyday applications the cost of micromanaging accesses to shared resources is simply not worth it.
Synchronous Waits
If you have used coroutines before, it is likely that you have already encountered the class WaitForSeconds
(UnityDoc). Like all the other classes that extend YieldInstruction
, it allows to temporarily suspend the execution of a coroutine. When coupled with yield return
, WaitForSeconds
provides an expressive way to delay the execution of the remaining code.
The following piece of code shows how it can be used within a coroutine:
IEnumerator A() { ... yield return new WaitForSeconds(10f); ... }
The diagram above, loosely inspired by the sequence diagrams in UML (Wikipedia), illustrates the effect of WaitForSeconds
. When invoked in a coroutine (called A
) it suspends its execution until a certain amount of time has passed. This type of wait is called synchronous, because the coroutine waits for for another operation to complete.
❓ What does yield mean?
The keyword yield
is used by C# to write iterators. Methods that return IEnumerator
can be treated as “collections” and can be iterated using a foreach
loop. In this context, yield
is the way to return an object in the for loop. This is discussed in great detail in Iterators in C#: yield, IEnumerable and IEnumerator.
❓ Hey! This is not how a Sequence Diagram works in UML!
I know. And that is not a question. 💁
Asynchronous Coroutines
Unity also allowed to start new coroutines within an existing coroutine. The most simple way in which this can be achieved, is by using StartCoroutine
. When invoked like this, the spawned coroutine co-exist in parallel with the original one. They do not interact directly, and most importantly they do not wait for each other. In comparison with the synchronous wait presented in the previous paragraph, this situation is asynchronous, at the two coroutines do not attempt to remain in synch.
IEnumerator A() { ... // Starts B as a coroutine, and continue the execution StartCoroutine( B() ); ... }
It is important to notice that, in this example, B
is a totally independent coroutine. Terminating A
does not affect B
, and vice versa.
Synchronous Coroutines
It is also possible to execute a nested coroutine and to wait for its execution to be completed. The simplest way to do this, is by using yield return
.
IEnumerator A() { ... // Waits for B to terminate yield return StartCoroutine( B() ); ... }
It’s worth noticing that, since the execution of A
is suspended during the execution of B
, this particular case does not need to start another coroutine. One might be tempted to optimise the coroutine by writing something like this:
IEnumerator A() { ... // Executes B as part of A B(); ... }
Executing B
as a traditional function has almost the same effect. The only difference, however, is that B will be executed in a single frame. By using StartCoroutine
, instead, A
is suspended and the next frame can occur.
The reason why this example is shown, however, is to introduce more complex cases of coroutine synchronisation.
Parallel Coroutines
When a coroutine is started using StartCoroutine
, a special object is returned. This can be used to query the state of the coroutine and, optionally, to wait for its termination.
In the example below the coroutine B
is executed asynchronously. Its father A
can continue its execution for as long as it needs. Then, if necessary, it can yield to the reference to B
for a synchronous wait.
IEnumerator A() { ... // Starts B as a coroutine and continues the execution Coroutine b = StartCoroutine( B() ); ... // Waits for B to terminate yield return b; ... }
This is particularly helpful if you want to start several parallel coroutines, all at the same time:
IEnumerator A() { ... // Starts B, C, and D as coroutines and continues the execution Coroutine b = StartCoroutine( B() ); Coroutine c = StartCoroutine( C() ); Coroutine d = StartCoroutine( D() ); ... // Waits for B, C and D to terminate yield return b; yield return c; yield return d; ... }
This new paradigm allows to start an arbitrary numbers of parallel computations, and to resume the execution when all of them have terminated.
Conclusion
This post shows several different patterns that can be implemented in your game to use coroutines effectively. The next posts of this series will focus on how to extends coroutines to support custom waits and events.
- Part 1. Iterators in C#: yields, IEnumerable and IEnumerator
- Part 2. Nested Coroutines
- Part 3. Extending Coroutines
Leave a Reply