Dennis Hackethal’s Blog

My blog about philosophy, coding, and anything else that interests me.

Breaking Out of Frames in Unity

Published · 2-minute read

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, or get accessor in which it appears is an iterator. [...]
You use a yield 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 the foreach loop calls the iterator method. When a yield 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

What are your thoughts?

You are responding to comment #. Clear

Preview

Markdown supported. cmd + enter to comment. Your comment will appear upon approval. You are responsible for what you write. Terms, privacy policy
This small puzzle helps protect the blog against automated spam.

Preview