About Coroutines in Unity Engine

coroutines

The Unity Engine offers us, developers, a lot of rich resources and content to make our job easier. Amongst them, there are the Coroutines in programming. I oftenly use Coroutines to do a lot of things on the projects I work, because I see it as a very strong resource, it allows me to have a lot more control over the system flow. However, it gave me tons of problems when I misused it. I’d like to share my experience with coroutines, because I learned a lot with it on my latest project and maybe it can help you not to make the same mistakes I made.

Understanding Coroutines


First, let’s understand what is a Coroutine and how it works. So basically, Coroutine is the name we give to a process we can control its execution time e.g. add delays, pauses until something happens, or anything like this. Be aware, it is not a Thread! Coroutines run on Unity’s main thread, so they are not an alternative to multi-threading. If you have no knowledge about Threads, I suggest you search about it later if you are really into programming, but you don’t need to understand it to work with Coroutines.

For the Coroutines to work accordingly, you need to create a function with an IEnumerator return type and make use of the yield return command. The IEnumerator type is an Interface (if you don’t know this, I strongly suggest you learn it) that allows iteration over a non-generic collection. The important here is the iteration process, which sustains the core of the Coroutine functionality.

Then, to run the Coroutine, you need to use the StartCoroutine function and then provide either the IEnumerator’s name in a string (which I do not recommend because it can cause overheads), or the IEnumerator object. This function will return a Coroutine object that you can use to stop its execution using the StopCoroutine function (you can also use the IEnumerator’s name as a string to stop it, but I also do not recommend for the same reasons as above).

Now I’ll provide some examples. To make it easy to understand Coroutines, think of the Update function. Think about controlling when it will execute, or in what intervals, and then pausing its execution. You probably want some things to happen only for some time, and that requires you to create a boolean variable, and then activate/deactivate its value to run a piece of code on the Update function, for example:

MeshRenderer mr;
float feedbackSeconds;
bool enteredTrigger;

void Start()
{
    mr = GetComponent<MeshRenderer>();
}

/** You can also declare this function without parameters if you aren't going
 *  to use them
 */ 
void OnTriggerEnter(Collider other)
{
    enteredTrigger = true;
    feedbackSeconds = 2f;
}

void Update()
{
    if (enteredTrigger && feedbackSeconds >= 0)
    {
        mr.material.color = new Color(Random.Range(0, 1f), Random.Range(0, 1f), Random.Range(0, 1f));
        feedbackSeconds -= Time.deltaTime;
    }
}

If you want to test the script, make sure you add a Rigidbody to the objects! This script randomizes the color of an object after something enters it’s trigger Collider for 2 seconds, but it works on the Update function. Imagine we have a lot of events like these, we would have tons of conditions on the Update function, resulting on a very big piece of code and lots of conditional checks. Now, let’s implement the same thing with a Coroutine. First of all we would remove the boolean and the float variables, and completely move the content of the Update function to our Coroutine with some adjustments.

MeshRenderer mr;

void Start()
{
    mr = GetComponent<MeshRenderer>();
}

// Without parameters, remember?
void OnTriggerEnter()
{
    // Here we start the Coroutine. Make sure you include the parenthesis
    StartCoroutine(FeedbackRoutine());
}

IEnumerator FeedbackRoutine()
{
    float time = 0;
    // We create a loop to control for how many time it will run
    while (time >= 2)
    {
        time += Time.deltaTime;
        mr.material.color = new Color(Random.Range(0, 1f), Random.Range(0, 1f), Random.Range(0, 1f));
        /** This command will pause the execution of this piece of code and resume
         *  on the next frame. In this case, null means the next update frame,
         *  you could use WaitForEndOfFrame() which is almost the same thing as
         *  returning null or even WaitForFixedUpdate() to sync with physics update
         */ 
        yield return null;
    }
    /** Once the code reaches here, the coroutine is completed and therefore
     *  released from memory
     */ 
}

That’s it, we successfully created a Coroutine for a timed event. It will run for 2 seconds, and then stop, instead of continuously checking for a boolean on the Update function for ever.

Less Basic Topics


Imagine a timed event function, where you would want things to follow a certain delay and control some stuff. You could do it with a Coroutine as the following:

