Handling UI with State Machine in Unity

One of my favorite use of State Machine design pattern is using states as different controllers of a game or an app. And today I’m going to present to you how you can implement such a thing in your Unity project! πŸ˜‰

As previously on AI subject with State Machine, we won’t change the implementation of State Machine and Base State scripts. They are staying the same. Maybe besides one reference and function call… 😜

Our goal here will be to create these states:

  • Menu state – to control menu
  • Game state – to control what is happening in the game
  • Pause state – to have a pause in the game
  • Game Over state – to display some summary of the game

Also, every state will have its own view that will have to take care of.

Setup

So first thing before we jump into those states, I’ve implemented UI views to make them as independent as I could. Their only purpose is to display view and pass user interactions to the states. This practice comes from one of the pillars of MVC, but I’m not going to speak about it now. So stay tuned for future posts! πŸ€“

To show you what I mean by that, here is the implementation of MenuView:

using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// Menu view class.
/// Passes button events.
/// </summary>
public class MenuView : BaseView
{
    // Events to attach to.
    public UnityAction OnStartClicked;
    public UnityAction OnQuitClicked;

    /// <summary>
    /// Method called by Start Button.
    /// </summary>
    public void StartClick()
    {
        OnStartClicked?.Invoke();
    }

    /// <summary>
    /// Method called by Quit Button.
    /// </summary>
    public void QuitClick()
    {
        OnQuitClicked?.Invoke();
    }
}

Rest of the UI scripts are available at the end of the post in public repository.

Now we are talking!

So what makes that approach so special? Most importantly, logic is implemented on the state-side where it belongs. Take a look on that code:

using UnityEngine;

/// <summary>
/// Menu state that show Menu view and add interpret user interaction with that view.
/// </summary>
public class MenuState : BaseState
{
    public override void PrepareState()
    {
        base.PrepareState();

        // Attach functions to view events
        owner.UI.MenuView.OnStartClicked += StartClicked;
        owner.UI.MenuView.OnQuitClicked += QuitClicked;

        // Show menu view
        owner.UI.MenuView.ShowView();
    }

    public override void DestroyState()
    {
        // Hide menu view
        owner.UI.MenuView.HideView();

        // Detach functions from view events
        owner.UI.MenuView.OnStartClicked -= StartClicked;
        owner.UI.MenuView.OnQuitClicked -= QuitClicked;

        base.DestroyState();
    }

    /// <summary>
    /// Function called when Start button is clicked in Menu view.
    /// </summary>
    private void StartClicked()
    {
        owner.ChangeState(new GameState());
    }

    /// <summary>
    /// Function called when Quit button is clicked in Menu view.
    /// </summary>
    private void QuitClicked()
    {
        Application.Quit();
    }
}

Isn’t it beautiful? On State Start, we are attaching functions to UI events, and they are connected until we change state. And this is true for every other state here.

Game State is just a little different because, in your project, you would need to load some levels or enemies, so there is something to do in your game. In my example, I have none of the above, so I’ve just left space for it in PrepareState to load, and in DestroyState I left space to destroy not needed content.

using UnityEngine;

/// <summary>
/// This is example of game state.
/// It shows game view and can load some content related to gameplay.
/// </summary>
public class GameState : BaseState
{
    // Variables used for loading and destroying game content
    public bool loadGameContent = true;
    public bool destroyGameContent = true;

    // Used when player decide to go to menu from pause state
    public bool skipToFinish = false;

    public override void PrepareState()
    {
        base.PrepareState();

        // Skip to finish game
        if (skipToFinish)
        {
            owner.ChangeState(new GameOverState { gameResult = GameResult.GetRandomResult() });
            return;
        }

        // Attach functions to view events
        owner.UI.GameView.OnPauseClicked += PauseClicked;
        owner.UI.GameView.OnFinishClicked += FinishClicked;

        // Show game view
        owner.UI.GameView.ShowView();

        if (loadGameContent)
        {
            // here we would load game content
        }
    }

    public override void DestroyState()
    {
        if (destroyGameContent)
        {
            // here we would destroy loaded game content
        }

        // Hide game view
        owner.UI.GameView.HideView();

        // Detach functions from view events
        owner.UI.GameView.OnPauseClicked -= PauseClicked;
        owner.UI.GameView.OnFinishClicked -= FinishClicked;

        base.DestroyState();
    }

