Dennis Hackethal’s Blog
My blog about philosophy, coding, and anything else that interests me.
Breaking Out of Frames in Unity
Disclaimer: I’m a mere beginner at Unity and game development generally. I wrote this for myself as I am learning and figured others might find it useful as well. Exercise caution when running this code!
TL;DR: Coroutines allow you to divvy up function executions across multiple frames.
While building a first-person shooter from scratch to learn Unity, I ran into the following problem: as the player holds down the mouse, I wanted projectiles to shoot away from the player at a certain interval.
The Update
method runs once per frame. That means code run in Update
has to finish within a single frame. If you have expensive code you need to run, you shouldn't do it in Update
. And intervals are tricky to do when you're dealing with frames.
Now, you could theoretically declare a variable in which you store the timestamp at which the user first clicked, then check every frame if, say, 0.1 seconds have passed since, and, if so, fire another projectile, until the user lets go of the mouse.
This should work (though I haven't tried it, mind you) because shooting a single projectile and performing the time check both fit into a single frame. But this approach isn't exactly hassle-free. And for more expensive tasks, you'd be out of luck.
There's a better way: coroutines. Coroutines allow you to run code across several frames while also giving you precise control over when to start and stop them.
For example, consider this weapon class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour
{
public GameObject projectile;
void Update() {
if (Input.GetMouseButtonDown(0)) {
StartCoroutine("Shoot");
} else if (Input.GetMouseButtonUp(0)) {
StopCoroutine("Shoot");
}
}
private IEnumerator Shoot() {
while (true) {
Instantiate(projectile, transform.position, Quaternion.identity);
yield return new WaitForSeconds(0.1f);
}
}
}
In the Update
function, I start the coroutine the first frame the left click is registered (that's what Input.GetMouseButtonDown
does as opposed to Input.GetMouseButton
, which fires during every frame the left click is held – I want to start only one coroutine, not multiple at once). Once the user lets go (ends the left click, Input.GetMouseButtonUp
), I stop the coroutine. The parameter passed, in both cases, is the string "Shoot"
, which is the name of a private function on the same class (it need not be private).
The Shoot
method itself is very simple. It starts a loop and instantiates a projectile. Then it waits for 100 milliseconds, after which time the loop repeats. WaitForSeconds
not only implements the wait time, it also ensures that the system doesn't get overwhelmed by a constantly running loop. And stopping the coroutine ensures that, although there's no termination condition in the while loop, the loop stops when the coroutine as a whole is stopped.
Using a loop is important since we're not in the world of frames anymore, so we need to ensure that our code keeps running. It's not like Update
, which gets called for us every frame! Being used to this convenience, I actually wrote the coroutine without the loop first, and then wondered why it only triggered once.
C Sharp's yield
keyword may look a bit funky at first, so let's look into it a bit more. From the docs:
When you use the
yield
contextual keyword in a statement, you indicate that the method, operator, orget
accessor in which it appears is an iterator. [...]
You use ayield return
statement to return each element one at a time.
The sequence returned from an iterator method can be consumed by using a foreach statement [...]. Each iteration of theforeach
loop calls the iterator method. When ayield return
statement is reached in the iterator method,expression
is returned, and the current location in code is retained. Execution is restarted from that location the next time that the iterator function is called.
Saying "the current location in code is retained" is an understatement. The corresponding scope's state is retained in full, meaning your variables will be the same as when you left them after the previous iteration. A bit later, the docs continue:
You can use a
yield break
statement to end the iteration.
In the context of Unity, this – I'm guessing – is useful for when you want to stop the coroutine from the inside. However, I don't know how Unity keeps track of coroutines, so it's possible this approach would just leave a defunct coroutine somewhere in memory.
There may be other times when you do not wish to wait for a particular amount of time, but simply want to resume the coroutine upon the very next frame. The Unity docs give the following example of changing an object's color:
IEnumerator Fade() { for (float ft = 1f; ft >= 0; ft -= 0.1f) { Color c = renderer.material.color; c.a = ft; renderer.material.color = c; yield return null; } }
As you can see, in this case your iterator would just return null
– you wouldn't use WaitForSeconds
. The iterator is still spread across frames instead of completely running in a single frame, but the difference here is that it continues every frame, whereas WaitForSeconds
causes the iterator to continue only every x out of all frames.
What people are saying