Coroutines

A coroutine is a special way to make logic happen over time. I must admit, I never used coroutines until Unity, I had been using event-based programming in every other comparable scenario. However, coroutines are a quick and easy alternative which is definitely worth a look. In this lesson I will show how Unity works with coroutines, including various ways of yielding control and even linking coroutines together in order to have full control over time-based logic. In the end, I will also show how to work with coroutines natively for anyone curious about how they work.

The Unity Coroutine

A coroutine is really just a method with a return type of “IEnumerator”. One key difference is that you don’t simply “return” the data type like you would in a normal method. Instead, you “yield” a value which causes execution of the logic to pause in place – it can be resumed again later. Let’s take a look at a quick sample:

using UnityEngine;
using System.Collections;

public class Demo : MonoBehaviour 
{
	void OnEnable ()
	{
		StartCoroutine("DoStuff");
	}

	void OnDisable ()
	{
		StopCoroutine("DoStuff");
	}

	IEnumerator DoStuff ()
	{
		int value = 0;
		while (true)
		{
			yield return new WaitForSeconds(1);
			value++;
			Debug.Log("value:" + value);
		}
	}
}

Note that we must make sure that the “System.Collections” namespace is used, or you will get an error: “The type or namespace name ‘IEnumerator’ could not be found. Are you missing a using directive or an assembly reference?”

In order to use a coroutine with Unity, you use a method called “StartCoroutine”. We do this inside of the OnEnable method (line 8). StartCoroutine is overloaded to allow you to pass an “IEnumerator” or a string representing the name of the coroutine to start. In the example I used the later version, because only that version can be manually “stopped” using StopCoroutine. I show StopCoroutine in the OnDisable method (line 13) although its use in this example is unnecessary because all coroutines managed by unity would stop when the script was disabled regardless of how they are started. You should also keep in mind that you can “StartCoroutine” multiple times – even for the same method, which may often be unintentional behavior. You may want to set a flag letting you know when a coroutine is active, so you don’t start it multiple times, or alternatively use StopCoroutine before using StartCoroutine just to be safe.

Here are a few variations of calling StartCoroutine:

// Version 1, started by string (name of coroutine) - this version is compatible with StopCoroutine
StartCoroutine("DoStuff");

// Version 2, Same as Version 1 but with a parameter - note, the target method must be modified to accept a parameter
StartCoroutine("DoStuff", 5);

// Version 3, This version is started by passing the IEnumerator - you CANT use StopCoroutine
StartCoroutine(DoStuff());

// Version 4, Same as Version 3 but with parameter - note, the target method must be modified to accept a parameter
StartCoroutine(DoStuff(5));

The method “DoStuff” is our “Coroutine”. The first statement creates a local variable named “value” and initializes it to zero. Then we begin an “infinite loop” (a “while” loop which loops for as long as “true” is equal to “true” – which is always). Inside the loop we see our first “yield” statement (line 21) which instructs Unity that we wish to wait for one second. At this point execution of this method is suspended and will not continue until our wait condition is satisfied. After waiting for one second, Unity resumes the Coroutine right where it left off, even keeping in tact the values of your local variables (in this case “value”). We increment value by one, and print it to the console window.

If you attach this script to something in your scene, you will see a new value printed to the console once every second. Try it out.

Unity has provided several options for yielding your Coroutine including:

  • WaitForEndOfFrame
  • WaitForFixedUpdate
  • WaitForSeconds
  • WWW

Note that you can also use “yield return null;” to simply wait a frame or “yield break;” to abort a Coroutine early.

A More Fun Sample

Our first example was functional but pretty boring. Let’s make another version where we make an object move across a series of locations (waypoints). I could imagine this as a piece on a game board that moves from one tile to another along a specified path.

using UnityEngine;
using System.Collections;

public class Demo : MonoBehaviour 
{
	public Vector3[] waypoints;
	public float speed;

	void OnEnable ()
	{
		StartCoroutine(DoStuff());
	}

	IEnumerator DoStuff ()
	{
		for (int i = 0; i < waypoints.Length; ++i)
		{
			while (transform.position != waypoints[i])
			{
				yield return null;
				transform.position = Vector3.MoveTowards(transform.position, waypoints[i], speed * Time.deltaTime);
			}
		}
		Debug.Log("Complete!");
	}
}

In this version of the script I declared a public array of Vector3 which represents locations in world space that I want the object to move through. I also specified a speed variable which determines how fast the object will cover those distances.

