Unity’s Animator Controller as a State Machine
This is a collaborative post, written by Andrej Petelin and myself.
After reading about the use of Animator Controller state machines as a general purpose state machine in Unity AI Game Programming, Second Edition by Ray Barrera, Aung Sithu Kyaw, Clifford Peters and Thet Naing Swe, we decided to try out the concept and used it to control the game states.
We read about this at roughly the same time as Colton Ogden was dealing precisely with the topic of State Machines in cs50’s Introduction to Game Development course. The topic for that week was the game Breakout, using Lua/Love2d for development. So we decided it would be a good idea to implement it in Unity and try the Animator Controller as a State Machine.
You can find the whole project here. The idea of this post is mostly to discuss advantages and disadvantages of Unity’s Animator Controller as a State Machine. Nevertheless, any questions you may have about the implementation or the project are welcome. Just ask here and we will answer the best we can. All graphics, sounds and music are from the project Colton shared on GitHub for the course.
The Setup
First of all, for those who are not familiar with the Animator Controller at all, we would recommend you watch, at least this tutorial by Unity themselves, in which some basic concepts are explained. Also, you should read this other tutorial, also by Unity, which deals with StateMachineBehaviours, especially everything related to the methods they use, how StateMachineBehaviours differ from MonoBehaviours, and how they communicate with each other.
The setup itself was quite simple. We basically took the state machine graph as Colton showed it in the lecture, and started laying it out in Unity’s Animator Controller. As pretty much everything with Unity, it’s extremely easy to do it. We decided to use a Persistent Scene, and load and unload all scenes from it, and attached the Controller to a GameObject in that scene.
Once the different states are laid out, all we had to do was decide what transitions between states to have, and what values to use to trigger those transitions. That is also quite simple to do. Most of the transitions that would be triggered by user input were set up as triggers directly from the SceneController, that would directly handle the input. Basically, all you need to do is something like:
if (InputHandle.Enter) {
anim.SetTrigger(“EnterPressed”);
}
InputHandle is our own class, where we handle all input, and anim is the reference to the Animator Controller. The important part here is that the trigger that we set is simply passed to the State Machine, and we let it take care of going from the correct state to the next correct state itself. The “enter” key is pressed in different states, but because of the way the State Machine is set up, you don’t need to check what state to enter yourself.
Other values will be handled from different classes, but the logic is the same: all you need to do is set up the value correctly, and let the Animator Controller take care of what to do. If, for example, the player has 3 lives to start with, that will be the default value that the “lives” parameter will have as the game begins. Then, every time the ball falls, you only need to decrease that value, and the State Machine will take care of transitioning to the “GameOver” state when lives == 0.
Now, transitioning through states is nice, but what we wanted to explore the most was using the StateMachineBehaviour and its methods in order to control what had to be done in each case. For the most part, we used the OnStateEnter() and OnStateExit() methods. Because StateMachineBehaviours can easily use inheritance, we created a base class to easily establish communication with our SceneController, which is the class that has all the methods and coroutines to load and unload scenes. This makes it quite simple to load and unload the correct scenes, without having to rely on if / else blocks to check what state we are currently in. We also used it to show and hide different UI elements as we transitioned through different states.
How much or how little is ideal to put in the StateMachineBehaviour classes is still debatable for us. Clearly, handling transitions between scenes and UI elements is a no-brainer. It also seems like the ideal way to do anything that is closely related to animations, like the particles example in the tutorial linked above.
The Good
There are quite a few advantages of using the Animator Controller as a State Machine:
- Having a clear graphic layout of the states gives you a huge advantage. It helps think about the way the game will be structured and, because it’s very easy to edit, it makes it better than simply drawing the states on pen and paper. It helps think and decide on what the transitions will depend on, and it’s also easy to scale: Adding or removing states is not only simple in the Animator Controller, but it doesn’t require refactoring a lot of code (we actually added a couple of states while working on it).
- Having the State Machine handle transitions makes the code less bug prone: We avoid having the horrible long series of “if / else if” sequences, there is no need for checking what state we’re in, what value a certain variable has, what state our bools are in, etc.
- But not only that: The Animator Controller shows all the information we need while playing the game: What state we’re in, when the transitions happen, and what values the different parameters are holding at any point in time. That makes it extremely easy to debug the code in case something goes wrong. You don’t need to write to the console to find out whether a function is not getting called because you never went into the state that calls it or for some other reason: you can see it on your screen. You miscalculated a value? You can see it there. This is invaluable for debugging.
- This may be a minor one, but you can also change the parameters while you’re playing. You’re running out of lives before managing to get to a certain point in the game? You can add more lives right there in the editor, as you’re playing. You want to see what would happen if a certain trigger was set at some point? You just click it.
The Bad
There are a few caveats, though, the most important that we found were:
- If a trigger falls in the forest…..
Seems to make a lot of sense now that we know it, but this one caught us off guard. Triggers are going to remain in their set state until they trigger some transition. So if you have, like we showed above, a trigger that is set when the user hits “enter”, the trigger will remain in the “on” state if you’re currently in a state that doesn’t use it at all. This can cause weird behaviour, of course, because eventually you will enter a state that will use it, and it will trigger an undesired behaviour.
Especially if you have triggers that depend on user input, what we would recommend is to reset all triggers when states are entered. It isn’t the prettiest solution, but it’s safe, particularly because you can never tell when the user will hit a key by mistake. - This one is a bit weirder. The automatically created comments for StateMachineBehaviours clearly state that OnStateExit() “is called when a transition ends and the state machine finishes evaluating this state”. This is not so clear in the documentation, to be honest. What this means, in practical terms is that the next state’s OnStateEnter() may be called before the current state’s OnStateExit() does. Which can be a problem if you have some data which you can only retrieve when the transition gets triggered. This happened to us while setting up the Highscores routine. If you remember Breakout (as most classic 80s games), has this 3 initial system for entering your name when you reached a high score. The only time in which we could retrieve the initials is when the user is done inputting them and presses “enter”, which triggers the next state. This makes OnStateExit() the ideal place to retrieve them: The user will hit “enter”, we get the initials from the UI. The problem was they were not being displayed when the next state was entered because of the order in which things happen. Because all StateMachineBehaviours (unlike MonoBehaviours) get instantiated at the same time, when the AnimatorController does, we worked around it by getting a reference to the next Behaviour and calling a function from it that would write the correct initials to the UI.
Also, not too pretty. The worst part is that we couldn’t find any definite answer as to whether this behaviour is constant or if in some cases OnStateExit() may be called before the next OnStateEnter().
The Ugly
Looking back at the code, there are a lot of things we would have done differently. We were experimenting as we moved along, and the code is not to be taken as an example of brilliant design, just as a show of different things that can be done.
The Verdict
For handling “big picture” state, we think this is pretty much ideal. Managing scenes, UI elements, keeping track of when to move to the next level or when to transition to a game over state, for those things the State Machine is a powerful tool.
Of course, Breakout is a game that doesn’t have much state during gameplay. For more complex games, we are still undecided. Certainly, if you have characters with animations, you need to use the Animator Controller, and the transitions are handled perfectly. We aren’t too sure how using the StateMachineBehaviours will scale in that case. We will try and will let you know.
Last, but certainly not least
A big shout out to Colton Ogden and the whole team at CS50 for an amazing and fun course. We’re enjoying it a lot, you should all check it out!