When you dive into the world of game development with Unity, you quickly encounter the concept of coroutines. They are a powerful tool for managing processes that span multiple frames, providing an elegant way to handle asynchronous tasks. In this post, we’ll explore the inner workings of Unity coroutines, shedding light on what happens behind the scenes when you use them.
The Need for Coroutines
In game development, many tasks are not suitable for one-frame execution. Consider pathfinding algorithms or complex calculations; performing them all at once could lead to laggy gameplay. To mitigate this, you need to split these processes into smaller chunks that can be executed over several frames. Traditional programming methods often require you to explicitly manage state across frames, which can be cumbersome.
Enter coroutines – a feature provided by Unity (and many other environments) to address this challenge. Coroutines allow you to write a function as a single, uninterrupted block of code and designate specific points where it should “pause” and resume execution later.
Coroutine Syntax
Coroutines in Unity are written in C#. Here’s a basic example in both languages:
IEnumerator LongComputation() {
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
How Coroutines Work
While we don’t have access to Unity’s source code, it’s safe to assume that Unity’s coroutine engine operates based on common programming principles. Key insights come from examining the C# version of coroutines.
In C#, a coroutine function returns IEnumerator
, and it uses the yield
keyword. This keyword is essential to understanding how coroutines function. Essentially, it tells the coroutine engine when to pause and resume execution.
The first heading below is a straight answer to the question. The two headings after are more useful for the everyday programmer.
Possibly Boring Implementation Details of Coroutines
Coroutines are explained in Wikipedia and elsewhere. Here I’ll just provide some details from a practical point of view. IEnumerator
, yield
, etc. are C# language features that are used for somewhat of a different purpose in Unity.
To put it very simply, an IEnumerator
claims to have a collection of values that you can request one by one, kind of like a List
. In C#, a function with a signature to return an IEnumerator
does not have to actually create and return one, but can let C# provide an implicit IEnumerator
. The function then can provide the contents of that returned IEnumerator
in the future in a lazy fashion, through yield return
statements. Every time the caller asks for another value from that implicit IEnumerator
, the function executes till the next yield return
statement, which provides the next value. As a byproduct of this, the function pauses until the next value is requested.
In Unity, we don’t use these to provide future values, we exploit the fact that the function pauses. Because of this exploitation, a lot of things about coroutines in Unity do not make sense (What does IEnumerator
have to do with anything? What is yield
? Why new WaitForSeconds(3)
? etc.). What happens “under the hood” is, the values you provide through the IEnumerator are used by StartCoroutine()
to decide when to ask for the next value, which determines when your coroutine will unpause again.
Your Unity Game is Single Threaded (*)
Coroutines are not threads. There is one main loop of Unity and all those functions that you write are being called by the same main thread in order. You can verify this by placing a while(true);
in any of your functions or coroutines. It will freeze the whole thing, even the Unity editor. This is evidence that everything runs in one main thread. This link that Kay mentioned in his above comment is also a great resource.
(*) Unity calls your functions from one thread. So, unless you create a thread yourself, the code that you wrote is single threaded. Of course Unity does employ other threads and you can create threads yourself if you like.
A Practical Description of Coroutines for Game Programmers
Basically, when you call StartCoroutine(MyCoroutine())
, it’s exactly like a regular function call to MyCoroutine()
, until the first yield return X
, where X
is something like null
, new WaitForSeconds(3)
, StartCoroutine(AnotherCoroutine())
, break
, etc. This is when it starts differing from a function. Unity “pauses” that function right at that yield return X
line, goes on with other business and some frames pass, and when it’s time again, Unity resumes that function right after that line. It remembers the values for all the local variables in the function. This way, you can have a for
loop that loops every two seconds, for example.
When Unity will resume your coroutine depends on what X
was in your yield return X
. For example, if you used yield return new WaitForSeconds(3);
, it resumes after 3 seconds have passed. If you used yield return StartCoroutine(AnotherCoroutine())
, it resumes after AnotherCoroutine()
is completely done, which enables you to nest behaviors in time. If you just used a yield return null;
, it resumes right at the next frame.
Using the Timing
Coroutines in Unity often involve waiting for something to happen, such as the end of an animation or a specific amount of time to pass. To handle this, Unity provides the YieldInstruction
base type and several derived types like WaitForSeconds
and WaitForEndOfFrame
. These derived types indicate different kinds of waiting periods.
The coroutine engine likely maintains lists of coroutines based on their waiting conditions. For example, coroutines waiting for time-based events (e.g., WaitForSeconds
) are sorted by the time they should resume. Others waiting for the end of the frame (WaitForEndOfFrame
) are placed in a different list. The engine then runs the coroutines accordingly.
Your Unity Game is Single Threaded (*)
Coroutines are not threads. There is one main loop of Unity and all those functions that you write are being called by the same main thread in order. You can verify this by placing a while(true);
in any of your functions or coroutines. It will freeze the whole thing, even the Unity editor. This is evidence that everything runs in one main thread. This link that Kay mentioned in his above comment is also a great resource.
(*) Unity calls your functions from one thread. So, unless you create a thread yourself, the code that you wrote is single threaded. Of course, Unity does employ other threads, and you can create threads yourself if you like.
Useful Insights
Understanding Unity coroutines can lead to some useful coding insights:
- Yield Anything: You can yield any expression, not just specialized Unity keywords. This flexibility can be handy for complex coroutine logic.
- Manual Iteration: You can manually iterate over coroutines if needed. This allows you to implement custom interruption conditions.
- Roll Your Own YieldInstructions: While Unity provides several
YieldInstruction
types, you can implement your own custom ones, though this may not be as performant as engine-provided options.
Conclusion
Unity coroutines are a powerful tool for managing asynchronous processes in game development. They allow you to write clean, structured code that executes over multiple frames without the need for complex state management. Understanding how coroutines work under the hood can empower you to use them effectively in your game development projects. So go ahead, harness the power of Unity coroutines to create smoother and more responsive gameplay experiences!