Designing finite state machines requires good discipline and a shift in design paradigm. It’s very easy to “break” a state machine by implementing improper design. It is very tempting to take shortcuts, but it never leads to clean lasting code. In this post, I want to examine (rant on) some tempting pitfalls.
The basic premise for an FSM is: each state updates and states run one at a time, each state has enter and exit logic and has one or more switch conditions to different state(s).
The examples are very simple, just a state enum and a state variable. But the associated issues could equally appear in a similar form in a more complex implementation. Even simple FSM frameworks cannot prevent abuse without unnecessarily restrictive or cumbersome design.
Conditions in State Updates
One of the biggest grievances of state update methods is when they grow new conditions in them:
1 2 3 4 |
private void UpdateSomeState() { DoSomeStateStuff(); } |
becomes
1 2 3 4 5 6 7 |
private void UpdateSomeState() { if (someExtraFlag) DoSomeSpecialStateStuff(); else DoSomeStateStuff(); } |
This is another state. There should never be branching conditions in a state machine that toggle the state’s actions. If state’s actions are conditional, then those are–by definition–different states. This can also appear as simply as:
Pausable States
Let’s say some state can be suspended:
1 2 3 4 5 |
private void UpdateSomeState() { if (!someStatePauseFlag) DoSomeStateStuff(); } |
Again, that’s another state. There is no such thing as “pausing” a state. A paused state is a different state whose logic is to wait until and resume the original state. Pausing a state is the same as adding a condition where one branch is “do nothing”.
External State Changes
While writing a state machine that reacts to external conditions, it’s tempting to simply tell the state machine to change. Even if this method is part of the state machine and the actual state and value are not exposed:
1 2 3 4 |
public void SwitchToExiting() { state = State.Exiting; } |
Firstly, this is still external from state machine’s point of view. This bypasses any state switch conditions or logic and introduces a second mechanism to switch states. What the proper state-respectful and error-tolerant way to do this is to set the “desired state” flag:
1 2 3 4 |
public void SwitchToExiting() { desiredState = State.Exiting; } |
and somewhere later, when one or more state switch are considered, check this flag in addition to any normal conditions (and reset it when the state is finally switched):
1 2 3 4 |
public bool ShouldSwitchIdleToExiting() { return currentTime > expiryTime || desiredState == State.Exiting; } |
The state value should always be read-only to anyone but the state machine’s state switch code. In other words, there would be only one place in code that assigns the state value.
Manual State Update Code
Each state should run at and only at its designated update time. For a simple example, the essential logic is this:
1 2 3 4 5 6 |
switch (state) { case State.Showing: UpdateShowingState(); break; case State.Idling: UpdateIdlingState(); break; case State.Hiding: UpdateHidingState(); break; } |
It is possible to write logic for the states while completely ignoring any state logic separation:
1 2 3 4 5 6 7 8 9 10 |
if (state == State.Showing || state == State.Hiding) sprite.opacity = currentTime / totalTime; if (state == State.Showing) currentTime += deltaTime; else if (state == State.Hiding) currentTime == deltaTime; if (state != State.Hiding) label.opacity = 1f; |
In small doses, this may even make sense (formatted nicely, it looks like good code). But this really just throws the state design out the window. This code cannot be refactored. The whole point of states are to be independent.
Reusing State Values
The simplest state machine is just an enum:
1 2 |
private enum State { Fade, Blink, Display } private State state; |
Since it’s an enum, you could manually assign underlying values to it:
1 2 |
private enum State { Fade = 0, Blink = 127, Display = 255 } private State state; |
And then actually use them for other purposes:
1 |
sprite.opacity = (float)state / 255f; |
This is breaking single responsibility principle. This is bad because neither the states nor the values can be refactored without affecting the other usage. This is also very forced. Variations include things like label[(int)state].Show() or animator.state = (int)state;.
Enumerated States
States can require additional persistent data. It could in some cases be encoded in the states themselves:
1 2 |
private enum State { Basement, Floor1, Floor2, Floor3, Floor4, Floor5, Attic } private State state; |
This has 5 “Floor” states that do exactly the same logic, but for different floor numbers. This mainly breaks “don’t repeat yourself” principle and makes adding new states of changing numbering cumbersome. This should likely be closer to:
1 2 3 |
private enum floor; private enum State { Basement, Floor, Attic } private State state; |
It’s still a bit ugly. Depending on how it’s used and the way the state machine is implemented, there are many ways such data can be stored (that I won’t go into).
Modifying State Data
States can have data and–unless we are using a fancy encapsulated framework–this data could likely be just another variable:
1 2 3 |
private enum State { Idle, Spinup, Firing, Cooldown } private State state; private int rocketsLeft; |
It’s important to not touch this variable outside the designated state (and rarely multiple states). It might be tempting to do something like:
1 2 3 4 |
public void EnableRocketBonus() { rocketsLeft += rocketsFromBonus; } |
This makes assumption about the current state and state specifics. It should be the state that handles this at the appropriate time (if necessary, there should be a separate variable for “next bonus”):
1 2 3 4 5 6 7 8 9 |
public void EnableRocketBonus() { rocketsFromBonus = extraRocketBonus; } private void OnEnterFiringState() { rocketsLeft = rocketCapacity + rocketsFromBonus; } |
Unknown Initial State
I haven’t added an initial state, such as “None”, to my examples above. In a real system, that can be a dangerous assumption if the system has persistent data and could halt and restart unexpectedly. Consider a forklift:
1 |
private enum State { None, Lifting, Idling, Lowering } |
We may expect the lift to always lower when it is powered off, but what if the power cuts off? In other words, the state machine has to know which state to enter first. It might be tempting to simply add some initial conditions:
1 2 3 4 5 6 |
if (position > 0) state = State.Lowering; else if (position < 0) state = State.Lifting; else state = State.Idle; |
Besides assigning state directly, this is also making assumptions about how a state switches/starts. The code is now duplicated for state entry and awaiting many hours of debugging.
Final Thoughts
A lot of issues can be solved simply by using an existing framework. Most frameworks would enforce proper design. Unfortunately, more complex frameworks also have downsides, such as learning curve, reduced performance and increased memory usage or many new classes. Many of these downsides come into play when the application has dozens if not hundreds of state machines. I often find myself needing a quick state machine in a single class and making half a dozen extra classes for a full-blown state machine is hard to justify. So I would use a simple state machine pattern, but one open to abuse without discipline.
State machines are alive and kicking. As with any design pattern, knowing its strengths and weaknesses let’s you choose and use it appropriately. When used correctly, it is a powerful tool that can divide complex timed systems into manageable chunks. FSM is still one of my favorite design patterns.
Good analysis on state machines. I still tend to use the classic State Pattern (Gang of Four) for more heavy-duty state machines, but a low-maintenance implementation can definitely help organize complex logic.