Welcome to our website.

What Unity Coroutines Are Really Doing Between Frames

Many game systems do not finish their work in a single frame. Some are heavy, continuous jobs, such as pathfinding, where the computation is spread out so it does not crush the framerate. Others are mostly quiet, such as gameplay triggers, and only occasionally need to do something important. Plenty of game logic sits somewhere between those two extremes.

When a process has to run across several frames without using multiple threads, the work needs to be divided into pieces. With an algorithm built around a central loop, that division is often straightforward. An A* pathfinder, for example, can keep its open and closed node lists alive between frames and process only a few nodes each update instead of resolving the entire path immediately.

There is a tradeoff, though. If a game is effectively stepping forward at 60 or 30 frames per second, then a process that advances once per frame only gets 60 or 30 opportunities per second to do work. Splitting the work too finely can increase latency. A cleaner design often exposes a very small unit of work at one level—say, processing a single A* node—and then adds a layer that batches those units together, such as processing nodes for a fixed number of milliseconds before yielding control back to the game.

The awkward part is state. Once work is split across frames, whatever would normally live in local variables must survive until the next step. For a clean iterative algorithm, that may just mean turning the logic into a class that stores its lists, counters, and current position. But not every process is that tidy. A long-running computation might perform different kinds of work on different frames, leaving behind an object full of half-useful values that only exist because the next frame will need them. Sparse processes can be even more annoying: sometimes a small state machine is required simply to remember whether anything should happen at all.

Coroutines are Unity’s answer to that problem. They let you write code in a mostly linear shape, while marking points where execution should pause and resume later.

In UnityScript, the shape looks like this:

function LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield;
    }
}

In C#, the same idea is written as an iterator:

IEnumerator LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield return null;
    }
}

The C# clue: IEnumerator and yield

The important clues are in the C# version. The function returns IEnumerator, and it contains yield return. That is not a Unity-specific syntax trick. Unity’s C# support is built on ordinary C#, and yield is a regular C# keyword used in what the language calls iterator blocks.

IEnumerator represents a cursor moving through a sequence. The two members that matter here are:

  • Current, which exposes the value currently produced by the sequence.
  • MoveNext(), which advances the sequence and returns false when there is nothing left.

Because IEnumerator is only an interface, it does not dictate how those values are produced. MoveNext() might increment an index, read data from a file, compute something expensive, download data, or generate an infinite sequence. The interface only says that calling MoveNext() advances the cursor and that Current gives the value produced by that step.

Normally, implementing an interface means writing a class and defining all the required members. Iterator blocks avoid that boilerplate. If a method returns IEnumerator and uses yield, the compiler generates the underlying implementation for you.

A yield return X tells the generated enumerator, “this is the next value.” A yield break tells it there are no more values. When execution reaches yield return X, the generated MoveNext() stops, returns true, and sets Current to X. When execution reaches yield break, MoveNext() returns false.

The useful trick is that the returned values do not have to be interesting. You can repeatedly call MoveNext() and ignore Current; the code still runs up to the next yield each time. That means the important part of the iterator is often not the sequence of values it produces, but the side effects of the code that runs while producing them.

For example:

IEnumerator TellMeASecret()
{
PlayAnimation(“LeanInConspiratorially”);
while(playingAnimation)
yield return null;

Say(“I stole the cookie from the cookie jar!”);
while(speaking)
yield return null;

PlayAnimation(“LeanOutRelieved”);
while(playingAnimation)
yield return null;
}

This iterator effectively produces a long sequence of null values. But that is not what matters. What matters is that it starts an animation, waits while it is playing, says a line, waits while the speech is happening, then plays another animation and waits again.

You could drive that enumerator manually like this:

IEnumerator e \= TellMeASecret();
while(e.MoveNext()) { }

That runs it until it finishes. More usefully, the loop can be mixed with other logic:

