Coroutines – More than you want to know

posted by horsman on August 26, 2012

You’ve probably used coroutines in Unity if you happened by this post. If not, go ahead and try them out; I’ll wait here. In this post we’ll talk about how they work, and then we’ll extend them to be even more useful.

The Yield keyword and IEnumerator

In C#, the yield keyword and IEnumerator can be used together to execute and create functions that are evaluated lazily. This is done by storing the state of the program’s stack frame between calls to the function (It’s actually a bit more complicated then that. See reddit reply by tracekill here). Read more about the yield keyword and IEnumerator here.

In this example we generate the Fibonacci sequence in a lazy way:


// This function represents a generator of the fibonacci sequence
IEnumerator Fibonacci(){
	int Fkm2 = 1;
	int Fkm1 = 1;
	yield return 1; // The first two values are 1
	yield return 1;
	// Now, each time we continue execution, generate the next entry.
	while(Fkm1 + Fkm2 < int.MaxValue){
		int Fk = Fkm2 + Fkm1;
		Fkm2 = Fkm1;
		Fkm1 = Fk;
		yield return Fk;
	}
}

void Start () {
	// Call the Fibonacci function, which 
	// immediately returns an IEnumerator
	// (No code in Fibonnacci is run)
	IEnumerator fib = Fibonacci();

	// Generate the first 10 Fibonacci numbers
	for(int i = 0; i < 10; i++){
		// MoveNext runs the Fibonacci function
		// with the stored stack frame in the Ienumerator
		if(!fib.MoveNext())
			break;
		// Current returns the value that was yielded
		Debug.Log((int)fib.Current);
	}
}

If you like you can use print statements to confirm that this is evaluated lazily.

So now that you can see how IEnumerator and yield are used together, you can start to see how they could be used to model execution over multiple frames.

Coroutines in Unity

So how does Unity make the coroutine 'magic' happen?
Disclaimer: this is just a good guess that would work, perhaps not the exact way the UnityEngine does it

When you make a call to StartCoroutine(IEnumerator) you are handing the resulting IEnumerator to the underlying unity engine.

StartCoroutine() builds a Coroutine object, runs the first step of the IEnumerator and gets the first yielded value. That will be one of a few things, either "break", some YieldInstruction like "Coroutine", "WaitForSeconds", "WaitForEndOfFrame", "WWW", or something else unity doesn't know about. The Coroutine is stored somewhere for the engine to look at later.

As you probably know, game logic is executed in "frame" steps, for example, Update will be called on MonoBehaviours with an Update function once per frame. At various points in the frame, Unity goes through the stored Coroutines and checks the Current value in their IEnumerators.

  • WWW - after Updates happen for all game objects; check the isDone flag. If true, call the IEnumerator's MoveNext() function;
  • WaitForSeconds - after Updates happen for all game objects; check if the time has elapsed, if it has, call MoveNext();
  • null or some unknown value - after Updates happen for all game objects; Call MoveNext()
  • WaitForEndOfFrame - after Render happens for all cameras; Call MoveNext

MoveNext returns false if the last thing yielded was "break" of the end of the function that returned the IEnumerator was reach. If this is the case, unity removes the IEnumerator from the coroutines list.

One common misconception cleared up: Coroutines do not execute in parallel to your code. They run in the same thread as everything else in your scripts, so editing the same values in Coroutines and Update is safe.

The Coroutine YieldInstruction

There is one more interesting thing here: StartCoroutine returns a YieldInstruction subclass called "Coroutine". Your coroutine can yield one of these in order to wait for another coroutine to finish before resuming execution.

The way the engine handles these ones is a bit special. The IEnumerator that yielded to the Coroutine is stored in the Coroutine object and when the Coroutine object's original IEnumerator has finished as discussed above, this IEnumerator is run and then added to the list.

Extending Coroutines: Return Values and Error Handling

There is no practical way to get a return value out of a coroutine at the moment or deal with errors that occur in coroutines you are waiting on. You would currently need to create a class that hold the type of value you like, pass it into the IEnumerator and have the coroutine function edit it. Lets make that a little nicer and support error handling.

