Topic 10 of 15 10 min intermediate

Coroutines & Async

Doing things over time

Plenty of game logic happens over time: fade a screen, wait three seconds, spawn a wave every few beats. Coroutines let you write that as a straight sequence instead of a pile of timer fields.

What a coroutine actually is

A coroutine is a method that can pause and resume. It returns IEnumerator and uses yield return to hand control back to Unity, which continues it later from exactly where it stopped.

C#
private IEnumerator FadeOut()
{
    float elapsed = 0f;

    while (elapsed < duration)
    {
        elapsed += Time.deltaTime;
        canvasGroup.alpha = 1f - (elapsed / duration);
        yield return null;          // resume next frame
    }

    canvasGroup.alpha = 0f;
    yield return new WaitForSeconds(0.5f);
    LoadNextScene();
}

// Start it
StartCoroutine(FadeOut());

The yields worth knowing

yield returnWaits untilNote
nullNext frameThe everyday one.
new WaitForSeconds(t)t seconds of game timeFreezes when timeScale is 0.
new WaitForSecondsRealtime(t)t seconds of real timeKeeps running while paused.
new WaitForFixedUpdate()The next physics stepFor physics-timed work.
new WaitUntil(() => ready)The condition is trueChecked every frame.
StartCoroutine(Other())The other coroutine endsSequencing.
WaitForSeconds uses scaled time. Pause with Time.timeScale = 0 and every one of them stops — including the coroutine driving your pause menu animation. Use WaitForSecondsRealtime there.

Stopping them

C#
// Keep the handle so you can stop this exact one
private Coroutine spawnRoutine;

private void Start()
{
    spawnRoutine = StartCoroutine(SpawnWaves());
}

public void StopSpawning()
{
    if (spawnRoutine != null)
    {
        StopCoroutine(spawnRoutine);
        spawnRoutine = null;
    }
}

// Or stop everything this script started
private void OnDisable() => StopAllCoroutines();
StopCoroutine("MethodName") only works if you also started it by string, and that form is slower and unrefactorable. Keep the Coroutine handle instead.

When they silently stop

This surprises everyone at least once. Coroutines belong to the MonoBehaviour that started them.

  • Disabling the GameObject stops its coroutines. They do not resume when it is switched back on.
  • Destroying the object stops them.
  • Disabling only the component lets them keep running.
  • Loading a new scene stops everything that was not marked DontDestroyOnLoad.
If a coroutine must outlive the object that triggered it — a death animation, say — start it on a manager that stays alive instead.

Garbage worth avoiding

new WaitForSeconds(1f) allocates every time it runs. Once is nothing; inside a loop that runs all game it adds up to regular GC spikes. Cache it.

C#
// Allocates once, reused forever
private static readonly WaitForSeconds WaitOneSecond = new WaitForSeconds(1f);

private IEnumerator Tick()
{
    while (true)
    {
        DoTick();
        yield return WaitOneSecond;
    }
}

Coroutines or async/await?

C# has async/await, and it is genuinely better for some jobs — but it is not a drop-in replacement, because it knows nothing about Unity.

The jobUseWhy
Anything frame-basedCoroutineyield return null maps to a frame.
Tied to a GameObjectCoroutineStops automatically with the object.
Web requests, file IOasync/awaitReturns a value; has real error handling.
Heavy work off-threadasync/awaitCoroutines are single-threaded.
Needs try/catchasync/awaitYou cannot yield inside a try/catch with a catch clause.
An async method does not stop when its GameObject is destroyed. It keeps running and will happily touch a destroyed object, throwing MissingReferenceException. Pass a CancellationToken and cancel it in OnDestroy.
Cancelling async work properly
C#
private CancellationTokenSource cts;

private void Awake() => cts = new CancellationTokenSource();

private void OnDestroy()
{
    cts.Cancel();
    cts.Dispose();
}

private async Task LoadDataAsync()
{
    try
    {
        var result = await FetchAsync(cts.Token);

        // The object may be gone by the time we resume
        if (cts.IsCancellationRequested) return;

        Apply(result);
    }
    catch (OperationCanceledException)
    {
        // Expected on destroy - not an error
    }
}

What to take away

  • Coroutines turn "over time" logic into readable straight-line code.
  • yield return null waits a frame; WaitForSeconds waits scaled time.
  • Use WaitForSecondsRealtime for anything that must survive a pause.
  • Deactivating a GameObject kills its coroutines for good.
  • Cache WaitForSeconds objects used in loops.
  • Use async/await for IO and threads, coroutines for frames — and always cancel async work in OnDestroy.