Create a Finite State Machine in Unity 3D using Zenject

One useful design pattern in game development is the State design pattern. It can be used in various occasions from player input to enemy AI.

We can manage all the states using a Finite State Machine. With a FSM we have distinct finite states that we can transition to, but having only one state enabled at any given time.

In our example we will implement a Game Manager, that controls our game states in Unity, using Zenject framework. Zenject is Dependency Injection Framework for Unity 3D.

Taken from the gitHub documentation:

Zenject is a lightweight dependency injection framework built specifically to target Unity 3D (however it can be used outside of Unity as well). It can be used to turn your application into a collection of loosely-coupled parts with highly segmented responsibilities. Zenject can then glue the parts together in many different configurations to allow you to easily write, re-use, refactor and test your code in a scalable and extremely flexible way.

For our example we will use Unity 5.6.0 and Zenject 5.1.0.
Zenject 5.2.0 has been recently released, so if you prefer you can use the newer version.
If you use an older version of Unity, the latest Zenject version might not be compatible, so you might need to download a previous version from the Releases Page.

You can install Zenject using different methods. The easiest way is to download it from the Asset Store Page. The above version includes two sample games (Asteroids and SpaceFighter) to further explore Zenject framework.

For a cleaner project, we will download the unity Zenject 5.1.0. package from the Releases Page without the sample projects.

Now create a new unity project and import Zenject 5.1.0 package. Right click in the Hierarchy and select Zenject → SceneContext. Then a SceneContext gameObject will be created.

The SceneContext component is the entry point of the application, where Zenject set up all the various dependencies.

Now in the Assets folder create a Scripts folder and inside it an Installers and a GameManager folder. Right click inside the Installers folder and choose Create → Zenject → MonoInstaller and name it GameInstaller.

Add GameInstaller script in the SceneContext GameObject and add a reference to your SceneContext component, by adding a new row in the inspector of the “Installers” property (press the + button) and then dragging the GameInstaller GameObject to it.

An Installer declares all the dependencies used in your scene and their relationships with each other.

It’s a good time to save our scene, so we give our scene the FSM_Example name and we put it inside a Scenes folder for better organization.

We are going to set aside the GameInstaller for now and focus on the creation of our GameManager. Inside GameManager folder create a new C# script called GameManager.

Fill it with the code below:

using UnityEngine;
using UnityEngine.UI;
using Zenject;
using FSM.GameManager.States;

namespace FSM.GameManager
{
    public class GameManager : MonoBehaviour
    {
        private GameStateFactory _gameStateFactory;

        private GameStateEntity _gameStateEntity = null;

        [SerializeField]
        private GameState _currentGameState;
        [SerializeField]
        private GameState _previousGameState;

        public Text stateText;

        [Inject]
        public void Construct(GameStateFactory gameStateFactory)
        {
            _gameStateFactory = gameStateFactory;
        }

        private void Start()
        {
            ChangeState(GameState.Menu);
        }

        /// <summary>
        /// Changes the game state
        /// </summary>
        /// <param name="gameState">The state to transition to</param>
        internal void ChangeState(GameState gameState)
        {
            if (_gameStateEntity != null)
            {
                _gameStateEntity.Dispose();
                _gameStateEntity = null;
            }

            _previousGameState = _currentGameState;
            _currentGameState = gameState;

            stateText.text = gameState.ToString();

            _gameStateEntity = _gameStateFactory.CreateState(gameState);
            _gameStateEntity.Start();
        }
    }
}

Don’t worry about the missing type/namespace errors. We will add all the missing dependencies in the following steps.

Normally a class that inherits from MonoBehavior does not have a constructor, but using the [Inject] attribute of the Zenject framework, we can inject our dependencies into our classes. There are a lot of ways to have these dependencies injected, but for our Construct method we are using what we call a Method Injection.

The core method is ChangeState which handles the state transitions. The way that we will implement the state change, is by using a Factory. Factories are used to create objects dynamically, but in our case we will use it to create a new state.

This lead us to the creation of the next script’ the GameStateFactory. Inside the GameManager folder create a new C# script called GameStateFactory. Include the following code:

using ModestTree;
using FSM.GameManager.States;

