Skip to content
Dunking Dog Games Dunking Dog Games

How to Use yield return in Unity: Advanced Coroutine Patterns & Examples

· Tutorials , C# , Unity

Advanced Unity yield return patterns explained

When working with Unity’s scripting system, coroutines are an essential feature for handling time-dependent tasks such as animations, delays, or timed events. Most developers know about yield return new WaitForSeconds(), but there are many other powerful uses of yield return that offer greater flexibility and cleaner code structure.

In this advanced Unity tutorial, we’ll explore how to effectively use yield return beyond basic waits, enabling you to write more modular, maintainable coroutine code.

Understanding yield return in Coroutines

A coroutine is a special method that allows you to pause execution and return control to Unity’s game loop. The function resumes from where it left off after the specified condition is met.

yield return controls this flow. Here’s the basic pattern:

IEnumerator ExampleCoroutine()
{
    Debug.Log("Starting...");
    yield return new WaitForSeconds(2f);  // Wait 2 seconds
    Debug.Log("2 seconds have passed!");
}

Beyond WaitForSeconds: Advanced Patterns

1. WaitUntil and WaitWhile

Wait for specific conditions rather than fixed time periods:

IEnumerator WaitForPlayerJump()
{
    while (!player.IsGrounded())
    {
        yield return null; // Wait one frame
    }
    
    Debug.Log("Player jumped!");
}

// Or using WaitUntil (Unity 2019+)
IEnumerator WaitForPlayerJumpModern()
{
    yield return new WaitUntil(() => player.IsGrounded());
    Debug.Log("Player jumped!");
}

Use cases:

  • Waiting for UI elements to appear
  • Pausing until a network request completes
  • Resuming when an animation finishes

2. Custom Yield Instructions

Create reusable wait conditions:

public class WaitForAnimation : CustomYieldInstruction
{
    private Animator animator;
    private string stateName;

    public WaitForAnimation(Animator animator, string stateName)
    {
        this.animator = animator;
        this.stateName = stateName;
    }

    public override bool keepWaiting
    {
        get
        {
            return !animator.IsInTransition(0) && 
                   animator.GetCurrentAnimatorStateInfo(0).IsName(stateName);
        }
    }
}

// Usage:
IEnumerator PlayAndDoSomething()
{
    animator.SetTrigger("Attack");
    yield return new WaitForAnimation(animator, "AttackComplete");
    Debug.Log("Animation finished!");
}

3. Chaining Coroutines

Build complex sequences from simple, reusable parts:

IEnumerator CutsceneSequence()
{
    // Step 1: Fade out current scene
    yield return StartCoroutine(FadeOut(1f));
    
    // Step 2: Load new scene asynchronously
    yield return StartCoroutine(LoadSceneAsync("Level2"));
    
    // Step 3: Fade in the new scene
    yield return StartCoroutine(FadeIn(1.5f));
    
    // Step 4: Play intro animation
    yield return StartCoroutine(PlayIntroAnimation());
}

IEnumerator FadeOut(float duration)
{
    float elapsed = 0f;
    while (elapsed < duration)
    {
        elapsed += Time.deltaTime;
        uiManager.SetAlpha(1f - (elapsed / duration));
        yield return null;
    }
}

4. Coroutine with Data Passing

While coroutines can’t directly return values, use callbacks or class members:

private string loadedSceneName;

IEnumerator LoadAndReport()
{
    AsyncOperation operation = SceneManager.LoadSceneAsync("Menu");
    
    while (!operation.isDone)
    {
        float progress = operation.progress;
        Debug.Log($"Loading... {progress * 100}%");
        yield return null;
    }
    
    loadedSceneName = SceneManager.GetActiveScene().name;
}

// Access result after coroutine completes:
void OnCoroutineComplete()
{
    Debug.Log($"Loaded scene: {loadedSceneName}");
}

5. Parallel Coroutines with WaitAll

