Making UIs with Coroutines in Unity

Originally I thought coroutines in Unity were exclusively for performing asynchronous operations, like AssetBundles.LoadAsync(). However when I was planning the UI for my new game, the most wise Eddie and Kalin told me about using coroutines to simulate a state machine. It took me a while to understand how this worked conceptually, and then how to implement it. I couldn’t find any particularly good tutorials either.

This is more of a series of annotated snippets than a full tutorial, but hopefully it will be useful for someone.

The important thing to remember with this is that we’re emulating a finite state machine. If you’re unfamiliar with FSMs, read up on them before starting this and sketch out a few FSMs for games you understand, thinking about what possible transitions there are between states, and what setup/teardown each state does.

With that out of the way, let’s look at some snippets!

Template Snippet

The first snippet is a template of what we’ll be doing with our UI-related coroutines. There’s not much to say about it beyond what’s in the comments.

public class UITest1 : MonoBehaviour {
    // Start can be a coroutine
    IEnumerator Start() {
        // Do initial setup

        while (true) {
            // Check states and jump in if conditions are met

            // Make sure to yield within the while loop
            yield return null;
        }
    }
}

Basic Pause Menu

Our second snippet has some code that actually does something useful.

public class UITest2 : MonoBehaviour {
    // Make private member variable visible in the inspector
    [SerializeField]
    GameObject m_pauseMenu;

    IEnumerator Start() {
        while (true) {
            // Check the condition to enter the state
            if (Input.GetKeyDown(KeyCode.Escape)) {

                // Enter the state
                yield return StartCoroutine(DoShowPause());

            }

            yield return null;
        }
    }


    IEnumerator DoShowPause() {
        // Initial setup on entering the state
        m_pauseMenu.SetActive(true);

        // Need to wait for one frame here as the key press event will still be
        // true during this frame
        yield return null;

        while (!Input.GetKeyDown(KeyCode.Escape)) {
            // Do whatever you want, like listen for menu click events

            // Make sure to yield in your while loops or you'll make Unity freeze
            yield return null;
        }

        // Tidy up on leaving the state
        m_pauseMenu.SetActive(false);
    }

}

The most important line in this snippet is yield return StartCoroutine(DoShowPause()). Here we start the pause coroutine, and importantly pause the Start method at that line until the DoShowPause coroutine finishes. As we are simulating a finite state machine, it will only finish when it exits the state.

One convention I like to use with coroutines is to prefix all coroutine method names with Do. The reason for this is if you call a coroutine without wrapping it in StartCoroutine() the coroutine stops at the first yield, but no warnings or errors are logged. By using a consistent naming convention, it’s easy to notice naked DoSomething() calls.

As in the comments make sure you include yield return null somewhere in your while loops or Unity will appear to freeze as it goes into an uninterrupted loop.

Player/Enemy Turn System with Pause

The third and final snippet for this tutorial is a very simple simulation of a turn-based game. It has alternating Player/Enemy turn states, and a pause state that can be called from either.

This snippet illustrates the possibility of having “global” states that can be called from any state. However you explicitly have to check for the trigger to enter the global state at a safe point in each state.

public class UITest2 : MonoBehaviour {
    enum Turn { Player, Enemy };
    Turn m_turn = Turn.Player;

    [SerializeField]
    GameObject m_enemyOverlay;
    [SerializeField]
    GameObject m_playerOverlay;

    bool IsPauseTriggered { get {
        return Input.GetKeyDown(KeyCode.Escape);
    } }

    IEnumerator Start() {
        while (true) {
            if (m_turn == Turn.Player)
                yield return StartCoroutine(DoPlayerTurn());
            else if (m_turn == Turn.Enemy)
                yield return StartCoroutine(DoEnemyTurn());

            // Make sure to yield within the while loop
            yield return null;
        }
    }

    IEnumerator DoEnemyTurn() {
        // Set up
        m_enemyOverlay.SetActive(true);
        while (m_turn == Turn.Enemy) {
            if (IsPauseTriggered)
                yield return StartCoroutine(DoPause());

            // Process enemy actions

            yield return null;
        }

        // Clean up
        m_enemyOverlay.SetActive(false);
    }

    IEnumerator DoPlayerTurn() {
        // Set up
        m_playerOverlay.SetActive(true);
        while (m_turn == Turn.Player) {
            if (IsPauseTriggered)
                yield return StartCoroutine(DoPause());

            // Wait for player input

            yield return null;
        }

        // Clean up
        m_playerOverlay.SetActive(false);
    }

    IEnumerator DoPause() {
        yield return null; // Wait one frame so Input is not triggered twice
        while (!IsPauseTriggered) {

            // Do nothing
            yield return null;
        }
    }
}

As you can see even with a simple situation, the code can get fairly long. In a more complicated game it would quickly become worthwhile to split up the code for each state into its own class.

So that’s a simple example of how you’d use coroutines to set up a FSM for menus. You can jump in and out of states easily and be sure which state you’re in.