Skip to main content

Polishing with Coroutines

To polish our game further, we need to ensure that:

  1. All player actions have discernible feedback
  2. The UI is coherent
  3. There exist a main menu to properly start a game
  4. Restart and pausing capability works as intended
  5. There exist some modularity in the code structure of the game, for instance: powerups

What we have learned so far: animation, sound management, input system, scriptable objects, singleton pattern, and asset management are more than sufficient to polish the game. But another aspect of polishing that we want to have in our game is responsiveness.

In certain situations, we might want to spread a sequence of events like procedural animations over time. We can utilise coroutines for this.

C#: Coroutines

A coroutine lets us spread tasks across several frames. It pauses execution and return control to Unity, and then continue where it left off on the following frame. A normal function like Update() cannot do this and must run into completion before returning control to Unity.

Coroutines are very resource efficient so it's okay to use them for your projects.

danger

It is important to understand that Coroutines are not threads. Synchronous operations that run within coroutines are still executed on Unity main thread. Do not use blocking operation in a coroutine. If you want to use threads for consuming an HTTP request, for example, you can use async methods instead.

You can declare and call Coroutine like this:

    IEnumerator  functionName(){
// implementation here
// typically contain a yield return statement
}

void UseCoroutine()
{
// call it
StartCoroutine(functionName());
// do other things (can be done immediately after coroutine yields)
}

Basic Idea

When you start a coroutine using StartCoroutine, the coroutine runs on the Unity main thread, just like the rest of your game code. It doesn't block the execution of the Update or other functions in your script. You also don't need to wait for the coroutine to finish before the Update function or other Unity event functions are called as per normal.

The Unity main thread continues to execute other code and events while the coroutine is running in the background. The coroutine itself will yield control back to the main thread when it encounters a yield statement, such as yield return new WaitForSecondsRealtime(time) (see later section), allowing other code to run during the specified time delay without freezing the entire game. Once the time delay is over, the coroutine resumes its execution.

In short, you can think of coroutines in Unity as a way to perform tasks over time or in the background without blocking the main thread and the normal execution of Unity's event functions like Update.

Examples

Health Replenish UI

One example of procedural animation is gradually increasing the health bar over time (e.g: after drinking a potion, etc):

HealthBar.cs
using System.Collections;
using UnityEngine;

public class HealthBar : MonoBehaviour
{
// an SO instance containing hp
public IntVariable playerHP;

public void RefillHealth(int amount)
{
StartCoroutine(HealthIncrease(amount));
}

IEnumerator HealthIncrease(float amount){
for(int x=1; x <= amount && playerHP.Value <= playerHP.maxValue >; x++){
playerHP.Value += x;
GameManager.instance.UpdateUI(); // calls for update
yield return new WaitForSeconds(0.2f);
}
}
}

Temporary Invincibility

For those making casual (chaotic) couch multiplayer game, you will also want to use coroutines to prevent the same player to be hit again before 0.5 seconds has passed. This will create temporary invincibility to avoid an unjust game experience. We would deactivate the player's hitbox or some vulnerable state for 0.5 seconds before enabling it again.

Fading Loading Screen

Another example is to fade UI elements in the loading scene right before game starts:

LoadAndWait.cs
using UnityEngine;
using UnityEngine.SceneManagement;

public class LoadAndWait : MonoBehaviour
{
public CanvasGroup c;

void Start()
{
StartCoroutine(Fade());
}
IEnumerator Fade()
{
for (float alpha = 1f; alpha >= -0.05f; alpha -= 0.05f)
{
c.alpha = alpha;
yield return new WaitForSecondsRealtime(0.1f);
}

// once done, go to next scene
SceneManager.LoadSceneAsync("World-1-1", LoadSceneMode.Single);
}

public void ReturnToMain()
{
// TODO
Debug.Log("Return to main menu");
}
}

We can tryout Fade above by creating another scene called LoadingScene with the following configuration. Feel free to create a Main Menu as well.

highlight on hover button

It's common to highlight a button by showing some image beside it, like the mushroom cursor on SuperMarioBros main menu. To do this, simply set the Normal Color of a button to have an alpha value of 0, and its Highlighted Color to have an alpha value of 1.

When you select a button on the UI, the button will remain "selected" even after you have finished clicking. In order to "reset" the button state, add the following instructions in the callback of that button (e.g: we want to reset highscore but not leave the button in the state of "pressed" after highscore is reset):

        GameObject eventSystem = GameObject.Find("EventSystem");
