Don’t Re-invent Finite State Machines: How to Repurpose Unity’s Animator
Finite state machines are one of the most popular patterns in game development for good reason. They can reduce complexity and create more readable programs by encouraging modular, re-usable states. While FSMs (finite state machines) fail to scale to more complex situations, they’re excellent solutions for most common-place situations like dealing with application state or making simple AI.*
There are two main approaches for creating and using FSMs: writing a quick and dirty implementation or buying an asset from the asset store to use. But there is a third option: using Unity’s built-in animator system (Mecanim).
Why would you use Unity’s animation system?
It can seem a bit weird to reuse the animation system to create finite state machines, but the animator is a finite state machine. While it has animation specific features that can get in the way, the animator system does almost everything you’d want. It comes with tools for editing and visualizing the state machine during run-time. Also, it’s supported by Unity, tested by millions of developers, and any Unity animation experience you already have is applicable.
Why not build a quick and dirty implementation instead?
A quick and dirty implementation works well for the task that it’s designed for, but once you need to use this pattern elsewhere, you’ll find yourself re-writing the same code over and over again. The animator system can easily create different state machines with re-usable state behaviors.
Then how about using a third-party asset?
There are third-party assets that rival the animator system in complexity and power (like NodeCanvas). They also have the advantage of a cleaner API because they don’t deal with animations. I’ve even built my own implementation that I eventually stopped using in favor of re-repurposing the animator. Because you’re going to using the animator to, well, animate, re-using it for state-machines means you don’t have to switch contexts between two similar systems.
Okay, how do I start?
First, I’ll cover the basic concepts of Unity’s Animator system (Mecanim).
- In Unity, you can create an asset called an Animator Controller. This is a state machine template.
- There are states inside your state machine. You’re probably used to associating states with animations, but they’re actually optional if you want to create a pure-logic state machine.
- To run your state machine, add a component called Animator to a GameObject and set it up with any Animator Controller that you’ve created. This is now an instance of your state machine.
- To run logic on each state, we’ll need to create a script derived from the class StateMachineBehaviour. Once we have this new behavior, we can add it to any state inside the state machine.**
**Note that this works similarly to MonoBehaviours: the script receives pre-defined messages, like OnEnter, OnExit, etc.
How do you use StateMachineBehaviours?
I most commonly use states to manage the lifecycle of objects and their own behavior, using the OnStateEnter and OnStateExit messages when inheriting from StateMachineBehaviour. For example, if I want my state to listen to any game events, I usually add listeners on enter and remove them on exit. By cleaning up objects on exit, you avoid hard-to-trace after-effects like left-over game objects or zombie listeners. You don’t want your state to be affecting the game when it’s not active!
One problem that I’ve found is that OnStateExit is not called when the Animator is disabled or destroyed. So, to clean up the current state properly, you’ll need to make sure to handle OnDisable as well***.
***To save myself time, I’ve created a class called LogicalStateMachineBehaviour (link) which I inherit from instead of StateMachineBehaviour. Feel free to use it in your own projects!
Let’s walk through a simple state machine that I’ve created for an enemy in my own game, Jellyquest.
This is the angry pufferfish. He’ll rotate around slowly until he spots the player, and then he’ll propel himself rapidly in the player’s direction.
The pufferfish is a prefab configured with an AnimatorController named AngryPufferfish. There are 3 states: Aiming, PreparingToShoot, and Shooting. The pufferfish starts off in the Aiming state, which is composed of two state behaviors: RotateFacingDirection and AimInFacingDirection.
RotateFacingDirection rotates the pufferfish based on a configurable speed. AimInFacingDirection figures out if the pufferfish is facing a target based on a raycast.
In the PreparingToShoot state, I re-use a state behavior named TriggerContinueAfterDelay with a parameter for how long the delay is.
And in the Shooting state, I use a MoveInFacingDirection state behavior to move the pufferfish towards the player.
By making each of these behaviors generic and single-purpose, I can tweak and re-use them in other state machines to create a variety of different enemy types.
Awesome, anything else I should know?
There are some common pitfalls that you should be careful of. These make more sense the more familiar you are with the animator system.
- Be weary of writing code that can set a trigger many times in the same frame. Because triggers are only consumed once by a transition, the trigger might get set after being consumed and persist past the current state.
- OnStateEnter and OnStateExit on sub-state machines probably do NOT work as you’d expect. They are only called when hitting the entry and exit nodes of the sub-state machine. But those nodes can be bypassed accidentally by making transitions that directly go into to a specific sub-state or out to an external state.
- Transition duration should probably always be set to zero. If transition duration is non-zero, the next state’s OnEnter will be called before the current state’s OnExit.
- Transitions using triggers will advance to the next state in the same frame. Transitions based on other parameter types take an extra frame.
- Finite state machines are useful to manage life-cycles of objects
- People either write their own state machines in code or buy a 3rd party asset from the asset store. But you can also repurpose the Unity Animator.
- The Animator comes with built-in visualization, tooling, transitions, and a familiar api.
- Using the Animator is awesome, but be careful of some pitfalls, get more details above!
Interested in reducing your compile time? Or maybe automatically validating your game to prevent broken builds? Check out my other articles!