Things worth knowing about coroutines

What are coroutines?

Coroutines are functions which are executed over multiple frames. They are very convenient and can be used for scripted sequences, for example to move an object from point A to B smoothly over multiple frames. Another coroutine could use the first coroutine to move an object from A to B, then pause 5 seconds and finally move from B to C. This can be achieved by letting a couroutine wait for the completion of another coroutine. Here is an example:

class MovingObject : MonoBehaviour
{
    void Start()
    {
        // Start the first coroutine
        StartCoroutine(Move());
    }

    // Move to 0,0,10 then wait 5 seconds and then move to 10,0,10
    IEnumerator Move()
    {
        // Stop execution until the MoveTo-coroutine has completed
        yield return StartCoroutine(MoveTo(new Vector3(0,0,10)));
        // Stop execution until 5 seconds have passed
        yield return new WaitForSeconds(5);
        // Stop execution until the MoveTo-coroutine has completed
        yield return StartCoroutine(MoveTo(new Vector3(10,0,10)));
    }

    // Move to the specified target over multiple frames (smoothed out)
    IEnumerator MoveTo(Vector3 target)
    {
        Transform t = transform;
        while(Vector3.Distance(t.position, target) > 0.2f)
        {
            t.position = Vector3.Lerp(t.position, target, Time.deltaTime);
            // Stop execution until the next frame
            yield return null;
        }
    }
}

The big advantage of coroutines is that the state is maintained while the coroutine is running (in this case the argument "target" and the local variable "t"). Thus, even the most complex sequences can be scripted in a relatively simple way.

How do coroutines work from a technical perspective?

Coroutines run in parallel, but no threads are used. Basically, a coroutine is a C# feature (other languages probably also support this feature).

For a foreach loop in C# to work at all, an array, a list, or any other class to be enumerated, must implement the interface IEnumerable. This interface declares a single method:

interface IEnumerable
{
    IEnumerator GetEnumerator();
}

The method returns an object that implements the IEnumerator interface. The IEnumerator interface looks something like:

interface IEnumerator
{
    // Gets the current element in the collection
    object Current { get; }

    // Advances the enumerator to the next element of the collection
    bool MoveNext();

    // Sets the enumerator to its initial position, which is before the first element in the collection
    void Reset();
}

A regular foreach loop is then converted to the following code:

IEnumerator enumerator = myList.GetEnumerator();
while (enumerator.MoveNext())
{
    ListElementType myElement = (ListElementType)enumerator.Current;
    // .. code that does something with myElement
    ..
}

To write your own enumerator, you would therefore need to create a class that implements the interface IEnumerator. At this point the above mentioned C# feature comes into play. Instead of having to write a complete class, an enumerator function can be created. This is a simplified syntax for creating custom enumerators. It's the exact same syntax that the coroutines in Unity use:

IEnumerator EverySecondChar(string name)
{
    for (int i = 0; i < name.Length; i += 2)
    {
        yield return name[i];
    }
}

The above enumerator that returns every second character of a string is converted to something like this:

class EverySecondCharEnumerator : IEnumerator
{
    private string _name;
    private int _index;

    public EverySecondCharEnumerator(string name)
    {
        _name = name;
        _index = -1;
    }

    public object Current
    {
        get { return _name[_index]; }
    }

    public bool MoveNext()
    {
        if (_index + 2 >= _name.Length)
            return false;
        _index += 2;
        return true;
    }

    public void Reset()
    {
        _index = -1;
    }
}

And can be used like this:

EverySecondCharEnumerator e = new EverySecondCharEnumerator(name);
while (e.MoveNext())
{
    char c = (char)e.Current;
    // ...
}

The same thing with the enumerator function:

IEnumerator e = EverySecondChar(name);
while (e.MoveNext())
{
    char c = (char)e.Current;
    // ...
}

Now imagine what the "yield return" does:

  • On the first call to MoveNext() the function executes until the first yield return or a yield break statement.
  • The yield return provides an object that will then be available in the "Current" property of the enumerator.
  • On the next call to MoveNext() the execution of the function is resumed right where it stopped (after the first yield return statement).
  • From there, the enumerator function is executed until the next yield return and this process is repeated until the function is either completed, or a yield break is encountered. The yield break and the end of the function make sure that MoveNext() will return false and the loop is broken.

Unity simply uses the main loop of the game instead of the while loop from the example above. The function MoveNext() is called once per frame for each coroutine. If MoveNext() returns false, then the coroutine has completed. If the yield return statement returns a null, then Unity will call MoveNext() in the next frame. If it returns a WaitForSeconds object, then Unity reads the number of seconds from this object and ensures that the next call to MoveNext() occurs only after this amount of time has passed. If it returns another coroutine, then calling MoveNext() is delayed until the other coroutine has also completed in the exact same way.

Is there anything else worth knowing about coroutines?

The coroutines are executed by the class MonoBehaviour, which is the base class of most unity scripts. Thus, when a game object is destroyed or if the script component is removed from the game object, any coroutines running on this script are interrupted immediately because there is no longer a script that will call the MoveNext() function.

For this reason you should always be careful when working with nested coroutines. Here is a small example:

class MyCube : MonoBehaviour
{
    public IEnumerator MoveAsync(Vector3 target)
    {
        // movement code with yield returns
    }
}

class ScriptedMovement : MonoBehaviour
{
    public MyCube cube;

    IEnumerator Movement1(Vector3 target)
    {
        // MoveAsync will be executed on this object
        yield return StartCoroutine(cube.MoveAsync(target));
    }

    IEnumerator Movement2(Vector3 target)
    {
        // MoveAsync will be executed on the cube object
        yield return cube.StartCoroutine(cube.MoveAsync(target));
    }
}

When you add a StartCoroutine(Movement1()) call, then the cube will begin to move. When the ScriptedMovement object is destroyed while the cube is still moving, the cube movement will be interrupted because the coroutine was running inside the ScriptedMovement object and not inside the MyCube object.

If you use the Movement2 function instead, the cube will continue its movement until it arrives at the destination, because this time the coroutine will run on the cube itself.

So, be careful. Otherwise you might encounter some unexpected coroutine interruptions in more complex projects...

To make sure that coroutines will always run in the class in which they are defined, you can use this little trick:

class MyCube : MonoBehaviour
{
    // Only this function can be called from the outside,
    // which returns a Coroutine-Object
    public Coroutine Move(Vector3 target)
    {
        // MoveAsync will run on this object
        return StartCoroutine(MoveAsync(target));
    }

    // The coroutine can't be called from the
    // outside because it's private
    private IEnumerator MoveAsync(Vector3 target)
    {
        // movement code with yield returns
    }
}

class ScriptedMovement : MonoBehaviour
{
    public MyCube cube;

    IEnumerator Movement(Vector3 target)
    {
        // The coroutine MoveAsync will run on the cube object because
        // Move(..) contains the StartCoroutine(MoveAsync(..)) call
        yield return cube.Move(target);
    }
}

There is a public method which has no other purpose than starting the private coroutine and returning a Coroutine object. This object is returned by the StartCoroutine() method.

A yield return StartCoroutine(..); could also be written as:

Coroutine c = StartCoroutine(...);
yield return c;

And hence:

Coroutine c = cube.Move(target);
yield return c;

In both cases, a Coroutine object is returned in the end.

2 Comments

  1. Sehr guter Beitrag über Coroutinen!

  2. Laurence Trippen

    5. June 2019 at 10:03

    Sehr hilfreich! Danke!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2019 michael-zehr.ch

Theme by Anders NorénUp ↑