eventSystem.GetComponent<UnityEngine.EventSystems.EventSystem>().SetSelectedGameObject(null);

This is what the EventSystem GameObject in your scene is for, which is to manage events of mouse clicks and keyboard presses to interact with the UI elements (automatically created when you added your first UI Element GameObject to your scene).


yield return

On a coroutine, the yield return [something] returns control to Unity until that [something] condition is fulfilled. If we do yield return null, your coroutine execution pauses for the next frame and continues where it left off (after the yield return) afterwards, depending on whether that [something] condition is fulfilled, all these done without blocking the caller of the coroutine. That [something] can be also be any of the things below:

  1. Wait for a few seconds (scaled): yield return new WaitForSeconds(0.1f)
  2. Wait for a few seconds (realtime): yield return new WaitForSecondsRealtime(0.1f)
  3. Wait until next fixed update frame or until end of frame:
    • yield return new WaitForEndOfFrame()
    • yield return new WaitForFixedUpdate()
  4. Wait until a certain delegate value is true or false
    • yield return new WaitUntil(() => some_value > some_condition)
    • yield return new WaitWhile(() => some_value < some_condition)

The example below illustrates the behavior using WaitForSecondsRealtime:

using System.Collections;
using UnityEngine;

public class Coroutines : MonoBehaviour
{

void Start(){
Debug.Log("Begin Start event");
StartCoroutine(WaitCoroutine(2f));
Debug.Log("End Start event");
}

private IEnumerator WaitCoroutine(float time){
Debug.Log("Inside coroutine");
yield return new WaitForSecondsRealtime(time);
Debug.Log("Finish coroutine after "+time+" seconds");
}
}

// Printed output:
//
// Begin Start event
// Inside coroutine
// End Start event
// Finish coroutine after 2 seconds

The order of execution of the code at first begin as per normal: line 8, 9, then line 14, and 15. When yield return statement is met, we return control to Unity first (hence line 10 is executed). Two seconds later, line 16 is then executed.

Here's another example using WaitWhile (taken from Unity official documentation):

using UnityEngine;
using System.Collections;

public class WaitWhileExample : MonoBehaviour
{
public int enemies = 10; // we assume something else is reducing this value, one per second

void Start()
{
Debug.Log("Begin rescue mission");
StartCoroutine(Rescue("Toad"));
Debug.Log("Rescue mission started");
}

IEnumerator Rescue(string name)
{
Debug.Log($"Waiting for Mario to rescue {name}...");
yield return new WaitWhile(() => enemies > 0);
Debug.Log($"Finally, all enemies are eliminated and {name} have been rescued!");
}

}

// Printed output:
//
// Begin rescue mission
// Waiting for Mario to rescue Toad...
// Rescue mission started
// Finally, all enemies are eliminated and I have been rescued!
info

When you start a coroutine using StartCoroutine, it doesn't block the execution of the calling method (in the above case, Start). The coroutine runs separately in the background, and the calling method continues to execute immediately after starting the coroutine.

Remember that Unity coroutines are not executed on separate threads. They run on the Unity main thread, just like the rest of your Unity game code. Coroutine provides a way to perform asynchronous-like operations without introducing threading complexities.

Waiting for a Coroutine to Finish

If you have a very specific case such as to wait for Coroutine launched in a Start function before the Update function is launched, then you need to use some kind of flag to make this happen, for instance:

using System.Collections;
using UnityEngine;

public class SomeScript : MonoBehaviour
{
private bool isStartComplete = false;

void Start()
{
Debug.Log("Begin Start event");
StartCoroutine(StartEventCoroutine());
Debug.Log("Start Invoked");
}

private IEnumerator StartEventCoroutine()
{
// Perform any initialization or tasks you need in Start here.
// ...

// Assume you want to delay for 2 seconds before Update() works
yield return new WaitForSeconds(2f);

// Mark the Start as complete.
isStartComplete = true;
}

void Update()
{
// Only execute Update when StartEventCoroutine completes
if (isStartComplete)
{
// Your Update code here.
// ...
}
}
}

Starting Multiple Coroutines

You can also start many coroutines at once, such as:

    void Start()
{
Debug.Log("Begin rescue mission");
StartCoroutine(Rescue("Toad"));
StartCoroutine(Rescue("Peach"));
Debug.Log("Rescue mission started");
}

