My FeedDiscussionsHeadless CMS
New
Sign in
Log inSign up
Learn more about Hashnode Headless CMSHashnode Headless CMS
Collaborate seamlessly with Hashnode Headless CMS for Enterprise.
Upgrade ✨Learn more

A better Coroutine for Unity - Part 1: Returning a value from a Coroutine

Matt Borum's photo
Matt Borum
·Sep 28, 2020·

9 min read

Unify_100x100.png How do we start trying to solve this problem?

If we look at a bare-bones Unity Coroutine that doesn't actually do anything, we see something like this:

public IEnumerator StandardCoroutine()
{
    yield return null;
}

We know every Coroutine must include at least one yield return statement, in fact, your code won't compile without it. So every Coroutine already returns a value! This is a good start.

So, if we change the above code to:

public IEnumerator StandardCoroutine()
{
    yield return "Hello, World!";
}

We are now returning our important return value from a Coroutine... the next question is; how do we get our hands on that value?

This is a bit trickier because Unity handles this internally, so when we write yield return new WaitForEndOfFrame(); that is handled under the Unity hood. We can't get at that code or hook into it in any way, and really, we don't want to be messing with it anyway.

So, what we need is a middle man. Something that sits between Unity and the Coroutine whose value we want. We need to be able to receive the return value from all those yield return statements, check if it is the return value we are interested in, and pass everything else onto Unity to deal with.

Ideally, our middle man also needs to be a Unify Coroutine so we can start it in the standard Unity way and not break existing code, and also to take advantage of other Unify features like pausing and resuming a Coroutine.

So effectively, we are going to have an inner Coroutine and an outer Coroutine. The inner Coroutine is the one we want the return value from and the outer Coroutine is our middle man.

So how do we do this?

When we yield return out of a Coroutine we are actually returning an object that implements the IEnumerator interface, we can see this in the example above and in the signature of all Coroutines. We know (or should know!) that we can use MoveNext() to move over an IEnumerator.

Let's see this with a List<T> which also implements the IEnumerator interface;

public void EnumeratorExample()
    {
        List<string> hello = new List<string> { "Hello", "Unify", "World" };
        IEnumerator<string> e = hello.GetEnumerator();

        while (e.MoveNext())
        {
            Debug.Log(e.Current);
        }
    }

If you try this in Unity you will see the following in the output console;

Hello Unify World.png

We can move over a Coroutine in the same way! This is the final piece of the jigsaw we need to reach our goal of returning a value.

Let's try it by writing a simple Coroutine that prints the same message as the EneumeratorExample above and waits 2 seconds between each word.

public IEnumerator CoroutineTest()
    {
        yield return "Hello";
        yield return new WaitForSecondsRealtime(2f);
        yield return "Unify";
        yield return new WaitForSecondsRealtime(2f);
        yield return "World!";
    }

This is no different from any Unity Coroutine. The difference is in the way we start the Coroutine running. Instead of a call to StartCoroutine(CoroutineTest()); we need to implement our outer Coroutine that we discussed above. Just for testing purposes, let's hard code a return value of the type string;

public IEnumerator StartUnifyCoroutine(IEnumerator target)
    {
        while (target.MoveNext())
        {
            if (target.Current?.GetType() == typeof(string))
            {
                Debug.Log("UnifyCoroutine Returns: " + target.Current);
            }

            yield return target.Current;
        }
    }

This is our middle man. We pass in a reference to the actual Coroutine we want the return value from, test if it is of type string and print the value to the Console, then we yield return target.Current to pass the value back to Unity so it can deal with things like WaitForSeconds or WaitForEndOfFrame etc.

We start our outer Coroutine like so;

StartCoroutine(StartUnifyCoroutine(CoroutineTest()));

To test this, create a new script in Unity, delete the default code and replace with the following;


using System.Collections;
using UnityEngine;

