Introduction
Coroutines in Unity allow us to write code that executes synchronously while it doesn’t block the rest of our program. Essentially inside a coroutine we have some code that will execute when the method starts, then wait for some condition to be true while allowing our program to continue executing other code and finally when the condition is true will resume execution of our method.
Here is a simple example:
void Start()
{
Debug.Log("Before Coroutine");
StartCoroutine(SimpleCoroutine());
Debug.Log("After coroutine");
}
IEnumerator SimpleCoroutine()
{
Debug.Log("Start of Coroutine");
yield return new WaitForEndOfFrame();
Debug.Log("End Of Coroutine");
}
this piece of code will return:
Before Coroutine
Start of Coroutine
After coroutine
End Of Coroutine
this happens because the coroutine will suspend its execution until the end of frame and then the After coroutine
debug statement will execute. Near the end of the game loop the WaitForEndOfFrame()
will become true and in the next update the End Of Coroutine
will be printed.
The same result would have happened if instead we had written yield return null
. Although both statements have the same result there is a minor difference.
The WaitForEndOfFrame
is part of Unity’s game loop and executes after the rendering. See Order of execution for event functions.
The yield return null
tells our coroutine to execute the next time it is eligible, which is at the start of the next update in our case.
The result is the same but with a minor performance issue. Before that though, let’s quickly see the options that Unity provides for our yield return
statement:
Unity provided yield return options
The first waits until the end of frame when Unity has rendered our world and GUI, but before all that is displayed on screen.
The second waits until the next fixed update which usually happens every 0.02 seconds unless we have specified otherwise.
The third waits for an amount of time in seconds that we specify as an argument and is affected by our game time (the Time.TimeScale variable).
The fourth waits for an amount of time in seconds that we specify as an argument but is not affected by our game time. It also provides a keepWaiting
bool field that suspends our coroutine as long as it is true.
The last two accept a delegate that returns a bool and suspend our coroutine until the result is true or while the result is true respectively. Both of those also have the keepWaiting
bool field.
All of those are classes that we instantiate with the new keyword, and this might be a performance problem. See performance tips below.
finally we can also use the yield return statement to chain coroutines:
yield return StartCoroutine(SimpleCoroutine2())
will suspend our coroutine until the SimpleCoroutine2 has executed.
Starting a coroutine
We have two ways to start a coroutine. Both involve the StartCoroutine method.
The first is to provide as an argument our IEnumerator as in our example:
StartCoroutine(SimpleCoroutine())
The second is to provide as an argument a string that is the name of our coroutine:
StartCoroutine("SimpleCoroutine")
or even better:
StartCoroutine(nameof(SimpleCoroutine))
so that our IDE can inform us of any mistakes, if we decide at some point to change the name of our method.
The first way is preferable as we can also provide parameters for our coroutine if it has any arguments.
With the second way we can only provide one argument as a second parameter in the StartCoroutine method, but there is a problem: The second parameter is of type object. That means that there is boxing involved when that parameter is value type parameter.
Generally the first way is preferable in most cases, unless for some reason we want to stop a coroutine by using a specific method name.
The StartCoroutine
method returns a Coroutine object that can be used to stop the execution of the specific coroutine.
Stopping coroutines
As we can start coroutines, Unity provides us with two methods that we can stop coroutines, without having to wait for them to finish executing.
StopAllCoroutines()
will stop all coroutines running on the MonoBehaviour that is called
and
StopCoroutine(string methodName)
StopCoroutine(IEnumerator routine)
StopCoroutine(Coroutine routine)
will stop a specific coroutine, either by the method’s name as a string, if it has been created with a StartCoroutine that has a string parameter, or with the IEnumerator variable that was used to start the coroutine or finally with the coroutine itself.
Performance tips
The first thing we have to remember, is that coroutines still execute on the main thread. Although they execute synchronously they don’t create a new thread for our code to execute and that means we don’t improve the performance of our code.
In fact coroutines always impose a small overhead so they always make our code a little slower, but that performance loss is usually negligible considering the fact that there are situations where coroutines can make our code easier to read and understand.
The performance problem that can be easily avoided though, is with the yield return new
statement. If you remember at the beginning of this post I said that the statements yield return null
and yield return new WaitForEndOfFrame()
are almost the same, but in the second case we instantiate a new object of type WaitForEndOfFrame
.
That object is created on the heap and will create garbage when it is not needed anymore. In our example it is created only once, but consider the following example which is somewhat common:
private IEnumerator EnemySpawner(float time)
{
// some code at the beginning
while (aCondition)
{
// spawn enemy code
yield return new WaitForSeconds(time);
// some other code
}
}
This piece of code creates a new instance of WaitForSeconds
every time
seconds, which will be garbage collected after every iteration. This is not a lot (20 bytes) and probably won’t be a problem, but it is needless. By caching our instance we can avoid this problem without having to make our code less readable.
private IEnumerator EnemySpawner(float time)
{
WaitForSeconds delay = new WaitForSeconds(time);
// some code at the beginning
while (aCondition)
{
// spawn enemy code
yield return delay;
// some other code
}
}
Conclusion
Micro performance optimizations are not something that should concern us, unless we find out that there are actual performance problems. There are situations though that we can avoid needless loss in performance by simply obtaining good habits.
Caching our object instantiations at the beginning of our coroutines, instead of creating a new object at every iteration in our yield return statement, is one of those habits that doesn’t actually cost anything in readability or productivity when we write our coroutines.
Essentially, is a performance boost for no cost and even if that performance gain is small, I think it is a good habit to have when writing code that uses coroutines that contains loops.
Thank you for reading, if you think I forgot something or if you have any questions or comments you can use the comments section or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.