The output:

Stopping Coroutines

You can stop any coroutines by using the StopCoroutine method. There are three ways to do so: with the name of the IEnumerator as string put as the parameter, or the reference to the coroutine, or the Coroutine created during StartCoroutine. The method you choose should match how you started the Coroutine.

  • If a string is used as the argument in StartCoroutine, use the string in StopCoroutine. This method only works for coroutines with no parameters
  • If you use the IEnumerator in StartCoroutine, use its reference too in StopCoroutine.
  • Alternatively, you can use StopCoroutine with the Coroutine used for creation.
// Method 1 (string)
StartCoroutine("Fade")
StopCoroutine("Fade");

// Method 2 (using reference to the IEnumerator)
private IEnumerator coroutine;
coroutine = Rescue("Toad")
StopCoroutine(coroutine)

// Method 3 (using the Coroutine)
Coroutine RescueToad = StartCoroutine("Rescue", "Toad")
StopCoroutine(RescueToad)
note

A coroutine will also automatically stop if the object that it’s attached to is disabled by SetActive(false) or by destroying the object with Destroy().

If you would like to stop all Coroutines in the Behavior (Coroutines on the script), then you can use the method StopAllCoroutines(). Note that this will only stop coroutines that are in the same script so other scripts won't be effected.

C#: Async Methods and Multithreading With Task

danger

It is highly unlikely that you will need to implement async methods with extra threads in your game, but this section is added here to highlight differences between async methods and coroutines.

Unity's coroutines and C#'s async methods are separate mechanisms for handling asynchronous operations. Unity's coroutines are specific to the Unity game engine and provide a way to perform tasks over time or in the background without blocking the main thread. C#'s async/await feature, on the other hand, is a general-purpose mechanism for handling asynchronous operations in C#.

Async methods running on a separate threads are useful if you need to perform very extensive computation that requires millions of CPU cycles while keeping your game responsive. In other words, we want to utilise the CPU only after it's done computing whatever it needs for each frame.

Consider the following script. Here we test three different approach to perform an expensive PerlinNoise computation: the vanilla way (just sequential), using a coroutine, and using another thread (Task) asynchronously. When key c is pressed, this heavy computation will begin.

HeavyComputation.cs
using System.Collections;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public enum method
{
useVanilla = 0,
useCoroutine = 1,
useAsync = 2
}

public class AsyncAwaitTest : MonoBehaviour
{
public method method;
public int size = 10000;
private bool calculationState = false;


void Update()
{
if (Input.GetKeyDown("c"))
{
Debug.Log("Key c is pressed");
if (!calculationState)
{
switch (method)
{
case (method.useVanilla):
PerformCalculationsVanilla();
break;
case (method.useCoroutine):
StartCoroutine(PerformCalculationsCoroutine());
break;
case (method.useAsync):
PerformCalculationsAsync();
break;
default:
break;
}
Debug.Log("Perform calculations dispatch done");
}
}

if (Input.GetKeyDown("q"))
{
Destroy(this.gameObject);
}
}

void PerformCalculationsVanilla()
{
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
calculationState = true;
float[,] mapValues = new float[size, size];
for (int x = 0; x < size; x++)
{
for (int y = 0; y < size; y++)
{
mapValues[x, y] = Mathf.PerlinNoise(x * 0.01f, y * 0.01f);
}
}
calculationState = false;
stopwatch.Stop();
UnityEngine.Debug.Log("Real time elapsed using vanilla method: " + (stopwatch.Elapsed));
stopwatch.Reset();
}


IEnumerator PerformCalculationsCoroutine()
{
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();

calculationState = true;
float[,] mapValues = new float[size, size];
for (int x = 0; x < size; x++)
{
for (int y = 0; y < size; y++)
{
mapValues[x, y] = Mathf.PerlinNoise(x * 0.01f, y * 0.01f);
}
yield return null; // takes super long, only called at 60 times a second
}
calculationState = false;
stopwatch.Stop();
UnityEngine.Debug.Log("Real time elapsed Coroutine: " + (stopwatch.Elapsed));
stopwatch.Reset();
yield return null;
}

async void PerformCalculationsAsync()
{
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
var result = await Task.Run(() =>
{
calculationState = true;
float[,] mapValues = new float[size, size];
for (int x = 0; x < size; x++)
{
for (int y = 0; y < size; y++)
{
mapValues[x, y] = Mathf.PerlinNoise(x * 0.01f, y * 0.01f);
}
}
return mapValues;
});

calculationState = false;
stopwatch.Stop();
UnityEngine.Debug.Log("Real time elapsed using Async & Await: " + (stopwatch.Elapsed));
stopwatch.Reset();

}
}