public class UnifyCoroutineTest : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyUp(KeyCode.S))
        {
            Debug.Log("* Starting UnifyCoroutine *");

            StartCoroutine(StartUnifyCoroutine(CoroutineTest()));
        }
    }

    public IEnumerator StartUnifyCoroutine(IEnumerator target)
    {
        while (target.MoveNext())
        {
            if (target.Current?.GetType() == typeof(string))
            {
                Debug.Log("UnifyCoroutine Returns: " + target.Current);
            }

            yield return target.Current;
        }
    }

    public IEnumerator CoroutineTest()
    {
        yield return "Hello";
        yield return new WaitForSecondsRealtime(2f);
        yield return "Unify";
        yield return new WaitForSecondsRealtime(2f);
        yield return "World!";
    }
}

Add this script to a GameObject and press the S key to run. If all goes well you should see the words Hello Unify World! appear in the Console at 2-second intervals.

Great! But we still haven't really got our hands on the return value yet and at the moment we can only return a string from our Coroutine.

So, now we know the principle of returning a value from a Coroutine, we can now really knock our code into shape and at the same time address those issues above.

While we are at it let's put our UnifyCoroutine code into its own class. It would also be really nice if we could use MonoBehaviour functionality without having to inherit from MonoBehaviour all the time. So let's go the whole hog and change that too.

I'm going to use an Action to pass on our return value so our code can use it and we can use Generics to let us return any type we like, not just a string.

Here is our UnifyCoroutine.cs class file;

using System;
using System.Collections;
using UnityEngine;

public class UnifyCoroutine<T>
{
    public Action<T> callback;

    //

    private IEnumerator m_target;
    private static readonly GameObject m_gameObject;
    private static readonly MonoBehaviour m_mono;

    //

    static UnifyCoroutine()
    {
        m_gameObject = new GameObject { isStatic = true, name = "[Unify]" };
        m_mono = m_gameObject.AddComponent<UnifyMonoBehaviour>();
    }

    public UnifyCoroutine(IEnumerator target, Action<T> callback)
    {
        m_target = target;
        this.callback = callback;
    }

    //

    public void Start()
    {
        m_mono.StartCoroutine(StartUnifyCoroutine());
    }

    //

    private IEnumerator StartUnifyCoroutine()
    {
        while (m_target.MoveNext())
        {
            yield return m_target.Current;
        }

        callback.Invoke((T)m_target.Current);
    }

}

To gain access to MonoBehaviour functionality we need to create a GameObject and add a MonoBehaviour component to it. We can't just AddComponent<MonoBehaviour> directly though, instead we need to add a component (class) that derives from MonoBehaviour. This is as simple as an empty class. We make our GameObject and MonoBehaviour instances static so we are not creating new objects for every UnifyCoroutine we create, all instances can use the same one.

Add a new C# file to the project, call it UnifyMonoBehaviour.cs and add the following code;

using UnityEngine;

public class UnifyMonoBehaviour : MonoBehaviour
{
    // Empty class
}

The constructor of UnifyCoroutine takes the target Coroutine and an Action<T> delegate for our return value callback. We iterate through the Coroutine and finally invoke our callback with the final return value casted to our Generic return type of T.

So how to use the Action<T> callback?

There are 2 ways; The first is to create a method to handle the callback and pass this reference via the constructor;

private void ReturnValueCallback(string returnValue)
    {
        Debug.Log("Return Value is * " + returnValue + " *");
    }

Create a new UnifyCoroutine instance like so;

UnifyCoroutine<string> _co = new UnifyCoroutine<string>(CoroutineTest(), ReturnValueCallback);

The second way is via an anonymous delegate in the argument list of the constructor;

UnifyCoroutine<string> _co = new UnifyCoroutine<string>(CoroutineTest(), (returnValue) =>
            {
                Debug.Log("Return Value is * " + returnValue + " *");
            });

All we need to do now is start the Coroutine with;

_co.Start();

We now have a Coroutine that returns a value!

Thank you for reading. Comments welcome.