Unity : Managing the State

Unity : Managing the State

When you code a game with Unity, you invariably come up against the question of State and how to manage it.

State refers to the state the game is in at any given moment.

For example, your game will most likely start on a Splash Screen, then display an introduction page, allowing you to choose options and start the game.

We may then go on to an exposition page, explaining the scene the player is about to arrive in, and then play the game.

You might lose a life, then die, or pass the level.

All this can be modeled as a sequence of states:

public enum GameState
{
    SplashScreen,
    Intro,
    Settings,
    Playing,
    DeathAnimation,
    GameOver,
    Winner,
    ...
}

You can switch from one state to another: only certain transitions are possible.

The visibility of game objects or UI elements will depend on this state. State management is therefore vital to development.

***

There are several ways for a developer to manage this state.

Unity doesn't define any rules in this respect - implementation is up to you.

You can use the State Pattern - many implementations of which can be found on the Internet.

These implementations are very complete - in particular, they allow you to easily manage animation transitions of 3D models - but sometimes a little complex for simple applications, and a little difficult to upgrade, when you want to add another state for instance.

As far as I'm concerned, I have simpler needs; I don't claim to offer the best solution, but after several attempts at State management, I think I've reached a good compromise between :

  • development time / implementation complexity
  • the ability to easily add new states and elements
  • code readability


We sometimes forget to take into account the scalability of the solution (the ability to add a new state, because we're adding a step that wasn't planned), a mistake that can cost a lot of development time.

***

From a code structure point of view, we'll create a GameManager object (in a fairly classic way) which will be the only one responsible to pass the State from one value to another: this is important to separate and isolate responsibilities (and save hours of debugging...😒).

GameManager will invoke an event each time a State value is modified; all objects that subscribe to the event will therefore receive the State modification, and can show or hide themselves.

By the way:

It's tempting with Unity to Enable or Disable an object entirely to hide it. It's quick and easy.

BUT: in the technique I'm proposing, you can't Disable a UI panel entirely, for example, otherwise it won't be able to receive an event!

What I propose instead is to Disable all the panel's children (and not the panel itself); this way, it will continue to receive the State change event, and will be able to display its children again.

We create a CommonTools.cs script, which will contain the methods used to display or hide a panel's children:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// Tools used to display / hide all the children of a panel
/// </summary>
public class CommonTools : MonoBehaviour
{
    public static void HideChildren(GameObject go)
    {
        // Hide children
        for (int i = 0; i < go.transform.childCount; i++)
        {
            var child = go.transform.GetChild(i);
            child.gameObject.SetActive(false);
        }
    }

    public static void ShowChildren(GameObject go)
    {
        // Show
        for (int i = 0; i < go.transform.childCount; i++)
        {
            var child = go.transform.GetChild(i);
            child.gameObject.SetActive(true);
        }

    }
}


When the state changes, panels that need to be displayed or hidden will call these methods - we'll see about that later.

We create a GameManager.cs script and associate it with the GameManager object.

In this script, we define the enum containing the various possible States, and we add an Action, which is an event that will be invoked each time a State changes:

public class GameManager : MonoBehaviour
{
    public static Action<GameState> StateHasChanged; // Event invoked when state changes

...
}

public enum GameState
{
    Intro,
    Playing,
    GameOver,
    Winning
}

For this example, we're going to add a PanelIntro, which is displayed when the game starts - showing options and a Play button.

The object hierarchy will be as follows:

 

The PanelIntro object is a Panel, from which we've removed the image (the panel is a graphically empty object), and which uses a PanelIntro.cs script.

PanelIntro.cs code :

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PanelIntro : MonoBehaviour
{
    public static Action PanelIntroHasClosed;

    private void Awake()
    {
        // Register to GameManager change state
        GameManager.StateHasChanged += GameManager_StateHasChanged;
    }

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }

    private void GameManager_StateHasChanged(GameState newState)
    {
        if (newState == GameState.Intro)
            CommonTools.ShowChildren(this.gameObject); // Show all the children
        else
            CommonTools.HideChildren(this.gameObject);
    }

    public void BtnClosedHasBeenPressed()
    {
        PanelIntroHasClosed?.Invoke();
    }
}

A few explanations:

  • The button in the Panel calls the PanelIntro object's BtnClosedHasBeenPressed() function.
  • PanelIntro has a PanelIntroHasClosed event that will be triggered when the button is pressed
  • PanelIntro subscribes to GameManager's StateHasChanged event
  • PanelIntro displays all its children when State = Intro, otherwise hides them.

Finally, here's the code for the GameManager.cs script:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.SocialPlatforms;

public class GameManager : MonoBehaviour
{
    public static Action<GameState> StateHasChanged; // Event invoked when state changes

    private GameState state; // Current State of the game

    private void Awake()
    {
        // Register to PanelIntro events
        PanelIntro.PanelIntroHasClosed += PanelIntro_PanelIntroHasClosed;
    }

    // Start is called before the first frame update
    void Start()
    {
        // Starts by setting the default State (this way, objects will show / hide accordingly)
        switchToNextState(GameState.Intro);

    }

    // Update is called once per frame
    void Update()
    {

    }

    private void switchToNextState(GameState newState)
    {
        Debug.Log("Switch to" + newState.ToString());
        state = newState;

        // Send message to observers
        StateHasChanged?.Invoke(newState);

        switch (newState)
        {
            case GameState.Intro:
                // Control is passed to PanelIntro object
                break;
            case GameState.Playing:
                // Do what you need to do
                // ...
                break;
            case GameState.GameOver:
                // Control is passed to PanelIntro object
                break;
        }

    }

    private void PanelIntro_PanelIntroHasClosed()
    {
        // Ok, panel intro has been closed
        // Time to do some stuff and switch to another state - for instance Playing
        // ...

        switchToNextState(GameState.Playing);
    }

}

public enum GameState
{
    Intro,
    Playing,
    GameOver,
    Winning
}

A few more explanations:

  • GameManager is the only one to change the state via the switchToNextState private method: so there's no risk of changing the state in another object
  • GameManager subscribes to the PanelIntro event at the start
  • To initialize the State, switchToNextState is called from the Start() method.

Here's how it all works:

  • Each child object (Panel in particular) subscribes to StateHasChanged to find out when the state changes, then displays / hides its children, and does what it has to do.
  • GameManager lets each Panel manage its own operation: responsibilities respected
  • If you want to add a new state and a new panel, you'll need to add an event to the new panel, and have GameManager subscribe to it.

The advantage of this system is that it's easy to evolve a project and add more without getting bogged down in code and side effects.

Don't hesitate to give it a try and let me know what you think 😉.