Wait for multiple coroutines to complete (Unity 2021+):

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

6. Timed Coroutine with Early Exit

Combine time limits with conditions:

IEnumerator WaitForInputWithTimeout(float timeout)
{
    float elapsed = 0f;
    
    while (elapsed < timeout && !player.HasPressedStart())
    {
        elapsed += Time.deltaTime;
        yield return null;
        
        // Update UI timer display
        uiManager.UpdateTimer(timeout - elapsed);
    }
    
    if (player.HasPressedStart())
    {
        Debug.Log("Player started the game!");
    }
    else
    {
        Debug.Log("Timeout! Starting anyway...");
    }
}

Modular Coroutine Design Patterns

State Machine with Coroutines

public class EnemyAI : MonoBehaviour
{
    private enum State { Patrol, Chase, Attack }
    private State currentState;

    IEnumerator RunStateMachine()
    {
        while (true)
        {
            switch (currentState)
            {
                case State.Patrol:
                    yield return StartCoroutine(Patrol());
                    break;
                case State.Chase:
                    yield return StartCoroutine(ChasePlayer());
                    break;
                case State.Attack:
                    yield return StartCoroutine(Attack());
                    break;
            }
        }
    }
}

Coroutine Pool for Object Spawning

public class ObjectPool : MonoBehaviour
{
    public GameObject prefab;
    public int poolSize = 10;
    
    private Queue<GameObject> pool;

    void Start()
    {
        pool = new Queue<GameObject>();
        
        for (int i = 0; i < poolSize; i++)
        {
            var obj = Instantiate(prefab);
            obj.SetActive(false);
            pool.Enqueue(obj);
        }
        
        StartCoroutine(PeriodicSpawn());
    }

    IEnumerator PeriodicSpawn()
    {
        while (true)
        {
            if (pool.Count > 0)
            {
                var obj = pool.Dequeue();
                obj.SetActive(true);
                // Use object...
                yield return new WaitForSeconds(5f);
                obj.SetActive(false);
                pool.Enqueue(obj);
            }
            else
            {
                yield return null;
            }
        }
    }
}

Common Pitfalls and Solutions

1. Forgetting to Stop Coroutines

Always cancel coroutines when objects are destroyed:

private IEnumerator currentCoroutine;

void Start()
{
    currentCoroutine = DoSomething();
    StartCoroutine(currentCoroutine);
}

void OnDestroy()
{
    if (currentCoroutine != null)
        StopCoroutine(currentCoroutine);
}

2. Modifying Collections While Iterating

Never modify a list inside a coroutine that’s iterating over it:

// BAD - will throw exception
IEnumerator DestroyAllEnemies()
{
    foreach (var enemy in enemies)
    {
        Destroy(enemy.gameObject); // Modifies the collection!
    }
}

// GOOD - use a copy or reverse iteration
IEnumerator DestroyAllEnemiesSafe()
{
    for (int i = enemies.Count - 1; i >= 0; i--)
    {
        Destroy(enemies[i].gameObject);
    }
}

3. Race Conditions with Shared State

Use locks or coroutine sequencing to prevent conflicts:

private bool isProcessing = false;

IEnumerator ProcessData()
{
    if (isProcessing) yield break; // Skip if already processing
    
    isProcessing = true;
    
    // Do work...
    yield return new WaitForSeconds(2f);
    
    isProcessing = false;
}

Best Practices Summary

  1. Use WaitUntil/WaitWhile for condition-based waits instead of polling loops
  2. Create custom YieldInstructions for reusable wait conditions (animations, network calls)
  3. Chain coroutines to build complex sequences from simple parts
  4. Always stop coroutines in OnDestroy() or when objects are disabled
  5. Avoid modifying collections inside iterating coroutines
  6. Use callbacks instead of trying to return values from coroutines

Mastering yield return patterns will make your Unity code cleaner and more maintainable. Check out our other Unity tutorials for more tips!

Tags: c#unityprogramming