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.
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 same logic as state fields, spread across the class
private bool isFading;
private float fadeElapsed;
private bool waitingAfterFade;
private float waitElapsed;
private void Update()
{
if (isFading) { /* ... */ }
else if (waitingAfterFade) { /* ... */ }
}The yields worth knowing
| yield return | Waits until | Note |
|---|---|---|
null | Next frame | The everyday one. |
new WaitForSeconds(t) | t seconds of game time | Freezes when timeScale is 0. |
new WaitForSecondsRealtime(t) | t seconds of real time | Keeps running while paused. |
new WaitForFixedUpdate() | The next physics step | For physics-timed work. |
new WaitUntil(() => ready) | The condition is true | Checked every frame. |
StartCoroutine(Other()) | The other coroutine ends | Sequencing. |
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
// 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.
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.
// 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 job | Use | Why |
|---|---|---|
Anything frame-based | Coroutine | yield return null maps to a frame. |
Tied to a GameObject | Coroutine | Stops automatically with the object. |
Web requests, file IO | async/await | Returns a value; has real error handling. |
Heavy work off-thread | async/await | Coroutines are single-threaded. |
Needs try/catch | async/await | You cannot yield inside a try/catch with a catch clause. |
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
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 nullwaits a frame;WaitForSecondswaits scaled time.- Use
WaitForSecondsRealtimefor anything that must survive a pause. - Deactivating a GameObject kills its coroutines for good.
- Cache
WaitForSecondsobjects used in loops. - Use async/await for IO and threads, coroutines for frames — and always cancel async work in
OnDestroy.