IEnumerator TimedRoutine()
{
    // Action 1 here
    yield return new WaitForSeconds(2f);
    // Action 2 here
    yield return new WaitForSeconds(3f);
    // Action 3 and so on
}

You can always use it to control something that happens constantly but with a different time update than the Update function:

IEnumerator ConstantRoutine()
{
    // Happens for ever. You can also use "for(;;)"
    while (true)
    {
        // Routine action here
        yield return new WaitForSeconds(.1f);
    }
}

There is only one issue on this. Coroutines like this, will create a new WaitForSeconds instance each loop, and that isn’t very good to the memory and performance of the system. So we can create this instance before the loop and then use its reference:

IEnumerator ConstantRoutine()
{
    WaitForSeconds time = new WaitForSeconds(.1f);
    while (true)
    {
        // Routine action here
        yield return time;
    }
}

That’s a more cleaner, performance-wise solution. There are two more return types I’d like to talk about, and they are WaitUntil and WaitWhile. I saw very little implementation of those two on the codes I saw, but they proved to be very useful on my last project. Let’s see an example:

public static class SingleColor
{
    public static Color color = Color.red;
    public static bool activated = false;
}

public class ExampleClass : MonoBehaviour
{
    MeshRenderer mr;

    void Start()
    {
        mr = GetComponent<MeshRenderer>();
        StartCoroutine(ConditionalRoutine());
    }

    // Test with a rigidbody
    void OnTriggerEnter()
    {
        SingleColor.activated = true;
    }

    IEnumerator ConditionalRoutine()
    {
        yield return new WaitUntil(() => SingleColor.activated);
        mr.material.color = SingleColor.color;
    }
}

If you don’t have much experience in general programming you could be wondering about this part `() => SingleColor.activated` and well, this is a lambda expression, which is basically an anonymous function that works like a delegate, in this case to check a condition. When SingleColor.activated returns true, the Coroutine will continue, therefore changing the material of the object. The same logic would apply to WaitWhile, but it would run while the condition returns true.


Coroutine Execution Time

If you are using coroutines, understanding when they are called can help you work on your logic. I don’t even need to say much about this, because Unity gives us this wonderful very well detailed image answering all our questions:

overview

You get it, right, WaitForFixedUpdate occurs on the end of the physics catchup phase, then returning null or WaitForSeconds just after Update, and finally WaitForEndOfFrame after rendering. Have it in mind when you are writing your coroutine, you probably won’t move something after the Rendering phase, otherwise it would only update the position on the next frame, which means having 1 frame of delay, and so on.


Nested Coroutines (and their overhead)

Well after all of this, we still have another subject I’d like to cover. This one, nearly killed my last project because I misused it. So, whenever you write an yield statement, you can actually return another Coroutine, and then wait until it ends to continue the first execution. This is the part where you need to be careful, after using this I strongly recommend you not to, although it gives nice results. Keep in mind that there is other ways to reach the same result. Let’s suppose you want to do an action, then wait for some other action, and do a third action.

public class NestedExample : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(MainRoutine());
    }

    IEnumerator MainRoutine()
    {
        // Action 1
        yield return StartCoroutine(NestedRoutine());
        // Action 3
    }

    IEnumerator NestedRoutine()
    {
        // Action 2
        yield return new WaitForSeconds(2);
    }
}

This is possible, however as you know, Coroutines run on Unity’s main thread, and nested coroutines depend on each other so as you go nesting, they won’t leave the memory until completed therefore generating a huge overhead. On my last project, I had a turn-based game completely based on the Nested Coroutines logic. As you may expect, once we deployed it to mobile it had tons of lags because of the coroutine’s overhead. My advice here: try a different approach.


Wrap-up

I guess this is all for this post, we already covered a lot of the basics and some more complex concepts of coroutines. I believe we can say that you have reinforced your understanding of coroutines and how they work, or how and when to use them, also you have been given some tips around its usage. You can find tons of other articles about coroutines on the web, there is people expanding it to a more advanced approach and using it in different ways, it’s really cool to keep researching.

So, what do you think about posts like these? Would you like to see more of this kind? Did I forget to mention something you think is important about Coroutines? Let me know in the comments! Keep researching and testing, see you there.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s