Asynchronous programming in Unity: Part 1 Coroutines

Over the years I have spent a lot of time working with Javascript in web apps, games and node.js and because of this, these days asynchronous programming just comes naturally. But for quite a few this way of programming and thinking can be a bit confusing! So what I'm going to try and aim to do is write a series of articles on the different techniques to take advantage of asynchronous programming in Unity and C# and hopefully demystify it all a little.

Coroutines

Lets start with coroutines, you have probably heard about them at some point and heard of people either loving them or having very strong opinions against them. I'm not going to weigh in on the debate too much but I will show you what they are, how they work and some examples of using them and alternatively how to do the same another way.

Coroutines allow us to schedule code to be run sometime in the future, this is done by taking advantage of the yield statement which when come across in execution it returns control to the calling instruction until a condition is met, then execution continues after the yield statement. For example:

private IEnumerator MyCoroutine()
{
    //Do something as soon as coroutine is started

    yield return null; //Return control to the calling instruction
    
    //Do something once control is returned to us
}

Knowing this we could use this function to maybe trigger off an event in 30 seconds when the game time runs up for example, ending the game and showing the game over screen. This is a very simple example, so lets cover some more advance uses of a coroutine. Before we go any further though there is just one caveat and that is to run a coroutine we need a MonoBehaviour on a GameObject in the current scene, so coroutines can't be used in a class not derived from MonoBehaviour. Not too much of an issue and there are plenty of ways of working around this but we won't worry about that today.

One of the nice uses of coroutines is allowing us to do work over multiple frames, maybe running some long running functions on an array that would be too much for a single frame, so to avoid spikes we do the work over multiple frames or alternatively we can have game logic to apply damage or similar effects to a player over time. Take a look at the class below where I have used a coroutine over multiple frames to work through an array:

using UnityEngine;
using System.Collections;

public class MyCoroutine : MonoBehaviour
{
    /*A list of tasks to run. Task is a simple class with a RunWork 
    function that will do some long running operation*/
    private Task[] m_tasks = null;

    public void StartWork(Task[] tasks)
    {
        m_tasks = tasks;

        /*We need to call StartCoroutine passing in the return value of 
        our DoWork function to have the UnityEngine start dealing with 
        our yields. As soon as we call DoWork() though any code before 
        the yield will run so in this case we will get the log "Doing 
        Work" printed out to the console and the first task run.*/
        StartCoroutine(DoWork());
    }

    private IEnumerator DoWork()
    {
        Debug.Log("Doing Work");

        foreach(Task task in m_tasks)
        {
            task.RunWork();
            /*Each time we call yield return SOMETHING (explained later 
            on) we return control to the executing method and at some 
            point (in this case in one frame) we will have control 
            returned to us and we will run the next task and continue 
            this pattern until all tasks are run then "Work Done" will 
            be printed out*/
            yield return null;
        }
        
        Debug.Log("Work Done");
    }
}

We have now seen a potential use for coroutines (if you want more examples or have questions just let me know in the comments below) and now that we have let me explain the yield statement a little more.

When we yield we are returning control to the Unity engine which will in turn return control to us after some time period. The Unity Docs explain the execution order of each of the return types but I will go into more detail:

  • yield return null; - when we return null (or any other value that doesn't inherit from YieldInstruction) it delays the code after the yield statment until the next time all the Update() functions of the engine and your scripts have run. Allowing you to delay the code for one game update.

  • yield return new WaitForSeconds(2); - WaitForSeconds is a class that inherits from YieldInstruction. It will allow us to yield for a period of time, in this case 2 seconds. There is a tiny caveat though with using this method for timing related uses, this method won't be called at exactly the time in the future you'd expect. It could be delayed by up to a whole Update() after the time has elapsed and depending on the length of one of your frames it could be delayed by that long.

  • yield return new WaitForFixedUpdate(); - WaitForFixedUpdate is another class that will allow you to delay execution until the end of all the FixedUpdate() methods on the next physics update loop have been called on all scripts. This allows you to schedule something to run after the next physics update has happened. Be aware this can be called within the same game frame depending on the frequency of your physics update.

  • yield return new WaitForEndOfFrame(); - WaitForEndOfFrame allows you to delay execution until all Update(), previous yields, LateUpdate() and rendering methods have been completed. Depending on when this is scheduled it could run on the same frame or the next frame (if scheduled from another WaitForEndOfFrame yield).

  • yield return StartCoroutine(DoWork()); - Will allow you to schedule code to run after a previous coroutine has been completed. This will return control after the Update() on the next frame after the previous coroutine has been completed.

We have now seen some uses of a Coroutine and how the different return types effect the scheduling of the code following the yield function. In the next article we will be talking about callbacks and how they work and relate to asynchronous programming and how we may use them in conjuction with coroutines.

IndyBonez

Read more posts by this author.