Lets start off by seeing how we could wrap a coroutine and peek at each of the values as it runs.



	public IEnumerator InternalRoutine(IEnumerator coroutine){
		while(true){
			if(!coroutine.MoveNext()){
				yield break;
			}
			object yielded = coroutine.Current;
			Debug.Log(yielded);
			yield return coroutine.Current;
		}
	}

...
// Use like:
StartCoroutine(InternalRoutine(SomeOtherRoutine()));

Cool, but not too useful. Lets say we check each object to see if it's a specific type we care about. If it is, we end the routine and store the type somewhere.


	public IEnumerator InternalRoutine(IEnumerator coroutine){
		while(true){
			if(!coroutine.MoveNext()){
				yield break;
			}
			object yielded = coroutine.Current;

			if(yielded != null && yielded.GetType() == typeof(TypeICareAbout)){
				returnVal = (TypeICareAbout)yielded;
				yield break;
			}
			else{
				yield return coroutine.Current;
			}
		}
	}

Lets wrap it up in an object so that we can ask for the return value.


public class Coroutine<T>{
	private T returnVal;
	public Coroutine coroutine;
	
	public IEnumerator InternalRoutine(IEnumerator coroutine){
...
	}
}

And then we make an extension method to MonoBehaviour so that we can start them naturally.


public static class MonoBehaviorExt{
	public static Coroutine<T> StartCoroutine<T>(this MonoBehaviour obj, IEnumerator coroutine){
		Coroutine<T> coroutineObject = new Coroutine<T>();
		coroutineObject.coroutine = obj.StartCoroutine(coroutineObject.InternalRoutine(coroutine));
		return coroutineObject;
	}
}

Voila: use as follows:


	IEnumerator Start () {
		var routine = StartCoroutine<int>(TestNewRoutine()); //Start our new routine
		yield return routine.coroutine; // wait as we normally can
		Debug.Log(routine.returnVal); // print the result now that it is finished.
	}
	
	IEnumerator TestNewRoutine(){
		yield return null;
		yield return new WaitForSeconds(2f);
		yield return 10;
	}

Currently there is no nice way to deal with errors in coroutines; they just get spit out to the console. Lets catch errors in our internal routine and store them in the Coroutine<T> object. When someone tries to access the result, we'll throw an exception so they could handle it inline.


public class Coroutine<T>{
	public T Value {
		get{
			if(e != null){
				throw e;
			}
			return returnVal;
		}
	}
	private T returnVal;
	private Exception e;
	public Coroutine coroutine;
	
	public IEnumerator InternalRoutine(IEnumerator coroutine){
		while(true){
			try{
				if(!coroutine.MoveNext()){
					yield break;
				}
			}
			catch(Exception e){
				this.e = e;
				yield break;
			}
			object yielded = coroutine.Current;
			if(yielded != null && yielded.GetType() == typeof(T)){
				returnVal = (T)yielded;
				yield break;
			}
			else{
				yield return coroutine.Current;
			}
		}
	}
}

...
	IEnumerator Start () {
		var routine = StartCoroutine<int>(TestNewRoutineGivesException());
		yield return routine.coroutine;
		try{
			Debug.Log(routine.Value);
		}
		catch(Exception e){
			Debug.Log(e.Message);
			// do something
			Debug.Break();
		}
	}
	
	IEnumerator TestNewRoutineGivesException(){
		yield return null;
		yield return new WaitForSeconds(2f);
		throw new Exception("Bad thing!");
	}

Well thats all for now. Get the Full Script here. Follow me on twitter @horsman or Twisted Oak Studios at @TwistedOakGames

  • Rafael Cordoba

    script is not working at all: see the error here:
    http://cl.ly/image/1h0f1r3U2r1G

  • Welcome Back

    Thanks a bunch for this! :) I wasn’t much amused seeing that one cannot use the coroutine reference returned by the Monobehaviour.StartCoroutine() method to call a Cancel on it or something. I don’t get why this is not implemented by default. Anyways this saved me from rethinking all my coroutine processes, so thanks again :)


Twisted Oak Studios offers consulting and development on high-tech interactive projects. Check out our portfolio, or Give us a shout if you have anything you think some really rad engineers should help you with.

Archive

More interesting posts (29 of 33 articles)

Or check out our Portfolio.