U
19 / 22 · 10 min
FAQ

Coroutines: doing something over time without an Update

the short answer

How do I make something happen over time, or wait before it happens?

A coroutine is a method you can pause: yield return new WaitForSeconds(1f) waits a second and then carries on from that same line. Start it with StartCoroutine, keep the handle if you will ever need to stop it, and remember that a coroutine stops when the GameObject running it is deactivated or destroyed.

Update runs once per frame and then ends, so anything that takes time has to be rebuilt out of variables that survive between frames — a timer, a flag, a phase. A coroutine is the same logic written as a straight line that is allowed to pause in the middle.

The same job, written twice

Flash red for a moment, then go back
// With Update: three fields and a branch, and the logic is scattered.
bool flashing;
float flashEnds;

void Update()
{
    if (flashing && Time.time >= flashEnds)
    {
        mat.color = Color.white;
        flashing = false;
    }
}

void Hit()
{
    mat.color = Color.red;
    flashing = true;
    flashEnds = Time.time + 0.1f;
}

// With a coroutine: the whole story reads top to bottom, in one place.
IEnumerator Flash()
{
    mat.color = Color.red;
    yield return new WaitForSeconds(0.1f);
    mat.color = Color.white;
}

void Hit() => StartCoroutine(Flash());

The rules

  • The method returns IEnumerator and pauses with yield return. The yield is what makes it a coroutine rather than an ordinary method.

  • Calling it does nothing. Flash() on its own creates the coroutine and throws it away; StartCoroutine(Flash()) is what runs it.

  • It resumes in the same place in the frame it paused — after Update, before LateUpdate — so it is not a thread and nothing runs in parallel.

  • It belongs to the MonoBehaviour that started it. Deactivate that GameObject or destroy it and the coroutine stops mid-sentence, without finishing. Merely disabling the component does not stop it.

What you can wait for

The four that cover almost everything
yield return null;                              // one frame
yield return new WaitForSeconds(2f);
yield return new WaitForSecondsRealtime(2f);    // two seconds of wall time
yield return new WaitUntil(() => player.isAlive);
yield return StartCoroutine(Other());           // run Other, wait for it to finish

Moving something over time

The pattern behind every fade, slide and pop
IEnumerator MoveTo(Vector3 to, float seconds)
{
    Vector3 from = transform.position;
    float t = 0f;

    while (t < 1f)
    {
        t += Time.deltaTime / seconds;

        // SmoothStep instead of a plain Lerp: starts and ends gently.
        transform.position = Vector3.Lerp(from, to, Mathf.SmoothStep(0f, 1f, t));
        yield return null;
    }

    // Land exactly on the target: the loop leaves t slightly over 1.
    transform.position = to;
}

Stopping one

  • StopCoroutine("Flash") by name only works if you started it by name, and the string is never checked by the compiler. Keep the handle instead.

  • Coroutine c = StartCoroutine(Flash()); then StopCoroutine(c). This is the version that survives a rename.

  • Starting the same coroutine twice runs it twice. For a flash, stop the old handle first, or two overlapping flashes leave the colour wrong.

When not to use one

  • Something that runs every frame forever belongs in Update. A coroutine that is a while (true) loop with yield return null is an Update with extra steps.

  • Anything touching physics belongs in FixedUpdate, or in a coroutine yielding WaitForFixedUpdate. A normal coroutine resumes on the frame clock, not the physics one.

  • Hundreds of simultaneous coroutines cost real memory: each one allocates. For hundreds of identical timers, one Update over a list is cheaper.

the lesson that builds this

Skill system with cooldown

This post is a standalone recipe. In the course, the same thing is built as part of the one project that runs through all five tabs — Character & Combat, lesson 04.