Skip to content
Dunking Dog Games Dunking Dog Games

How Does StartCoroutine / yield return Really Work in Unity?

· C# , Blog , Unity

Unity coroutine mechanics explained

When you dive into the world of game development with Unity, you quickly encounter the concept of coroutines. They are a powerful tool for managing processes that span multiple frames, providing an elegant way to handle asynchronous tasks. In this post, we’ll explore the inner workings of Unity coroutines, shedding light on what happens behind the scenes when you use them.

The Need for Coroutines

In game development, many tasks are not suitable for one-frame execution. Consider pathfinding algorithms or complex calculations; performing them all at once could lead to laggy gameplay. To mitigate this, you need to split these processes into smaller chunks that can be executed over several frames. Traditional programming methods often require you to explicitly manage state across frames, which can be cumbersome.

Enter coroutines – a feature provided by Unity (and many other environments) to address this challenge. Coroutines allow you to write a function as a single, uninterrupted block of code and designate specific points where it should “pause” and resume execution later.

How Coroutines Work Under the Hood

The State Machine Transformation

When you write a coroutine with yield return, Unity’s compiler transforms your method into a state machine. Here’s what happens:

// Your code:
IEnumerator MyCoroutine()
{
    Debug.Log("Step 1");
    yield return new WaitForSeconds(1f);
    Debug.Log("Step 2");
    yield return null; // Wait one frame
    Debug.Log("Step 3");
}

Becomes approximately this internally:

// Simplified state machine (what Unity generates):
class MyCoroutineStateMachine : IEnumerator
{
    public int currentState = -1;
    
    public bool MoveNext()
    {
        switch (currentState)
        {
            case 0:
                Debug.Log("Step 1");
                currentState = 1;
                return true; // Pause here
            
            case 1:
                if (!waitOneSecond.isDone) return false;
                Debug.Log("Step 2");
                currentState = 2;
                return true; // Pause here
            
            case 2:
                Debug.Log("Step 3");
                currentState = 3;
                return false; // Coroutine complete
        }
        return false;
    }
}

Key Insight: Coroutines Run on the Main Thread

This is the most important thing to understand about Unity coroutines: They do NOT run on separate threads. All coroutine code executes on Unity’s main thread, just like any other MonoBehaviour method.

The yield return statement doesn’t create a new thread—it simply tells Unity: “Pause execution here and resume later.”

Understanding yield return Values

Different yield return values tell Unity different things about when to resume:

Yield ValueBehaviorUse Case
nullResume next frameFrame-by-frame control
WaitForSeconds(float)Resume after time delayTimed events, delays
WaitForEndOfFrameResume after renderingPost-processing effects
WaitForFixedUpdateResume at next physics stepPhysics-related timing
WWW/UnityWebRequestResume when download completesAsset loading
AsyncOperationResume when operation finishesScene loading
CoroutineResume when nested coroutine endsChaining coroutines

Common Misconceptions

Myth 1: “Coroutines Create Background Threads”

False. Coroutines run entirely on the main thread. They cannot perform long-running operations without blocking the game loop. For true background work, use System.Threading.Thread or Task.Run().

// BAD - blocks main thread!
IEnumerator BadExample()
{
    for (int i = 0; i < 1000000; i++) { /* heavy computation */ }
    yield return null; // Game freezes during loop
}

// GOOD - use threads for heavy work
void Start()
{
    System.Threading.ThreadPool.QueueUserWorkItem(_ =>
    {
        // Heavy computation here (not on main thread)
        
        // Switch back to main thread when done:
        StartCoroutine(OnComputationComplete());
    });
}

Myth 2: “You Can Return Values from Coroutines”

Partially true. Coroutines return IEnumerator, not your actual data. To get results, use callbacks or class members:

private string result;

IEnumerator GetData()
{
    UnityWebRequest request = UnityWebRequest.Get("https://api.example.com/data");
    yield return request.SendWebRequest();
    
    if (request.result == UnityWebRequest.Result.Success)
    {
        result = request.downloadHandler.text; // Store in class member
    }
}

void OnCoroutineComplete()
{
    Debug.Log(result); // Access after coroutine finishes
}

Myth 3: “Coroutines Are Expensive”

Mostly false. Coroutines have minimal overhead—essentially just a state machine allocation. They’re far cheaper than creating actual threads or using complex event systems. The real cost comes from what you do inside the coroutine, not the coroutine itself.

Advanced Patterns

Chaining Coroutines

IEnumerator Sequence()
{
    yield return StartCoroutine(Step1());
    yield return StartCoroutine(Step2());
    yield return StartCoroutine(Step3());
}

IEnumerator Step1()
{
    Debug.Log("Starting step 1...");
    yield return new WaitForSeconds(1f);
    Debug.Log("Step 1 complete!");
}

Parallel Coroutines (Unity 2021+)

IEnumerator ParallelTasks()
{
    var task1 = StartCoroutine(DownloadData());
    var task2 = StartCoroutine(LoadAssets());
    
    // Wait for both to finish
    yield return new WaitAll(task1, task2);
    
    Debug.Log("Both tasks complete!");
}

Conditional Waiting

IEnumerator WaitForPlayerReady()
{
    while (!player.IsReady())
    {
        yield return null; // Check every frame
    }
    
    Debug.Log("Player ready! Proceeding...");
}

Best Practices Summary

  1. Remember: coroutines run on the main thread—don’t put heavy computation inside them
  2. Use yield return null for one-frame waits, WaitForSeconds() for time-based delays
  3. Always stop coroutines in OnDestroy() to prevent memory leaks
  4. Prefer named coroutines (StartCoroutine("MyCoro")) when you need to stop them later
  5. Avoid nested coroutines unless necessary—they can make debugging difficult

Understanding how coroutines work under the hood will make you a better Unity developer. Check out our other Unity tutorials for more insights! 🎮✨

Tags: c#unity