namespace FSM.GameManager
{
    internal enum GameState
    {
        Menu,
        Gameplay,
        GameOver,
        Victory
    }

    public class GameStateFactory
    {
        readonly MenuState.Factory _menuFactory;
        readonly GameplayState.Factory _gameplayFactory;
        readonly GameOverState.Factory _gameOverFactory;
        readonly VictoryState.Factory _victoryFactory;

        public GameStateFactory(MenuState.Factory menuFactory,
                                GameplayState.Factory gameplayFactory,
                                GameOverState.Factory gameOverFactory,
                                VictoryState.Factory victoryFactory)
        {
            _menuFactory = menuFactory;
            _gameplayFactory = gameplayFactory;
            _gameOverFactory = gameOverFactory;
            _victoryFactory = victoryFactory;
        }

        /// <summary>
        /// Creates the requested game state entity
        /// </summary>
        /// <param name="gameState">State we want to create</param>
        /// <returns>The requested game state entity</returns>
        internal GameStateEntity CreateState(GameState gameState)
        {
            switch (gameState)
            {
                case GameState.Menu:
                    return _menuFactory.Create();

                case GameState.Gameplay:
                    return _gameplayFactory.Create();

                case GameState.GameOver:
                    return _gameOverFactory.Create();

                case GameState.Victory:
                    return _victoryFactory.Create();
            }

            throw Assert.CreateException("Code should not be reached");
        }
    }
}

If you remember the ChangeState method of the GameManager class calls the CreateState method of GameStateFactory which returns the requested game state entity.

In GameManager folder create a States folder and inside create a new C# script called GameStateEntity.

Include the following code:

using System;
using Zenject;

namespace FSM.GameManager.States
{
    public abstract class GameStateEntity : IInitializable, ITickable, IDisposable
    {
        public virtual void Initialize()
        {
            // optionally overridden
        }

        public virtual void Start()
        {
            // optionally overridden
        }

        public virtual void Tick()
        {
            // optionally overridden
        }

        public virtual void Dispose()
        {
            // optionally overridden
        }        
    }
}

GameStateEntity is abstract class that contains the virtual methods that will be overridden by our state classes. Thus GameStateEntity will play the role of the base class for the all the implemented states.

In our example we decided to implement Iinitializable, Itickable, Idisposable interfaces.

In order to understand how to inherit from the GameStateEntity class, we are going to create our first state. Inside the States folder create a new C# script called MenuState.

Add the following code:

using UnityEngine;
using Zenject;

namespace FSM.GameManager.States
{
    public class MenuState : GameStateEntity
    {
        public override void Initialize()
        {
            Debug.Log("MenuState Initialized");
        }

        public override void Start()
        {
            Debug.Log("MenuState Started");
        }

        public override void Tick()
        {
        }
  
        public override void Dispose()
        {
            Debug.Log("MenuState Disposed");
        }

        public class Factory : Factory<MenuState>
        {
        }
    }
}

We added some debug methods to figure out when the overridden methods are called.

The Initialize method will be called on startup and it’s a good place to put all the initialization logic before the MenuState is created.
When our state is created (by calling ChangeState(GameState.Menu) in GameManager) Start() is the first method that is called.
Use Tick() to perform per frame tasks.
Finally Dispose will be called when we change to a new state, when the scene changes, the app closes, or the composition root object is destroyed. This is useful when you want to clear unused resources or disable redundant functionality.

Similarly we will create our Gameplay state. Create the GameplayState script inside the States folder. The script will look like this:

using UnityEngine;
using Zenject;

namespace FSM.GameManager.States
{
    public class GameplayState : GameStateEntity
    {
        public override void Initialize()
        {
            Debug.Log("GameplayState Initialized");
        }

        public override void Start()
        {
            Debug.Log("GameplayState Started");
        }

        public override void Tick()
        {
        }

        public override void Dispose()
        {
            Debug.Log("GameplayState Disposed");
        }

        public class Factory : Factory<GameplayState>
        {
        }
    }
}

It’s up to you to fill the Start and Tick methods with code that will be responsible managing the actual gameplay.

If you remember the GameState enum inside the GameStateFactory script, we also have two other states’ GameOver and Victory. You can add more states to your game, by expanding the GameState enumerator and the GameStateFactory class.

