Finite State Machines
This week’s entry is about Finite State Machines. It quickly became clear to me while implementing the Enemy Scout Ship logic that standard if/then/else code was too fragile and not scalable. Even simple AI agent rules broke down and became hard to debug.
Enter the Finite State Machine.
Mat Buckland’s book Programming Game AI by Example is an excellent resource for learning about AI techniques in games. It covers techniques such as state- and goal-based behavior, steering behaviors, goal evaluation and arbitration, and fuzzy logic. The examples are in C++, but I’ve included the state machine design I’m using here in C#.
As Mat points out in his book, finite state machines have been around in game development for a long time. The reasons are pretty simple -
They are simple to code
They are easy to debug
They is very little runtime overhead
They make sense to humans (it’s how humans tend to think)
They are flexible
A finite state machine allows an autonomous object (or agent) to perform relatively complex actions by breaking those actions down into very small pieces. It allows the programmer to factor out the behavior code into discrete chunks that make it very easy to understand and debug. As the name implies, a finite state machine only allows an agent to exist in a single state at any given time. A state is defined as having an entry point, an exit point, and a point where the code executes the logic for the agent when the agent is in the state. For this, I used a generic interface in C# -
IState interface
T represents the owner that the state belongs to. The design of the finite state machine itself is pretty straightforward -
Finite state machine class
The state machine needs to be instantiated by the owner and the Configure method needs to be called at some initialization point (in Unity this is typically either Start or Awake).
The state machine supports the concept of a Global state. The code in a global state would be behavior that you want the agent to perform all of the time. In the case of Star Jet Alpha, the Enemy Scout Ship is always looking for the player. For this, I have a state called ScoutShipRadarMonitorState -
ScoutShipRadarMonitorState used as a global state
The code in Execute basically looks for the player with a rudimentary distance check (I’m not using colliders in this case). If the player is found, a call to the owner is made to shutdown the radar. This is implemented in the owner by setting the global state to null. After that, a call is made to tell the owner it’s time to return to base. That behavior is implemented by changing the current state so that the scout ship changes its current destination so it can head home. No check is made to see if the scout ship is currently on patrol or heading home already - the ChangeState method in the state machine checks to see if the new state is the same as the current state. If it is, it does nothing.
This functionality is fairly straightforward and encapsulated. The owning class only has to provide functionality to instantiate the state machine and a state for each of the behaviors it needs to implement. The logic for how to manage the transition between states is handled in each individual state class.
If you have questions about finite state machines or other comments, please leave a comment below!