IEnumerator e \= TellMeASecret();
while(e.MoveNext())
{
// If they press ‘Escape’, skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}

The underlying mechanism is simple: each call to MoveNext() advances the coroutine until the next yield point.

The yielded value becomes a scheduling hint

Every yield return expression has to yield something, even if that something is null. A coroutine that yields only null can still be useful because its side effects happen over time. But Unity makes the yielded value more meaningful.

Instead of treating every yielded value as irrelevant, Unity can inspect it and decide when the coroutine should be resumed. Often, continuing on the next frame is exactly what is wanted. But sometimes the code should resume after a sound finishes, after an animation completes, at the end of the current frame, or after a specific number of seconds.

Unity provides a base type called YieldInstruction, along with concrete instructions for common waiting patterns. WaitForSeconds resumes the coroutine after the requested time has passed. WaitForEndOfFrame resumes it later in the same frame at a specific point. A Coroutine itself can also be yielded; when coroutine A yields coroutine B, A is paused until B has completed.

A plausible runtime model looks like a scheduler that keeps different lists of coroutines depending on when they are eligible to run again:

List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;

foreach(IEnumerator coroutine in unblockedCoroutines)
{
    if(!coroutine.MoveNext())
        // This coroutine has finished
        continue;

    if(!coroutine.Current is YieldInstruction)
    {
        // This coroutine yielded null, or some other value we don't understand; run it next frame.
        shouldRunNextFrame.Add(coroutine);
        continue;
    }

    if(coroutine.Current is WaitForSeconds)
    {
        WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
        shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
    }
    else if(coroutine.Current is WaitForEndOfFrame)
    {
        shouldRunAtEndOfFrame.Add(coroutine);
    }
    else /* similar stuff for other YieldInstruction subtypes */
}

unblockedCoroutines = shouldRunNextFrame;

The real Unity implementation does not need to look exactly like this for the mental model to be useful. The coroutine runs until it yields. If it is finished, it disappears. If it yields null or some value the engine does not specially understand, it can be scheduled for the next frame. If it yields a known instruction, the engine can put it into the appropriate waiting group.

This also explains why additional yield instructions could make coroutines more expressive. Engine-level support for a signal system, for instance, could allow something like WaitForSignal("SignalName"). Then code could say yield return new WaitForSignal("GameOver"), which is clearer than repeatedly polling:

while(!Signals.HasFired(“GameOver”)) yield return null

If the engine implements the waiting behavior directly, it may also be faster than doing the same polling in script.

yield return is not a special Unity command

One subtle point is easy to miss: yield return simply yields an expression. YieldInstruction is an ordinary type. That means the expression can be chosen dynamically before yielding it.

YieldInstruction y;

if(something) y = null;
else if(somethingElse) y = new WaitForEndOfFrame();
else y = new WaitForSeconds(1.0f);

yield return y;

The familiar forms such as yield return new WaitForSeconds() and yield return new WaitForEndOfFrame() are not special syntax. They are just common expressions used with yield return.

You can drive a coroutine yourself

Because Unity coroutines in C# are iterator blocks, they can be iterated manually. The engine is not the only thing capable of calling MoveNext(). That opens the door to wrapping a coroutine with extra behavior, such as an interruption condition.

IEnumerator DoSomething()
{
    /* ... */
}

IEnumerator DoSomethingUnlessInterrupted()
{
    IEnumerator e = DoSomething();
    bool interrupted = false;
    while(!interrupted)
    {
        e.MoveNext();
        yield return e.Current;
        interrupted = HasBeenInterrupted();
    }
}

The wrapper advances the inner enumerator, yields whatever the inner coroutine yielded, then checks whether it should stop. This works because the coroutine is still just an IEnumerator.

There is one practical detail to be careful about: robust wrapper code normally needs to consider whether MoveNext() has returned false, because that means the inner coroutine has completed. The basic idea remains the same, though—the coroutine can be treated as an object that advances one yield point at a time.

Yielding other coroutines can mimic custom waits

Unity also allows a coroutine to yield another coroutine. That can be used to build higher-level waiting patterns, almost like custom yield instructions, although it is not necessarily as efficient as native engine support.

IEnumerator UntilTrueCoroutine(Func fn)
{
    while(!fn())
        yield return null;
}

Coroutine UntilTrue(Func fn)
{
    return StartCoroutine(UntilTrueCoroutine(fn));
}

IEnumerator SomeTask()
{
    /* ... */
    yield return UntilTrue(() => _lives < 3);
    /* ... */
}

This expresses the intent clearly: pause until a condition becomes true. The downside is that starting coroutines has a cost, so this approach is not always something to use freely in performance-sensitive code.

What this means in practice

A Unity coroutine is not a separate thread. It does not automatically move work away from the main thread, and it does not remove the need to think about how much work is being done per frame. Instead, it gives you a structured way to pause and resume execution without manually building a state machine for every multi-frame task.

The C# compiler turns an iterator block into an IEnumerator. Unity repeatedly advances that enumerator. Each yield return marks a pause point, and the yielded value tells Unity when the coroutine should be considered ready to continue. This is why coroutines are so useful for sequencing animations, timed waits, staged computations, cutscenes, gameplay triggers, and other logic that naturally unfolds over multiple frames.

Related Posts