Coroutines

From ETC Public Wiki
Jump to: navigation, search

Reference

The Unity website has a reference page on Coroutines.

Concurrency

In games, virtual worlds, and other interactive multimedia experiences, it's often necessary to give the impression that several events are taking place at the same time. This is often referred to as concurrency. In fact, it's almost never the case. In Unity, the great majority of the time, there's only one thing ever happening at any time.

An example of several events taking place at one time could be:

  • The character controlled by the guest is moving forward.
  • Another character is moving towards her.
  • A clock has its hands rotating nearby.
  • The sun is setting is the sky is slowly changing color from blue to red to black.

From your guest's point of view, it will look like all this is happening at once. But in fact, it won't be. The simplest way Unity achieves this is by using coroutines.

Update

The image in movies, despite what the name "movie" night indicate, never really really moves at all. It's a sequence of static images. Games are the same. Each individual image is call a frame. Before rendering each frame, Unity will run the update method for each script attached to each object in the scene.

So, ideally, continuing from the above example, the update method should do the following:

  • Move the guest's character forward just a little bit.
  • Move the other character in the right direction just a little bit.
  • Turn the arrow on the clock just a little bit.
  • Shift the color of the sky just a little bit.

Of course, all of these instructions could change at any time depending on what's going on in the world. This is clearly not tenable if you were to do it manually. That's why we have coroutines.

The yield statement

Even though for Unity we'll use mostly C#, I'm going to use Python for the following example. Python has the advantage of being very succinct and readable. People who don't know Python should still be able to follow easily.

The following is a Python program that takes a list of numbers, doubles it, puts it in a string and prints each one:

>>> numbers = [1, 2, 3]
>>> def double_numbers(a_list):
...   new_list = [] # create a new empty list
...   for item in a_list:
...     new_list.append(item * 2)
...   return new_list
... 
>>> def add_text(a_list):
...   new_list = [] # create a new empty list
...   for item in a_list:
...     new_list.append('Number ' + str(item))
...   return new_list
... 
>>> for text in add_text(double_numbers(numbers)):
...   print text
... 
Number 2
Number 4
Number 6

What happens in the code above is that, for each transformation, a new list is returned. This works because these lists are tiny and the operations performed on them are simple. But what if the lists were large and the operations performed on them were complex? Then, there would be a very long wait for the first number to be printed, and all of them would appear at once. This is often not an efficient way to go. The answer to that is generators. Generators yield results rather than return them.

>>> numbers = [1, 2, 3]
>>> def double_numbers(a_list):
...   for item in a_list:
...     yield item * 2
... 
>>> def add_text(a_list):
...   for item in a_list:
 ...     yield 'Number ' + str(item)
... 
>>> for text in add_text(double_numbers(numbers)):
...   print text
... 
Number 2
Number 4
Number 6

In the preceding example, each number is computed in order. This is what happens:

  • the first item on the list is doubled
  • it is yielded
  • it is passed to the second function
  • the text is added to it
  • it is yielded again
  • it is printed
  • the loop in the first function goes on to the next number.

There is a slightly more detailed version of this code at the end of this tutorial.

In C#, structures that yield rather than return are called enumerators. If it is asked to yield a value for each frame, we can use this as a way to create concurrency. That's how coroutines work.

Example

Here's what's necessary to make a coroutine work in C# for Unity:

  • The actual coroutine must return an IEnumerator type.
  • The coroutine must yield a return value.
  • The coroutine must then be started with the StartCoroutine() function.
public class coroutineExample : MonoBehaviour{
   void Update()
   {
        //starts coroutine and runs it with the input of 5 seconds
        StartCoroutine(moveForward());
   }

   //waits t seconds and prints the number it waited
   IEnumerator moveForward()
   {
        while (true)
        {
            transform.Translate(Vector3.forward);
            yield return null;    
        }
   }
}

Each frame, the object's position (its transform value) will move forward a bit. This won't work as expected because not all frames take as much time to calculate and each frame, the object will always move the same distance. So the movement will be erratic. To compensate for this, we multiply the object's movement by the amount of time passed between this frame and the previous one. Unity provides this value to us as deltaTime.

public class coroutineExample : MonoBehaviour{
   void Update()
   {
        StartCoroutine(moveForward());
   }
 
   IEnumerator moveForward()
   {
        while(true)
        {
            transform.Translate(Vector3.forward *  Time.deltaTime);
            yield return null;
        }
   }
}

You can't really make your code sleep in Unity. If you do, the whole game stops. Fortunately, another side effect of coroutines is that, rather than just yield nothing, they can also yield instructions. Unity will know how to use these instructions to do things like stop running the coroutine for a while.

Here's an example where a coroutine is used to wait for five seconds before printing a value.

public class coroutineExample : MonoBehaviour{
   void Start()
   {
        //starts coroutine and runs it with the input of 5 seconds
        StartCoroutine(waitAndPrint(5));
   }

   //waits t seconds and prints the number it waited
   IEnumerator waitAndPrint(float t)
   {
        yield return new WaitForSeconds(t);    
        Debug.Log("Time has passed...");
   }
}

The coroutine starts immediately. It yields a WaitForSecounds object to the SartCoroutine function. That function then knows to wait the specified amount of time to ask the coroutine for the next value. After five seconds, the coroutine is resumed and the text is printed. For the outside, it looks like the script waited for five seconds.

Coroutines for scheduling events

Let's imagine you have a virtual world where after 50 seconds night falls and a minute after that fireworks start and another minute later smooth music is played. Here's how you'd probably be tempted to do it:

public class someScript : MonoBehaviour{
   void update()
   {
        if (Time.timeSinceLevelLoad >= 50f) startNight();
        if (Time.timeSinceLevelLoad >= 110f) startFireworks();
        if (Time.timeSinceLevelLoad >= 170f) startMusic();
   }

   // great code goes below
   // ...
}

But this won't work. The start... functions will be started again at every frame. That's probably not what you want. One way around that is to do the following.

public class someScript : MonoBehaviour{
   private bool nightHasStarted = false;
   private bool fireworksHaveStarted = false;
   private bool musicHasStarted = false;

   void update()
   {
        if (Time.timeSinceLevelLoad >= 50f && !nightHasStarted) startNight();
        if (Time.timeSinceLevelLoad >= 110f && !fireworksHaveStarted) startFireworks();
        if (Time.timeSinceLevelLoad >= 170f && !musicHasStarted) startMusic();
   }


   void startNight() {
      nightHasStarted = true;
      // more code
      // ...
   }

   void startFireworks() {
      fireworksHaveStarted = true;
      // more code
      // ...
   }

   void startMusic() {
      musicHasStarted = true;
      // more code
      // ...
   }

   // ...
}

This quickly gets messy and out of hand. Coroutines provide a much cleaner and more concise way to accomplish the same thing.

public class coroutineExample : MonoBehaviour{
   void start()
   {
        // Run the scheduled events
        StartCoroutine(worldSchedule());
   }

   // Define the schedule of events.
   IEnumerator worldSchedule()
   {
        yield return new WaitForSeconds(50f);    
        startNight();
        yield return new WaitForSeconds(60f);    
        startFireworks();
        yield return new WaitForSeconds(60f);    
        startMusic();
   }

   // more code
   // ...
}

Your scheduled events are now listed in order and in more readable way. Furthermore, the game does not have to go through a list of if statements on each update for functions that will only need to be run once each.

Coroutines and Lerping

One common use case for coroutines is to use Vector3.Lerp to move an object from one position to another over a given span of time.

Lerp is short for Linear Interpolation.

The Unity website has documentation about the Vector3 lerp as well as for the general float lerp.

In short, Vector3.Lerp will blend between two vectors based on the third parameter which should scale from 0 to 1. If it is 0, it will return the first vector, as that parameter approaches 1 it will blend the two until it returns the second vector when it is 1. This scales from 0 to 1 as time progresses in the coroutine.

The Lerp function

Let's take an example that illustrates how lerps calculate their data. This example is to illustrate how lerps work only. In practice, it's generally a bad idea to use Time.time with lerps. For actual code, it's much better to accumulate time like in the code example below.

Let's say that one minute after the world has started, an object has 10 seconds to go from position 100 to position 200, the way the lerp function will behave is described in the function below.

Time.time (Time.time - 60f) / 10f Mathf.Lerp(100, 200, (Time.time - 60f) / 10f)
20 -4 100
60 0 100
61 .1 110
65 .5 150
68 .8 180
70 1 200
80 2 200
1000 94 200

No matter what, the return value of the lerp will never be below the specified minimum or above the specified maximum.

A example with actual code

Unity will calculate differences of position between two points in three dimensions. Here is simple example in code. Here, we're accumulating time into a variable rather than checking a builtin value each time. This is really the way to go.

IEnumerator MoveThisObject(Vector3 target, float moveDuration)
{
	// store the starting position of the object this script is attached to as well as the target position
	Vector3 oldPos = transform.position;
	Vector3 newPos = target;
	float moveTime = 0.0f;

	while (moveTime < moveDuration)
	{
		moveTime += Time.deltaTime;
		transform.position = Vector3.Lerp(oldPos, newPos, moveTime / moveDuration);
		yield return null;
	}
}

Here is another example for rotating an object using Slerp.

    IEnumerator Rotater(float degrees, float rotDuration)
    {
        IsTurning = true;
        Quaternion startRotation = transform.rotation;
        Quaternion newRotation = Quaternion.Euler(new Vector3(
            transform.rotation.eulerAngles.x,
            transform.rotation.eulerAngles.y - degrees,
            transform.rotation.eulerAngles.z));
       
        float elapsedTime = 0.0f;
        while (elapsedTime < rotDuration)
        {
            elapsedTime += Time.deltaTime;
            float t = elapsedTime / rotDuration;
            transform.rotation = Quaternion.Slerp(
                startRotation,
                newRotation,
                t);
            yield return null;
        }
        IsTurning = false;
    }

Conclusion

So coroutines are an extremely useful way of organizing the code in such as way that while only one thing is ever done at any time, it makes the illusion of concurrency very easy to handle. It's essential to understand coroutines to make good use of Unity.


A more detailed yield example

>>> def double_numbers(a_list):
...   new_list = [] # create a new empty list
...   for item in a_list:
...     print 'processing ' + str(item)
...     new_list.append(item * 2)
...   return new_list
... 
>>> def add_text(a_list):
...   new_list = [] # create a new empty list
...   for item in a_list:
...     new_list.append('Number ' + str(item))
...   return new_list
... 
>>> for text in add_text(double_numbers(numbers)):
...   print text
... 
processing 1
processing 2
processing 3
Number 2
Number 4
Number 6
>>> def double_numbers(a_list):
...   for item in a_list:
...     print 'processing ' + str(item)
...     yield item * 2
... 
>>> def add_text(a_list):
...   for item in a_list:
...     yield 'Number :' + str(item)
... 
>>> for text in add_text(double_numbers(numbers)):
...   print text
... 
processing 1
Number :2
processing 2
Number :4
processing 3
Number :6

These examples show how yield provides the illusion of concurrency.