In this video below, we bind the mouse right button click and movement with camera rotation to demonstrate how each method differs.

Vanilla Method

The vanilla method causes the game to be unresponsive for about 2 seconds.

Coroutine

When the method is changed into using a Coroutine, the time taken to complete the computation becomes close to 17 seconds instead (too long!). This is because of the yield statement at each outer loop (means the next loop is only resumed at the next frame). When x is set to 1000 and the game is run at 60fps, it takes 100060=16.67\frac{1000}{60} = 16.67 seconds to complete just this computation. This is way too long of a wait even though the system stays responsive in the meantime.

Async & Await with Task

What we wanted is to utilise our CPU as much as possible while staying responsive. To do so, we can run async function and await for Task completion. Remember that Update is only called 60 times a second (where we check for inputs, execute basic game logic, etc), so there’s plenty of leftover time that we can use to complete this calculation function. It still takes slightly more than 2 seconds to complete the computation. This shows that using async function does not (necessarily) make any computation time faster than the vanilla method, but in the meantime, the system is still responsive.

danger

Calling an async function without any await in its body results in synchronous execution. We need to await some Task as shown in the example above, e.g: var result = await Task.Run(().

Comparison With Coroutine

This section briefly covers the comparison between the two. There’s no better or worse solution, and you can simply choose the solution that suits your project best. The content for this section is distilled from this video.

Async Functions Always Complete

Async functions always runs into completion because they continue to run even after the MonoBehavior is destroyed, while Coroutines are run on the GameObject. Therefore, disabling the gameobject will cause any coroutine running on it to stop but doesn’t exit naturally.

Consider the following script:

TestDestroy.cs showLineNumbers
using System.Collections;
using UnityEngine;
using System.Threading.Tasks;
public class TestDestroy : MonoBehaviour
{
async void Start()
{
StartCoroutine(Sampletask());
SampleTaskAsync();
await Task.Delay(1000);
Destroy(gameObject);
}

async void SampleTaskAsync()
{
// This task will finish, even though it's object is destroyed
Debug.Log($"Async Task Started for object {this.gameObject.name}");
await Task.Delay(5000);
Debug.Log($"Async Task Ended for object {this.gameObject.name}");
}

IEnumerator Sampletask()
{
// This task won't finish, it will be stopped as soon as the object is destroyed
Debug.Log($"Coroutine Started for object {this.gameObject.name}");
yield return new WaitForSeconds(5);
Debug.Log($"Coroutine Ended for object {this.gameObject.name}");
}
}

Attaching the above script and running it will result in the output:

Notice how the message Coroutine Ended... never gets printed out, but instruction at line 19 causes an error because we tried to access the already destroyed GameObject's name (since async function always completes). Therefore you have to be careful when accessing the instance's member in async functions.

Memory Leak With Coroutine

On the other hand, Coroutines might result in memory leak if not used properly since it does not exit if the gameObject has been destroyed. Assets in Unity (textures, materials, etc) are not garbage collected as readily as other types. Unity will clean up unused assets on scene loads, but to keep them from piling up it's our responsibility to manage the assets we're creating, and Destroy() them when we're finished.

In the following example, the finally block never gets executed and thus results in memory leak.

    IEnumerator RenderEffect(UnityEngine.UI.RawImage r)
{
var texture = new RenderTexture(1024, 1024, 0);
try
{
for (int i = 0; i < 1000; i++)
{
// do something with r and texture
// then give control back to Unity after one interation
yield return null;
}
}
finally
{
texture.Release();
}
}

Stopping Async Functions

To be sure that Coroutines always exit especially on destroyed GameObjects, we need to be mindful to StopCoroutine(...) during onDisable of the GameObject. Likewise, we can also cancel the running of async functions using cancellation tokens.

    // declare and initialise at Start()
CancellationTokenSource token;

void Start(){
token = new CancellationTokenSource();
}


async void PerformCalculation(){

// passed the token when defining Task
var result = await Task.Run(() =>
{
calculationState = true;
float[,] mapValues = new float[size, size];
// ... implementation
for (.....){
// ... implementation
// periodically check for cancellation token request
if (token.IsCancellationRequested)
{
Debug.Log("Task Stop Requested");
return mapValues;
}
}
return result;
}, token.Token); // token passed as second argument of Task.Run()
}

// set the token to Cancel the function on object disable
void OnDisable()
{
Debug.Log("itemDisabled");
token.Cancel();
}

Return Values

We cannot return anything in a Coroutine, but async functions can the following return types (thats why we can await its results!):

  • Task, for an async method that performs an operation but returns no value.
  • Task<TResult>, for an async method that returns a value.
  • void, for an event handler.

For example, the following async function returns a Sprite placed at a path.

Resource Path

Using Resources.Load or Resources.LoadAsync, you can load an asset of the requested type stored at a path in the Resources folder. You must first create the Assets/Resources folder for this to work. Note that the path is case insensitive and must not contain a file extension. Read the full documentation here.

    async Task<Sprite> LoadAsSprite_Task(string path)
{
// getting sprite inside Assets/Resources/ folder
var resource = await Resources.LoadAsync<Sprite>(path);
return (resource as Sprite);
}

We can call them as such in Start() (notice how the Start method has to be async now to await this Task), and print some quick test in Update() to confirm if Update() is run at least for one frame before Start() is continued, and we can obtain some information about the return value of LoadAsSprite_Task async function. Here's an example:

AsyncReturnValue.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class AsyncReturnValue : MonoBehaviour
{
// Start is called before the first frame update
private bool testTask = false;
private int frame = 0;
async void Start()
{
Debug.Log("Start method begins...");
Sprite s = await LoadAsSprite_Task("Sprites/play-button"); // the actual file is at Assets/Resources/play-button.png
testTask = true;
Debug.Log("The sprite: " + s.name + " has been loaded.");
Debug.Log("Start method completes in frame: " + frame.ToString());
}
void Update()
{
frame++;
if (!testTask)
Debug.Log("Update called at frame: " + frame);
}

async Task<Sprite> LoadAsSprite_Task(string path)
{
// getting sprite inside Assets/Resources/ folder
ResourceRequest request = Resources.LoadAsync<Sprite>(path);
// While the request is not done, yield to Unity's coroutine system.
while (!request.isDone)
{
await Task.Yield();
}

return (Sprite)request.asset;
}

}

Here's the console output:

It shows that Start is called first as usual, but asynchronously, allowing Update to advance and increase the frame value. When the sprite has been loaded, the Start method resumes and print the Start method completes message.

danger

We can't await a Coroutine, it does not make sense because Unity's coroutines are not Task-based and don't return a Task object that you can await. However you can achieve similar result such as using a flag that will be set to true once a Coroutine completes.

Summary

Choosing between coroutines and async/await Task isn't always straightforward due to their differing functionalities.

It's essential to understand that asynchronous code doesn't always imply multithreading, and the behavior can vary depending on the specific APIs and libraries you're working with.

Coroutines are best for fire-and-forget tasks like fading the screen, replenishing health bar, triggering explosion on crates two seconds after it collides with the player and similar tasks, while async is essential for processing intensive tasks in the background without causing game stalls. Coroutines can be tricky, but async functions can get complex when handling task cancellation. In practice, using both methods in your project is common. As a broadly general rule, it may be simpler to employ coroutines for object-related game logic and reserve async for situations like executing lengthy background tasks.

Fix The Powerup Bug

If you follow the tutorial exactly, you will have a particular powerup bug. While our powerup works at first glance, having the collider placed above the box will cause problems if Mario approached it from above as follows:

The Starman powerup looks alright if we only collide with it from below, however it mistakenly collided with Mario even before spawned. On the other hand, the Magic Mushroom powerup didn't have that.

You need to disable the collider and set its RigidBody2D.type to static at first, and then enable the collider and set the RigidBody2D.type to dynamic upon SpawnPowerup(). However, you can't do it all in the same function because adding Impulse Force to a currently static body type will not work. Even though you have changed its type to dynamic in this frame, the Engine does not know yet and so you technically need to wait until the next frame to add the Impulse force to move the powerup once spawned. You can utilise Coroutine or utilise await Task.Delay() for this.