I start the coroutine by passing the IEnumerator directly. Note that this is the preferred way to begin a coroutine unless you MUST be able to stop the coroutine using Unity’s StopCoroutine method (You could insert logic into the method to abort early as an alternative).

The Coroutine has two loops which are nested together. The outer “for” loop iterates over the array of Vector3 waypoints, and the inner “while” loop iterates for as long as it takes for the object to actually reach its desired location. Note that I wait a frame before updating the objects position. If there were no yield statement inside the while loop, the object would complete its path before showing any of the “steps” of its progress to the user. Once the path has been followed to its final point, a message prints to the console indicating that our job is complete.

Create a new scene, and add a Cube. Attach this demo script and make sure to assign values to each of our public properties via the inspector. For example your waypoints could be:
5,0,0
5,1,0
5,1,3
3,0,0
0,0,0

and your speed could be 1. Of course you can use any values you like, but your speed should at least be greater than zero.

Nested Coroutines

Sometimes you may find it convenient to nest coroutines so you can reuse bits of logic. Here is a sample which does that:

using UnityEngine;
using System.Collections;

public class Demo : MonoBehaviour 
{
	Vector3 m1 = new Vector3(-1, 0, 0);
	Vector3 m2 = new Vector3(1, 0, 0);
	Vector3 s1 = new Vector3(1, 1, 1);
	Vector3 s2 = new Vector3(0.5f, 0.5f, 0.5f);

	void Start ()
	{
		StartCoroutine(DoStuff());
	}

	IEnumerator DoStuff ()
	{
		while (true)
		{
			switch (UnityEngine.Random.Range(0, 2))
			{
			case 0:
				yield return StartCoroutine(Move ());
				break;
			case 1:
				yield return StartCoroutine(Scale ());
				break;
			}
		}
	}

	IEnumerator Move ()
	{
		Vector3 target = transform.position == m1 ? m2 : m1;
		while (transform.position != target)
		{
			yield return null;
			transform.position = Vector3.MoveTowards(transform.position, target, Time.deltaTime);
		}
	}

	IEnumerator Scale ()
	{
		Vector3 target = transform.localScale == s1 ? s2 : s1;
		while (transform.localScale != target)
		{
			yield return null;
			transform.localScale = Vector3.MoveTowards(transform.localScale, target, Time.deltaTime);
		}
	}
}

This sample created three Coroutines. The DoStuff coroutine is the “main” coroutine which is triggered by our Start method. The Move and Scale coroutines are triggered randomly within the loop and will play until they are completed before the original main coroutine continues.

If you used the scene from the previous demo and just updated the script, you would now see your cube move randomly back and forth as well as scale up and down.

The Native Coroutine

You may be curious about how to use Coroutine’s outside of Unity’s implementation. If so check out the following simple example:

using UnityEngine;
using System.Collections;

public class Demo : MonoBehaviour 
{
	IEnumerator trend;

	void Start ()
	{
		trend = TrendLine();
	}

	void Update ()
	{
		if (trend.MoveNext())
			Debug.Log((int)trend.Current);
	}

	IEnumerator TrendLine ()
	{
		int value = 0;
		while (true)
		{
			value += UnityEngine.Random.Range(-1, 2);
			yield return value;
			if (Mathf.Abs(value) >= 10)
				yield break;
		}
	}
}

On line 6 I created a variable to hold a reference to an IEnumerator called trend. I assign it to our coroutine method in Start (line 10).

I use Unity’s update loop to handle resuming the Coroutine from its yielded execution points, although you could have used any sort of event to do so. Resuming the coroutine occurs with the “MoveNext” method which returns a true or false based on whether or not execution was completed. If the coroutine was not complete, I print the return value of the coroutine which is accessed by the “Current” property (line 16).

In this Coroutine, I generate the value of an imaginary trend line and watch how it grows until it reaches a maximum value at which point the coroutine is complete. I achieved this effect by causing a value to either go up, down, or remain the same based on the result of the Random number which Unity generates.

Attach this sample to something in scene and run it and you will see a bunch of numbers generate in the console window. At some point the trend line should reach its maximum extent at either positive or negative 10 and the output will stop.

Summary

In this lesson we learned all about a language feature called a coroutine. We learned to start and stop coroutines managed by Unity. We covered coroutines with and without parameters, used different yield options, and nested coroutines together. Finally we explored how a coroutine works natively and handled stepping through a coroutine and retrieving its values manually.

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