Now it’s time to create the GameOverState by adding a new a new C# script. In GameOverState script add the following:

using System;
using System.Collections;
using UnityEngine;
using Zenject;
using FSM.Helper;

namespace FSM.GameManager.States
{
    public class GameOverState : GameStateEntity
    {
        readonly AsyncProcessor _asyncProcessor;
        readonly GameManager _gameManager;
        readonly Settings _settings;

        public GameOverState(AsyncProcessor asyncProcessor,
                             GameManager gameManager,
                             Settings settings)
        {
            _asyncProcessor = asyncProcessor;
            _gameManager = gameManager;
            _settings = settings;
        }

        public override void Initialize()
        {
            Debug.Log("GameOverState Initialized");
        }

        public override void Start()
        {
            Debug.Log("GameOverState Started");
            _asyncProcessor.StartCoroutine(ProceedToMenu());
        }

        private IEnumerator ProceedToMenu()
        {
            yield return new WaitForSeconds(_settings.waitingTime);
            _gameManager.ChangeState(GameState.Menu);
        }

        public override void Tick()
        {
        }

        public override void Dispose()
        {
            Debug.Log("GameOverState Disposed");
        }

        [Serializable]
        public class Settings
        {
            public float waitingTime;
        }

        public class Factory : Factory<GameOverState>
        {
        }
    }
}

As you can see we added some new elements to GameOverState, an AsyncProcessor and a Settings class.

Imagine that when you transition to a GameOver screen (or Victory), you want to move back to the Menu screen after an amount of time.
In our example the GameOverState is a normal C# class. Using Zenject, there is less of a need to make every class a MonoBehavior. But there are still occasions that we want to call a StartCoroutine to add asynchronous methods.

One suggested solution is to use a dedicated class and just call StartCoroutine on that instead. AsyncProcessor class will play that role.

In the Scripts folder create a Helper folder. Inside it create a new C# script called AsyncProcessor. You just have to put the following code:

using UnityEngine;

namespace FSM.Helper
{
    public class AsyncProcessor : MonoBehaviour
    {
        // Purposely left empty
    }
}

Alternatively, if you don’t want to use the AsyncProcessor class, you can use UniRx or a coroutine library that implements a similar functionality (Look here for an example).

The Settings class has a waitingTime field, which represents the time to wait before moving to the Menu state. Instead of changing the float value using code or the Unity inspector we can use a ScriptableObjectInstaller, which has the advantage to change these values at runtime and have those changes persist when play mode is stopped.

Before creating our installers, we will add the our final state. In the States folder create the VictoryState script.

Add the following code in VictoryState:

using System;
using System.Collections;
using UnityEngine;
using Zenject;
using FSM.Helper;

namespace FSM.GameManager.States
{
    public class VictoryState : GameStateEntity
    {
        readonly AsyncProcessor _asyncProcessor;
        readonly GameManager _gameManager;
        readonly Settings _settings;

        public VictoryState(AsyncProcessor asyncProcessor,
                            GameManager gameManager,
                            Settings settings)
        {
            _asyncProcessor = asyncProcessor;
            _gameManager = gameManager;
            _settings = settings;
        }

        public override void Initialize()
        {
            Debug.Log("VictoryState Initialized");
        }

        public override void Start()
        {
            Debug.Log("VictoryState Started");
            _asyncProcessor.StartCoroutine(ProceedToMenu());
        }

        private IEnumerator ProceedToMenu()
        {
            yield return new WaitForSeconds(_settings.waitingTime);
            _gameManager.ChangeState(GameState.Menu);
        }

        public override void Tick()
        {
        }

        public override void Dispose()
        {
            Debug.Log("VictoryState Disposed");
        }

        [Serializable]
        public class Settings
        {
            public float waitingTime;
        }

        public class Factory : Factory<VictoryState>
        {
        }
    }
}

If you did all the previous steps correctly you shouldn’t get any errors in Unity console.

We return to the GameInstaller script and fill it with the necessary bindings. In Zenject framework an Installer is a re-usable object, that contains a group of bindings.

The actual dependency mapping is achieved by adding bindings to a container. As a result the container knows how create all the object instances in your application, by recursively resolving all dependencies for a given object.