    /// <summary>
    /// Function called when Pause button is clicked in Game view.
    /// </summary>
    private void PauseClicked()
    {
        destroyGameContent = false;

        owner.ChangeState(new PauseState());
    }

    /// <summary>
    /// Function called when Finish Game button is clicked in Game view.
    /// </summary>
    private void FinishClicked()
    {
        owner.ChangeState(new GameOverState { gameResult = GameResult.GetRandomResult() });
    }
}

Depending on use I also left a few variables that are used for loading, destroying content and skipping to game summary. The last one is important as we are going to need it to go from PauseState to GameOverState. It’s a great practice not to write code twice, so content managing for the game is only in GameState.

using UnityEngine;

/// <summary>
/// This is example of pause state.
/// It shows how you can implement pause menu as different state then game state.
/// </summary>
public class PauseState : BaseState
{
    public override void PrepareState()
    {
        base.PrepareState();

        // Stop time in game
        Time.timeScale = 0;

        // Attach functions to view events
        owner.UI.PauseView.OnMenuClicked += MenuClicked;
        owner.UI.PauseView.OnResumeClicked += ResumeClicked;

        // Show pause view
        owner.UI.PauseView.ShowView();
    }

    public override void DestroyState()
    {
        // Hide pause view
        owner.UI.PauseView.HideView();

        // Detach functions from view events
        owner.UI.PauseView.OnMenuClicked -= MenuClicked;
        owner.UI.PauseView.OnResumeClicked -= ResumeClicked;

        // Resume time in game
        Time.timeScale = 1;

        base.DestroyState();
    }

    /// <summary>
    /// Function called when Menu button is clicked in Pause view.
    /// </summary>
    private void MenuClicked()
    {
        // we are using skipToFinish variable to have finishing code in one place - game state
        owner.ChangeState(new GameState { skipToFinish = true });
    }

    /// <summary>
    /// Function called when Resume button is clicked in Pause view.
    /// </summary>
    private void ResumeClicked()
    {
        // we are disabling game content loading as game is already loaded and prepared
        owner.ChangeState(new GameState { loadGameContent = false });
    }
}

And the last state that we have to speak about is GameOverState. This one stores information about a game result which is just a simple class that stores random score here. As the state is enabled, we are passing that data to UI to be displayed. Rest is the same as in other states.

using UnityEngine;

/// <summary>
/// This is example of game over state.
/// It shows how you can handle data with game result and display it in UI view.
/// </summary>
public class GameOverState : BaseState
{
    // Data with information about game result
    public GameResult gameResult;

    public override void PrepareState()
    {
        base.PrepareState();

        // Attach functions to view events
        owner.UI.GameOverView.OnReplayClicked += ReplayClicked;
        owner.UI.GameOverView.OnMenuClicked += MenuClicked;

        // Pass data to display it in UI
        owner.UI.GameOverView.data = gameResult;

        // Show summary view
        owner.UI.GameOverView.ShowView();
    }

    public override void DestroyState()
    {
        // Hide summary view
        owner.UI.GameOverView.HideView();

        // Detach functions from view events
        owner.UI.GameOverView.OnReplayClicked -= ReplayClicked;
        owner.UI.GameOverView.OnMenuClicked -= MenuClicked;

        base.DestroyState();
    }

    /// <summary>
    /// Function called when Replay button is clicked in Game Over / Summary view.
    /// </summary>
    private void ReplayClicked()
    {
        owner.ChangeState(new GameState());
    }

    /// <summary>
    /// Function called when Menu button is clicked in Game Over / Summary view.
    /// </summary>
    private void MenuClicked()
    {
        owner.ChangeState(new MenuState());
    }
}

Results

We’ve programmed all behaviors into our states, and now we can see how they are working.

Super! We just learned about the new way to use State Machine design pattern! Isn’t it great?

I’ve prepared for you repository with this example so you can take a closer look at the implementation of states and UI elements. The repository is available at this link. I hope you’ll enjoy it! 😎

I hope you’ll enjoy it! 😎

avatar
  Subscribe  
Notify of