Coroutines in Unity – Encapsulating with Promises [Part 1]

Every Unity developer should be familiar with Coroutines. For many they are the way to script a lot of asynchronous and time-delayed tasks. If you’re not up to the speed, they’re pretty much special methods that can suspend and resume their execution for any time desired. In practice, they can be perceived as an illusion of concurrent functions (although they have nothing to do with threads!). There are, however, some problems that many programmers stumble upon while using them. Let’s take a closer look.

The internals

So what exactly are Unity’s coroutines under the hood? How do they work? Granted that we don’t have a direct access to Unity’s source code, but gathering evidence from Manual and expanding on the knowledge from C# we can assume this is more or less how they’re done. Let’s lean over this simple example:

StartCoroutine(TestCoroutine()); //… IEnumerator TestCoroutine() { Debug.Log("Hello there!"); yield return new WaitForSeconds(2); Debug.Log("Hello from future!"); } 1 2 3 4 5 6 7 8 StartCoroutine ( TestCoroutine ( ) ) ; //… IEnumerator TestCoroutine ( ) { Debug . Log ( "Hello there!" ) ; yield return new WaitForSeconds ( 2 ) ; Debug . Log ( "Hello from future!" ) ; }

You don’t have to be genius to figure out that this will log “Hello there!” in the console and “Hello from future!” after 2 seconds. But how? To understand coroutines one must first look at the function’s signature – more precisely, the returned type. The IEnumerator serves as a way of performing iterations on a collection. It controls moving from one object to the next one in a sequence.

To do it, it declares two very important members: a Current property, which has a reference to the element that enumerator (or a cursor you could say) is currently over, and a MoveNext() function, which moves to the next element whilst calculating the new current value. It also has a Reset() function that sets the enumerator to its initial position, but we’ll skip that.

Now since the IEnumerator is just an interface it doesn’t explicitly specify the Current type (other than it’s an object). We can do whatever we want to calculate the next object. MoveNext() will simply do the job and return false if we end up at the end of a sequence.

Iterator blocks

In pure C# we can easily “implement” this interface in special iterator blocks. In practice, C# compiler converts iterator blocks into state machines. Those are the functions that return the IEnumerator type and use the yield return statement to return the values (yes, multiple ones). Calling that makes the MoveNext() function simply return true and set Current to whatever you’re returning. If you want to stop the enumerator you can just call yield break which makes sure that MoveNext() returns false and terminates the sequence (think of it like break; in a loop). An example:

using System; using System.Collections.Generic; class TestIEnumerator { static readonly string inEnumerator = "####"; static IEnumerator GetTexts() { Console.WriteLine(inEnumerator + "First line of GetTexts()" + inEnumerator); Console.WriteLine(inEnumerator + "Just before the first yield" + inEnumerator); yield return "First returned text"; Console.WriteLine(inEnumerator + "Just after the first yield" + inEnumerator); Console.WriteLine(inEnumerator + "Just before the second yield" + inEnumerator); int b = 2; int a = 5 + b; yield return "Second returned text - " + a; Console.WriteLine(inEnumerator + "Just after the second yield" + inEnumerator); } static void Main() { Console.WriteLine("Calling GetTexts()"); IEnumerator iterator = GetTexts(); Console.WriteLine("Calling MoveNext()...

"); bool returnedValue = iterator.MoveNext(); Console.WriteLine("

Returned value = {0}; Current = {1}", returnedValue, iterator.Current); Console.WriteLine("Calling MoveNext() again...

"); returnedValue = iterator.MoveNext(); Console.WriteLine("

Returned value = {0}; Current = {1}", returnedValue, iterator.Current); Console.WriteLine("Calling MoveNext() again...

"); returnedValue = iterator.MoveNext(); Console.WriteLine("

Returned value = {0} - stopping", returnedValue); Console.ReadKey(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 using System ; using System . Collections . Generic ; class TestIEnumerator { static readonly string inEnumerator = "####" ; static IEnumerator GetTexts ( ) { Console . WriteLine ( inEnumerator + "First line of GetTexts()" + inEnumerator ) ; Console . WriteLine ( inEnumerator + "Just before the first yield" + inEnumerator ) ; yield return "First returned text" ; Console . WriteLine ( inEnumerator + "Just after the first yield" + inEnumerator ) ; Console . WriteLine ( inEnumerator + "Just before the second yield" + inEnumerator ) ; int b = 2 ; int a = 5 + b ; yield return "Second returned text - " + a ; Console . WriteLine ( inEnumerator + "Just after the second yield" + inEnumerator ) ; } static void Main ( ) { Console . WriteLine ( "Calling GetTexts()" ) ; IEnumerator iterator = GetTexts ( ) ; Console . WriteLine ( "Calling MoveNext()...

" ) ; bool returnedValue = iterator . MoveNext ( ) ; Console . WriteLine ( "

Returned value = {0}; Current = {1}" , returnedValue , iterator . Current ) ; Console . WriteLine ( "Calling MoveNext() again...

" ) ; returnedValue = iterator . MoveNext ( ) ; Console . WriteLine ( "

Returned value = {0}; Current = {1}" , returnedValue , iterator . Current ) ; Console . WriteLine ( "Calling MoveNext() again...

" ) ; returnedValue = iterator . MoveNext ( ) ; Console . WriteLine ( "

Returned value = {0} - stopping" , returnedValue ) ; Console . ReadKey ( ) ; } }

Compile this on your own and you’ll get the following output:

This example uses the generic interface IEnumerator<T>, but as previously mentioned, you can use the regular one, IEnumerator, where your Current will be of any type. This is exactly what Unity requires in coroutines.

So, with that in mind, we start to get a clearer picture on what Unity actually does with coroutines. StartCoroutine adds the coroutine to some sort of a container. Unity iterates through every executed coroutine in StartCoroutine and performs MoveNext() on those which are ready to continue their work. As shown in the example above, it evaluates expressions between yield return statements and returns a value. If it returns false then it obviously tells Unity to terminate that coroutine (as it simply has just finished). If true, it checks the Current property (remember, non-generic interface!) and sees if there are any familiar types. Remember WaitForSeconds() in the very first example? Unity sees that and pauses the coroutine for seconds specified. In fact, it actually extends from YieldInstruction base type. You’ve also got:

WaitForEndForFrame, which resumes the coroutine at the end of the frame after all cameras and GUI are rendered

WaitForFixedUpdate, which waits until next fixed frame rate update function

Coroutine type itself, which you could use for nest coroutines

CustomYieldInstruction, introduced to write your own custom yield statements – just extend from that class and override keepWaiting property

So where’s the catch?

It goes without saying that with these simple types you can suddenly script some time-based game logic as well as asynchronous events very easily. You can show a UI notification after few seconds in just a few lines of code without any dirty timers. Not only that, but you can even yield return the UnityWebRequest.Send() function and pause your function until you get a HTTP response. “What’s the problem with that then?”, you may ask.

First of all, you can’t easily return a value from coroutines. The signature needs to return IEnumerator to keep a track of when and where to resume your method. Second of all, there is no exception handling at the coroutine level. You can’t use try…catch block with a yield statement in it. You would have to basically try catch every non-yield expression in your coroutine. But what if there are multiple expressions segregated with multiple yields? Or maybe you’d like to stack coroutines one after another. But how to communicate to the coroutines down below the pipe that the top one has encountered a runtime exception?

Fortunately, there is a solution. In the next part we’ll take a look at Promises and their origin from Javascript. Read it here.