Add the following code inside the GameInstaller script:

using Zenject;
using FSM.GameManager;
using FSM.GameManager.States;
using FSM.Helper;

namespace FSM.Installers
{
    public class GameInstaller : MonoInstaller<GameInstaller>
    {
        public override void InstallBindings()
        {
            InstallGameManager();
            InstallMisc();
        }

        private void InstallGameManager()
        {
            Container.Bind<GameStateFactory>().AsSingle();

            Container.BindInterfacesAndSelfTo<MenuState>().AsSingle();
            Container.BindInterfacesAndSelfTo<GameplayState>().AsSingle();
            Container.BindInterfacesAndSelfTo<GameOverState>().AsSingle();
            Container.BindInterfacesAndSelfTo<VictoryState>().AsSingle();

            Container.BindFactory<MenuState, MenuState.Factory>().WhenInjectedInto<GameStateFactory>();
            Container.BindFactory<GameplayState, GameplayState.Factory>().WhenInjectedInto<GameStateFactory>();
            Container.BindFactory<GameOverState, GameOverState.Factory>().WhenInjectedInto<GameStateFactory>();
            Container.BindFactory<VictoryState, VictoryState.Factory>().WhenInjectedInto<GameStateFactory>();
        }

        private void InstallMisc()
        {
            Container.Bind<AsyncProcessor>().FromNewComponentOnNewGameObject().AsSingle();
        }
    }
}

To add bindings to an installer we have to override the InstallBindings method, which is called by whatever Context the installer has been added (in our example the SceneContext).

Let’s see the first statement of the InstallGameManager() method.

By calling:

Container.Bind<GameStateFactory>().AsSingle(); 

we say that any class that take GameStateFactory as input will receive the same instance of the GameStateFactory.

In the next four lines we bind all the interfaces to the classes and the classes themselves by calling the BindInterfacesAndSelfTo shortcut.

Therefore

Container.BindInterfacesAndSelfTo<MenuState>().AsSingle();

is equivalent to:

Container.Bind<IInitializable>().To<MenuState>().AsSingle();
Container.Bind<ITickable>().To<MenuState>().AsSingle();
Container.Bind<IDisposable>().To<MenuState>().AsSingle();
Container.Bind<MenuState>().AsSingle();

In

 
Container.Bind<ITickable>().To<MenuState>().AsSingle();

binding the Itickable interface will result Tick() being called like Update().

Finally in

 
Container.Bind<MenuState>().AsSingle();

it means that the Container will only ever instantiate one instance of the type MenuState type.

So it is recommended to use BindInterfacesAndSelfTo and BindInterfacesTo in order to group interface bindings and increase the code readability.

In the final four statements we bind all the all the factory dependencies. For example by using VictoryState.Factory all the dependencies for the VictoryState (like GameManager) will be filled in automatically.

In InstallMisc method we add the binding for the AsyncProcessor. The FromNewComponentOnNewGameObject is a construction method that creates a new empty gameObject and instantiates a new component of the given type (in our case an AsyncProcessor).

So when we transition to GameOverState (or VictoryState) for the first time, Zenject will create a new gameObject with an AsyncProcessor component, in order to be used by GameOverState (or VictoryState). AsSingle says that every class that requires a dependency of type AsyncProcessor (like GameOverState), will be given the same instance of type AsyncProcessor.

For our settings we will create a Scriptable Object Installer. Inside the Installers folder right click and select Create → Zenject → Scriptable Object Installer and name it GameSettingsInstaller. Inside the GameSettingsInstaller put the following code:

using System;
using UnityEngine;
using Zenject;
using FSM.GameManager.States;

namespace FSM.Installers
{
    [CreateAssetMenu(fileName = "GameSettingsInstaller", menuName = "Installers/GameSettingsInstaller")]
    public class GameSettingsInstaller : ScriptableObjectInstaller<GameSettingsInstaller>
    {
        public GameStateSettings gameState;

        [Serializable]
        public class GameStateSettings
        {
            public GameOverState.Settings gameOverState;
            public VictoryState.Settings victoryState;
        }

        public override void InstallBindings()
        {
            Container.BindInstance(gameState.gameOverState);
            Container.BindInstance(gameState.victoryState);
        }
    }
}

