A Matter of States: General Overview of a FSM

Ben Mercier
7 min readJul 23, 2021

Matter can exist in any one particular form called a state. In our everyday lives, water is probably the most familiar representation of this with each of its three states being ice (solid), water (liquid), and vapor (gas). Each of these states have their own defined characteristics, and in order to transition from one to another, certain conditions must be met. These conditions are influenced by outside forces (e.g. temperature) and may also limit which state matter can transition to from its current state. For example, liquid water will transition into ice if the temperature decreases to 32°F or below, but it can become water vapor if the temperature increases to 212°F or above.

By ElfQrin — Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=15779425

These same principles can be applied to programs via the use of a finite state machine (FSM). A FSM is a design pattern wherein the object or character can only be in one of a finite number of states at a time. Just like with states of matter, any one active state is assigned in response to some input or condition being met (e.g. pressing X to jump).

In a game, three simple actions a player may be performing at any one particular time may be idling/standing, walking, or jumping. The player can only be performing one of these actions at a time, and if, for example, he or she wanted to go from walking to jumping, then the jump input would need to be activated. While jumping, the player would no longer be walking and certainly wouldn’t be idle. Although these three actions are simple enough to combine together, the code can quickly grow into a tangled mess of if()…else if() statements as new features are added and new checks are required.

Abstract Classes

To begin building a FSM, it’s important to first have a general understanding of abstract classes and class inheritance. Abstract classes are classes which can’t be instantiated and can’t be added to game objects so their purpose is to act as a common base class or template for any derived classes to implement.

Unity wants you to know this can’t be added to a game object.

Making a class abstract forces inheritance on any derived classes, and unlike interfaces, classes can only inherit from one abstract class. Like regular classes, abstract classes are able to declare and use any variables, those variables are private by default but may be public, and they can contain non-abstract methods. Using abstract methods within an abstract class would require any derived class to implement those methods while any virtual methods could be shared across those classes. For example, there can be a base Animal class which has methods for walking, Walk(), and speaking, Speak(), as shown below:

Both the Dog and Cat need to walk and speak, and while they may have a similar walking behavior that can be shared, the Dog barks while the Cat meows. They will each still be Animals, just with varying method implementations. To force them to Walk() and Speak(), the Animal class can be made abstract. Walk() can then be made into a virtual method since that behavior is similar whereas Speak() will need to be abstract in order to force the Dog and Cat to implement their own Speak() methods.

To make a class abstract, you simply add the “abstract” keyword before “class” so it would become “public abstract class Animal : MonoBehaviour { }”. The Dog and Cat can then inherit from Animal allowing the methods to be accessed and overridden. Note that with Animal inheriting from MonoBehaviour, Dog and Cat will also be MonoBehaviours. Furthermore, since the Animal class is abstract, Cat will have an error until it implements the Speak() method like Dog.

Finite State Machines

A FSM can be built in a similar way as the Animal, Dog, and Cat classes using the previously mentioned idle, walking, and jumping states. For this example, the FSM will be divided into the following parts:

  • An abstract BaseState class to act as a template for each individual state, but this will NOT inherit from MonoBehaviour.
  • Separate IdlingState, MovingState, and JumpingState classes each inheriting from BaseState, and each needing the ability to transition to either of the other two (this won’t be the case for every state).
  • If idle, the player can move or jump,
  • If moving, the player can stop and become idle or jump, and
  • If jumping, the player can land and become idle or continue moving.
  • A PlayerControllerFSM class attached to the player game object with references to each state

Like with the Animal class, the abstract BaseState class will serve as a common template for each of the derived state classes. Within this class there are three abstract methods for entering, exiting, and updating each state that will be forced on any inheriting class. There is also a stored reference to the player controller script and a virtual AssignPlayer() method with a PlayerControllerFSM parameter to assign the player controller during the first state transition.

Now, when creating each state class and inheriting from BaseState, the EnterState(), ExitState(), and Update() methods will be required, and each state will have access to the stored player controller through the property or protected _player.

Back in the PlayerControllerFSM script, references can be made to the BaseState as well as each of the Idling, Moving, and Jumping state classes. These inheriting states can be made “public readonly” since they will only be used to set the value of the current state.

Next, a public TransitionToState() method will be created within the PlayerControllerFSM class that takes a BaseState parameter. This method will be used within each state class to transition from one state to the next, and is composed of four parts:

  1. First, a check to determine if there is a current active state. If there is, then the ExitState() method for the current state is called.
  2. Second, the current state is set equal to the BaseState parameter that was passed into the method.
  3. Third, a check to determine if the player reference for the states has been assigned. If not, the AssignPlayer() method is called on the current state with the PlayerControllerFSM being passed in as the parameter. This should only be called during the first state transition, but also serves as insurance in case the reference is lost.
  4. Finally, the EnterState() method is called on the current state.

Within the Start() method for the PlayerControllerFSM, TransitionToState() is called with the IdlingState being passed in as parameter to signify that the idling state will be the default state for the player. This may be the only time this method is called within the player script as every other state transition can occur in the state classes since they share a reference to the player and the transition method is public.

Now, with there being references to each state, a shared method to transition between them, and a means to start in a designated default state, the current state’s Update() method can be called in the PlayerControllerFSM’s Update() method so that whichever code is designated for that particular state can run each frame.

Although tedious to set up, organizing behaviors this way makes it vastly easier to add, remove, and debug features because the classes are more modular and the functions within one aren’t reliant on the functions within another (OOP points!).

Outside of player states, this design pattern also works really well when dealing with any enemy AI behaviors such as wandering, pursuing, or fleeing. Their wandering behavior can be fully maintained within the WanderingState, and then depending on whichever gameobject entered their collider, they could transition to the PursuingState and pursue that object. If their health decreased or if another condition was met, they could then transition to the FleeingState to try and avoid destruction.

Hopefully this provided a little bit of useful insight into the power of using a FSM, and helped kickstart some ideas on how to implement one in your own project.

Cheers!

--

--

Ben Mercier

I’m an emergent software developer with a longtime love of games and a growing understanding of how to actually build them.