Coroutines in Unity — All you need to know about them
You may have already stumbled upon Coroutines in Unity, typically used to execute some code after a given amount of time, detaching from Unity’s main Update() loop.
The goal of this guide is to get to know Coroutines better, because we need to take a bit of extra care when using them, as they can become hard to debug, and even cause performance issues if we don't really know what we are doing.
But also Coroutines lets us do things that otherwise would look really ugly if we keep ourselves stuck in the Update() loop.
When to use Coroutines?
The general rule is:
- When we don't want to execute some piece of code instantly in the current frame.
Also, Coroutines are needed for generic asynchronous operations in Unity such as HTTP transfers, asset loads, file I/O, and such.
Why is that?
Because Coroutines lets us stop their execution at any given point, return the control during the current frame, and continue from the same point in the next frame (also possible after some time/condition).
Example
A Coroutine is just a special method with the ability to yield return
some type of YieldInstruction
.
To declare a Coroutine, you need to create a method that has IEnumerator
as its return value, and at least a yield return statement:
IEnumerator MyCoroutine()
{
Debug.Log("Hi");
yield return null;
Debug.Log("Next frame!");
}
yield return null
indicates Unity to just stop execution there and continue in the next frame.
To start the above coroutine, you need to call:StartCoroutine(IEnumerator routine)
, and pass the coroutine as an argument.
For example, we can start the coroutine in the Start() method:
void Start()
{
StartCoroutine(MyCoroutine());
}
A really common type of YieldInstruction
is WaitForSeconds(float seconds)
, that just indicates Unity to stop the execution and resume after the given scaled time has passed. (scaled time is modified by your game’s current Time.timeScale
)
For example, the following coroutine, when started with StartCoroutine()
, will wait for 2.5 seconds before executing the Respawn() method:
IEnumerator SetRespawn()
{
yield return new WaitForSeconds(2.5f);
Respawn();
}
If you want to wait for real seconds, you can use:WaitForSecondsRealtime(float time)
Stoping and keeping track of Coroutines
It’s important to keep in mind some key facts about Coroutine’s lifecycle:
- They run attached to the MonoBehaviour where they were started.
- Existing Coroutines still run if the attached MonoBehaviour’s
enabled
is set tofalse
. - If the related GameObject’s
SetActive()
is set tofalse
, the Coroutines stop. - Also calling
Destroy()
on either the related GameObject or MonoBehaviour, will stop related Coroutines.
Unity also provides us with 2 useful methods to force a Coroutine to stop:
StopCoroutine()
, with overloads forstring
,IEnumerator
andCoroutine
.StopAllCoroutines()
, which just stops all coroutines in the MonoBehaviour.
Spreading heavy methods across several frames
An interesting use case for Coroutines is spreading the execution of some particular method across several frames, this could be any method that includes iteration across a large collection, and performing resource-intensive tasks such as Instantiate()
.
For example, imagine the case where we suddenly need to spawn a lot of different enemies.
Calling a lot of Instantiate()
in the same frame will surely produce a performance spike and we don't want that.
Without using Coroutines, our method would look like this:
void SpawnEnemies()
{
List<Enemy> enemies = GetEnemiesToSpawn();
foreach(var enemy in enemies)
{
Instantiate(enemy.prefab);
}
}
Now, we can optimize this by making it a Coroutine, and including a small delay between instantiations:
IEnumerator SpawnEnemiesCoroutine()
{
List<Enemy> enemies = GetEnemiesToSpawn();
foreach(var enemy in enemies)
{
Instantiate(enemy.prefab);
yield return new WaitForSeconds(.2f);
}
}
In this case, I used a 0.2-second delay because I don't mind seeing the enemies not spawning all at the same moment, but even a smaller delay of a couple of frames will help to smooth out the initial performance spike.
This same pattern can be applied to any other case where we iterate a collection, and maybe instead of using Instantiate, we are calculating distances, pathfinding, or some complex math.
Different and custom YieldInstruction types
Apart from just skipping a frame with yield return null;
and waiting some time with WaitForSeconds
, there are other YieldInstruction
types we can use such as:
WaitForEndOfFrame
useful if you need to read the final rendered display, or use it like a LateUpdate() for CoroutinesWaitForFixedUpdate
if you need to stick to the Physics Update.WaitUntil
to wait until a given condition istrue
WaitWhile
makes the coroutine stop until the condition isfalse
(just the opposite ofWaitUntil
)WaitForSecondsRealtime
already mentioned this one, uses unscaled time.
Also, you can define your own YieldInstruction
inheriting from CustomYieldInstruction
.
You can read more about each one by clicking the attached links.