With the above code we can create a scriptable object installer and Zenject will bind the instances to the required types.

Right click in the Installers folder and from the newly added menu select Create → Installers → GameSettingsInstaller.
Select the created GameSettingsInstaller.asset file and to the Inspector, put the value 2 for the Game Over State waiting time and 5 for the Victory State waiting time.

The last thing to do is add the GameSettingsInstaller scriptable object in our scene context.
Select the SceneContext gameObject and in the Scriptable Object Installer press the + button to add a new row on the list.
Drag and drop GameSettingsInstaller into that slot.

We are going to add some UI elements in our scene in order to test our GameManager functionality.

In the Hierarchy create a new gameObject called GameManager and add a GameManager component to it.
We will try to validate our scene by selecting Edit → Zenject → Validate Current Scene or simply pressing CTRL+SHIFT+V. What validation does is that it executes all installers for the current scene and iterate though the the object graphs and verify that all bindings can be found.

If we try to do this we will get the following error in Unity console:

ZenjectException: Unable to resolve type ‘FSM.GameManager.GameManager’ while building object with type ‘FSM.GameManager.States.GameOverState’. Object graph:

This means that GameManager MonoBehavior is not added to the Zenject Container, thus can’t be injected into other classes (like GameOverState or VictoryState).

One way to handle the Scene Bindings is by adding the ZenjectBinding component in the GameManager gameObject. In the Components array increase it’s size to 1 and drag and drop the GameManager Script. Then the GameManager script will look likes this:

Clear the Unity console messages and try to validate your scene again (by pressing CTRL+SHIFT+V). If everything will go as planned you will get the following message:

In the Hierarchy add a UI → Text and name it StateText. Clear it’s text value and place it in the following rect transform position if you like:

Drag and drop the StateText gameObject into the State Text field of our GameManager:

Add a UI Button, name it MenuButton and change the text value in it’s text component to Menu. You can place the MenuButton here:

With the same process add another UI Button, name it GameplayButton and change the text value in it’s text component to Gameplay. You can place the GameplayButton here:

Add another Button, name it GameOverButton and change the text value in it’s text component to GameOver. You can place the GameOverButton here:

Finally add a Button, name it VictoryButton and change the text value in it’s text component to Victory. You can place the VictoryButton here:

We will add a new Monobehavior to control the buttons behavior. Under the scripts folder create a UI folder, and inside a new C# script called StateChangeManager.
Add the following code in StateChangeManager:

using UnityEngine;
using Zenject;
using FSM.GameManager;


namespace UI
{
    public class StateChangeManager : MonoBehaviour
    {
        [Inject]
        readonly GameManager _gameManager;

        public void ChangeToMenu()
        {
            _gameManager.ChangeState(GameState.Menu);
        }

        public void ChangeToGameplay()
        {
            _gameManager.ChangeState(GameState.Gameplay);
        }

        public void ChangeToGameOver()
        {
            _gameManager.ChangeState(GameState.GameOver);
        }

        public void ChangeToVictory()
        {
            _gameManager.ChangeState(GameState.Victory);
        }
    }
}

Add the StateChangeManager script in the Canvas gameObject.

Now select the MenuButton gameObject and inside the Button component add a new item in the On Click() List. Select the Canvas game object and on the function list choose StateChangeManager → ChangeToMenu.

Repeat the same process to the remaining buttons.
In GameplayButton select StateChangeManager → ChangeToGameplay.
In GameOverButton select StateChangeManager → ChangeToGameOver.
In VictoryButton select StateChangeManager → ChangeToVictory.

Run the scene and press the buttons to change states.

By pressing the GameOver or the Victory buttons, we return to the Menu state after the time specified in GameSettingsInstaller scriptable object.

Now all you have to do is to extend the functionality of the FSM to cover your game requirements. Moreover it’s recommended to study Zenject documentation and experiment with the given sample games. Zenject framework has a learning curve, thus will take some time to understand and apply all the concepts.

You can download the complete project from here.

Resources:

1) State pattern
2) Game Programming Patterns – State
3) Finite-state machine
4) Game programming patterns in Unity with C# – State pattern
5) Unity download archive
6) Zenject – Dependency Injection Framework for Unity3D