Hope you’ve had a great week so far, and here’s to heading into the new weekend with some knowledge on coroutines! Way back when I first discovered coroutines, I was really confused by them – I didn’t quite understand how they worked and what they could do for me.
But once I took a short amount of time to learn about them, they have immensely improved my game programming skills. I use coroutines in just about every single project I work on and are an essential part of my development toolbelt.
So in this Feature Friday, we’ll be dissecting coroutines, more specifically:
- What they are and how to use them
- Common use cases and configurations
- How coroutines work under the hood
So strap on in and get ready to learn everything you need to know about how to start using coroutines effectively.
1. Introduction Into Coroutines
To understand coroutines and their benefits, we need to talk about how methods work in Unity. When you call a method in Unity, the entire code of that method is executed before the current frame is rendered and drawn to the screen.
So let’s say we want to make a simple movement method to drive a car 100 units down the road in our game. If you didn’t know any better, you may start off by writing something like this:
private void MoveCarForward(){
for(int i = 0; i < 100; i++){
_car.transform.position += Vector3.forward;
}
}
See the problem? When this method is called, the entire for loop is iterated through during the frame the method was called. So when we go to test our game, instead of the car driving down the road, the car appears to teleport from its starting location, to 100 units down the road.
Of course, this isn’t the behavior we intended to have for our car movement. So we need a way to spread out execution of this method over multiple frames.
If you’re familiar with the Unity game engine, then you may think to use the Update()
method. This is possible, however as you start to add complexity to your game, the Update()
loop can get quite messy, and in general I like to keep my Update()
loops as clean as possible. Also, we have little control over the frequency that our code executes in an Update()
method.
And thus brings us to coroutines. Coroutines give us a self contained method to spread out execution of a function that we have high control over. Let’s now write our MoveCarForward()
from before as a coroutine:
private IEnumerator MoveCarForward(){
for(int i = 0; i < 100; i++){
_car.transform.position += Vector3.forward;
yield return null;
}
}
This looks very similar to the method from above, with a few key differences. For starters, we declare the method with a return type of IEnumerator
(See below for more information on why this is). The other notable difference is the line yield return null;
The yield return
keywords essentially tell the computer to halt execution of the coroutine and return at a later time. After the yield return
keywords, we specify when we would like to resume execution of the coroutine. By saying yield return null
, this means return to resume execution on the next frame.
So now when our coroutine is initiated, the car will move one unit per frame just as we initially intended.
I should mention that initiating a coroutine is slightly different than calling a mehtod, but it is quite simple. All we do is call the StartCoroutine()
method, passing in the coroutine as such:
StartCoroutine(MoveCarForward());
Note that coroutines can take in arguments as parameters much like a regular method; and you can just pass them in inside the parenthesis as you’d expect.
And those are the basics on what coroutines are and how to use them. If you’ve never written a coroutine before, I’d highly recommend popping open Unity and your IDE of choice and playing around with them before reading any further.
And if you have been finding this helpful so far, please share this with a friend who would also like to read this.
2. Uses and Configurations of Coroutines
Now that you know what coroutines are and have experimented with them, now we can start to explore some of the other options we have when it comes to coroutines to take full advantage of their power.
Going back to the car movement coroutine we setup above, what if we wanted to slow down the movement of the car so that instead of moving one unit every frame, we move the car one unit every tenth of a second. Luckily there is a built in class we can add to our yield return
statement to do just that:
private IEnumerator MoveCarForward(){
for(int i = 0; i < 100; i++){
_car.transform.position += Vector3.forward;
yield return new WaitForSeconds(0.1f);
}
}
This means that the execution of the coroutine would halt and not resume exaction until at least 0.1 seconds have elapsed. And of course you can pass in any float value into the WaitForSeconds()
which is extremely nice to have for timers or other time based events in your game.
One thing to watch out for with the WaitForSeconds()
class is that it depends on the time scale of your game. So for example if you were to implement a simple pause game function where you set your time scale to 0, any coroutines would stop counting time until your game resumes and the time scale is set back to 1. And conversely, if you set your time scale to 2 to speed up your game, your coroutines would resume execution in half the time.
Now depending on your game and what you are trying to do with that specific coroutine, that may or may not be what you are trying to accomplish. Luckily there is another class called WaitForSecondsRealtime()
that will ignore the time scale. So even if you pause your game or speed it up, the coroutine will resume its execution in the exact number of seconds you passed into the function as such:
yield return new WaitForSecondsRealtime(15f);
With the two yield return
statements for time, you may have noticed the use of the new
keyword. You may know that any time you see the new
keyword, it means you are allocating new memory in the system. If you allocate too much memory, your system can crash.
Luckily, C# has a garbage collector which can cleanup any items in memory that are no longer needed. But you can’t rely on it too heavily because it can be very costly as far as performance goes. As a best practice, it is good to avoid allocating memory if it isn’t absolutely necessary.
So what can be done about that in coroutines? Well, back to our car movement example, if we want to always move the car at a fixed speed, we can cache the value of a WaitForSeconds()
variable in Start()
and use that in our coroutine rather than allocating a new one in memory every tenth of a second.
WaitForSeconds _timeBetweenMoves;
Start(){
_timeBetweenMoves = new WaitForSeconds(0.1f);
StartCoroutine(MoveCarForward());
}
private IEnumerator MoveCarForward(){
for(int i = 0; i < 100; i++){
_car.transform.position += Vector3.forward;
yield return _timeBetweenMoves;
}
}
As you see, a new WaitForSeconds
variable is initialized in Start()
and then we can just reference that in the yield return
statement without having to allocate more memory frequently. Now there will be certain circumstances where allocating lots of memory is unavoidable – that’s okay just be aware of instances where you can reduce the amount of allocation.
The other yield return types are: WaitForEndOfFrame()
, WaitForFixedUpdate()
, WaitUntil()
, and WaitWhile()
. Most of these are self-explanatory, but you can get some more information on the specifics in the Unity documentation:
https://docs.unity3d.com/ScriptReference/WaitForEndOfFrame.html
By the way, you can get real crazy with these yield return
statements as you can have as many as you want in a coroutine. You could execute some logic, wait for a few seconds, do some more things, wait until the next frame, come back for more logic, and so on.
So now that we’ve gone over how to start coroutines and configure the frequency of their execution, one thing that is important to know is how to stop a coroutine mid-execution. One simple way is by calling the StopAllCoroutines()
method. This will stop the execution of all coroutines associated with that instance and the execution of the coroutine will not not progress any further.
This may be useful for some scenarios i.e. at the end of a level in your game, but there is a safer way to stop a specific coroutine without disturbing any other coroutines. Now one thing I did not mention about the StartCoroutine()
method, is that it actually returns a Coroutine
type.
Knowing that, we can actually cache a coroutine as a variable and stop it whenever we like using the StopCoroutine()
method.
Coroutine _carMovement
Start(){
_carMovement = StartCoroutine(MoveCarForward());
}
public void ApplyBrakes(){
StopCoroutine(_carMovement);
}
private IEnumerator MoveCarForward(){
/* Movement logic */
}
And that is really all you need to know to get started using coroutines effectively. Now if you want to learn more about how coroutines work under the hood keep reading, otherwise jump to the bottom of the email for the featured Unity Asset Store Asset of the week.
3. How Coroutines Work Under the Hood
Because coroutines execute their code somewhat separately from your main game loop, when I first started learning about them, I thought that they ran on separate threads than the your regular game logic.
Coroutines are not multi-threaded, if you want to implement multi-threading in Unity, that is where something like the C# Job System comes in to play, but that is a topic for a different day.
Coroutines do in fact run on the main thread, and if you use the Unity Profiler to analyze the call stack, you will actually find that each coroutine appears twice – where StartCoroutine()
is called and in the DelayedCallManager
.
When a coroutine is first called, all of the initial code of the coroutine up until the first yield return
statement is logged in the call stack where StartCoroutine()
is called. All the remaining code execution appears in the DelayedCallManager
.
To the computer, a coroutine is seen as a collection of code execution steps. You may have noticed that when declaring a coroutine, it returns an IEnumerator
interface. The IEnumerator
interface is a simple interface that most C# collections implement.
The IEnumerator
interface provides a way to iterate through a simple collection of things – its only property: Current
returns the current element in the collection, and the two methods MoveNext()
and Reset()
increment the iterator and reset it to the initial position, respectively. With that in mind, you can see how a coroutine works as a collection of steps.
When a coroutine is initialized, the new collection is generated based off the number of times the code yields. When a coroutine yields, the IEnumerator
provides a way to get the current set of execution steps, which is then executed, and at the next yield statement, the iterator increments to the next bit of code – the cycle continues until all code has been ran.
Another key thing to know about coroutines, is how information about them is kept in memory by the computer. When your game is compiled, a new class is automatically created for each coroutine. Each time you initiate a new coroutine, an instance of that class is initialized and saved in memory.
These class instances contain information about where the code is in terms of execution and stores the values of local variables. The reason for this is because these variables need to persist over time through multiple yield calls, so they can’t simply be stored in the memory stack as most other local variables would in traditional methods.
Again, this is another one of those things to be aware of, that when using coroutines more memory is being allocated. And don’t be scared to use them because of that – computers are built to deal with this after all – but it is something to keep an eye on if you do run into any performance issues.
And that is a basic overview of how coroutines work under the hood. I hope that this brief explanation gave you a better understanding of how coroutines work and can help you as you implement these more in your game projects. There are a ton of great uses for coroutines, so just play around with them and figure out how you can get them to work for you.
Featured Unity Asset Store Tool of the Week
If you’ve ever tried to make your own custom editor or inspector windows, then you’ll know that it can be quite time consuming. Luckily the Odin Inspector and Serializer greatly reduces the difficulty and time to create custom Unity windows. The Odin Inspector gives you a suite of attributes that you can add to any Unity script for a much cleaner experience in the editor.
There are so many things you can do with the Odin Inspector and lots of quality of life things that it adds such as class extensions for common classes. There are far too many features to list out here in this email, so I’d suggest you head over to the Unity Asset Store and see what it’s all about – it’s absolutely one of my favorites.
Oh and did I mention it is 50% off right now? If you’ve been on the fence about buying the Odin Inspector and Serializer, now is the time to get it.
I hope that today’s email gave you a good introduction into the power of coroutines or that you learned something new if you already use coroutines in your projects. I’m serious when I say that these changed how I make games and that I use them in every single project I make.
Again if you found this email helpful, I’d really appreciate if you share it with someone would would like to read it. And do let me know what other topics you’d like to learn about in an upcoming Feature Friday.
Anyways I hope you are all staying well, and as always, keep on creating!
-Johnny Thompson
Turbo Makes Games
Recent Comments