Using The State Pattern To Simplify Your Game States
Patrick T Coakley 13 min read November 07, 2024 Godot, Game Development, Design Patterns[ #godot #gamedev #csharp #object-oriented #design patterns ]
One of the hardest things to do when developing games is to handle different states. The state of a game can generally be described as condition or status of one or more entities in a game. So in the case of a player character in a 2D platformer, the player state might be something like standing, ducking, jumping, etc. There are a few ways to handle managing different states, so in this tutorial I wanted to take a progressive look at how many folks handle basic state management and gradually move towards using more abstractions that make it easier to maintain and extend your code.
This content is somewhat influenced by the State chapter of Game Programming Patterns, an amazing book that I highly recommend buying a copy of if you haven't checked it out already. I picked up a print copy some years back and still flip through it from time to time, and it's a great way to learn common design patterns and apply them to real examples in games, as opposed to some of the more abstract examples most books on design patterns use. If you want to learn more about design patterns, then definitely check out the original "Gang of Four" book, Design Patterns.
One of the reasons I wanted to write this tutorial was to have a concrete implementation of some of the ideas from the book using Godot, as I feel it helps a lot to have something in full to see and understand. To that end, there is a lot of code in here, and while I will attempt to give context to it all, my goal is to have it make sense just by reading it and the comments; this is not a tutorial for absolute beginners to programming, but it should also be understandable if you've ever tried to make a game before or programmed a little bit in an object-oriented programming language. If there are areas that could be improved or expanded upon, please feel free to reach out using any of the methods on the site; my goal is to try and update content on this site over time using feedback.
The full project can be found here, and while I'll do my best to sync everything up when changes occur, the repo will likely be the most up-to-date.
A Basic Approach
One of the most common and simplest approaches to handling state in a game is to just use a boolean for each state. In this way, we can decide if something simply is or is not in a specific state and just use these to check when we push another button or move in a direction. It's an easy way to represent different states, and you can get started without plan too much.
Here is an example of using this approach:
using ;
;
/// <summary>
/// The following is intentionally messy code that is meant to represent a typical example of a beginner-level state
/// management script, with the idea that it will further be refactored multiple times to introduce ways to organize
/// the code better. One of the most common ways to handle state is to use "flags" (booleans) to get and set different
/// states, which can work fine enough depending on the scope, but it can quickly become hard to maintain and extend
/// over time.
/// To keep this example simple, the Player can do the following:
/// - Stand -> Duck or Jump
/// - Duck -> Stand
/// - Move -> Jump or Duck
/// - Jump -> Dive or Stand
/// The main difficulty with using flags is that the more you add, the more places you have to check and (re)set them.
/// </summary>
public partial
What we have here is something that is functional but messy, with the possibility of having unexpected behavior because we didn't fully consider every single edge case we would need to check. There are slight alternatives to using booleans, such as bit fields[0], but this is a very standard way folks start out handling their player state. Some of this behavior could be refactored out a little bit, but the core problem with using flags boils down to the complexity of maintaining them. This comes into play when you want to add another state, especially if that state is similar to how the diving is implemented where we have to handle it in multiple places.
Finite State Machines
Thankfully, the easiest way to tame this complexity is to use a finite state machine (FSM). A finite state machine is essentially an abstraction that consists of a finite number of states and transitions between those states:
The idea is that you can only move from one state to another, so there isn't a possibility to be stuck in more than one state. This simplifies the game logic because we don't have to concern ourselves with the idea that you might have accidentally set a flag improperly or that you forgot to add an animation somewhere else: there is only one possible state you can be in at one time. FSMs are not a silver bullet, and they have their own limitations, but they provide a very useful and simple way to organize states in not only games, but all kinds of software. Anything from traffic lights to vending machines can be represented in finite state machines, and they are one of the most important abstractions in software development.
The simplest way to implement an FSM to represent state is to just use enums and have a single property to maintain the state:
using ;
;
/// <summary>
/// Building off of the previous example,`PlayerBasic.cs`, this implementation
/// handles state by using enums and consists of a primitive finite state machine.
/// This change alone can make a huge impact on organizing your state code together without having to do large if/else
/// statements, with the added benefit of also making much easier to understand the transitions
/// between each state. The primary downside to this method is that every new state requires modifying everywhere it
/// interacts with, and also requires you to handle all the state in this switch statement.
/// This is pattern is where many folks end up stopping, but there are further improvements you can make by using the
/// state pattern.
/// </summary>
public partial
Not only did this simple change get rid of the nesting, it also clearly divides the states in a way that makes it easier to understand and maintain. Instead of having to check each individual flag and compare them for mutual exclusivity, we can simply say that the player is in a specific state at any given time and they simply flow between them based on input. This is probably good enough for a lot of games, but the core issue with this and the previous approach is that you will have to update state changes in multiple places because the way we transition between each state is not decoupled in a clean way yet. Also, it means you need to put everything in one place, which is either easier or harder to refactor depending on the person; some folks like to have large files with everything in it, while others like to abstract chunks out into separate modules and functions, so this is totally a personal preference.
State Pattern
In order to abstract out some of these pieces we need to turn to the state pattern. Instead of simply using enums, we can organize each state into its own class and transition between them without having to know or care about that in the player class:
using ;
;
/// <summary>
/// In comparison to the previous, we are able to separate the state management logic
/// into its own base class to create a reusable container of sorts. This lets a developer create new states
/// independently of any other while still allowing them to transition between one another.
/// </summary>
public partial
...
using Godot;
;
/// <summary>
/// This example of the state pattern is loosely based on the initial implementation presented in
/// Game Programming Patterns (https://gameprogrammingpatterns.com/state.html). The primary State class only
/// implements `Enter` and `PhysicsProcess`, but you might also want to consider `Exit` or `Process` methods as well.
/// For example, maybe you need certain sounds to play or some kind of property change to occur independent of physics
/// processing.
/// Another thing to note is that you could also handle changing states in a different way, such as returning new
/// instances when states require a unique instance, or use a data structure to hold re-usable instances, like a
/// Dictionary; the next and final example includes a way to handle reusable instances of state objects in a type-safe manner.
/// However, to keep things simple we will just keep static instances inside the base class.
/// </summary>
public abstract
public
public
public
public
public
Now each state class can contain its own logic, including handling the sprite animations and directions, as well as the player velocity itself. Some people might not like modifying a direct refences to the owner but it is straightforward for our purposes. Also, how you handle the instances of states is entirely based on whether or not your states need to be unique, but in order to keep things simple we simply re-use static instances of each state in the base class.
In some scenarios you might need to return new instances of state, such as wanting unique properties, and in those cases returning a new instance would make more sense at the cost of some performance, since you are constantly creating and collecting new instances, but it shouldn't matter too much for most games, and C#'s garbage collection is quite good.
The only remaining issue with this code is that we leak the state transitions into the player class. This is not a big deal, but having to check for state changes and nullability still ties both code directly to one another, and in order to further decouple we need to create another class.
Going Further
Instead of having the player class handle the state transitions, it might make more sense to have a dedicated class to do that for us, letting the state code exist in a way that the player class doesn't need to know or care about it. In order to do that, we need a StateMachine
class that we delegate to during PhysicsProcess
:
using ;
;
/// <summary>
/// This example code further abstracts the state code so that a dedicated state machine is able to handle
/// transitioning between the states, encapsulating the state management code in a dedicated class away from the
/// caller. Now the Player class doesn't have to know anything about the different phases of states and can just
/// delegate that to the PlayerStateMachine. This allows for a separation of concerns.
/// </summary>
public partial
...
using Godot;
;
/// <summary>
/// The State class changes from the previous version by having a parent StateMachine injected, allowing it to
/// transition to another state and have access to the owner without having to have a direct reference to it.
/// Outside of that, we aren't returning a new State instance each time, but instead reusing existing ones in the
/// parent. This code could be refactored even further in a few ways, including using the Name property to match the
/// animation, as well as extract some of the remaining repeated logic into separate methods.
/// </summary>
public abstract
public
public
public
public
public
...
using Godot;
using ;
using ..;
;
/// <summary>
/// The StateMachine's primary purpose is to handle the registration and transitions of its various states. It uses
/// generics to use the type of state to retrieve the instance. You could do the same with any other method, such as
/// strings or enums, but
/// leveraging the type system has the added benefit of intellisense and correctness at the cost of slightly more
/// complicated code.
/// </summary>
public abstract
public
There is a lot more going on here, but the gist of it is we are moving all of the state instances from the PlayerStateMachine
into a Dictionary that uses generics. You could handle this in different ways, including using events. The direct result of moving some of this code out into its own class is that the player class is just calling the state machine, and the state machine is just letting each individual state handle the transitions by calling up to its parent.
Handling Multiple States At The Same Time
One of the primary limitations of using a FSM is that you can only handle transitions between a single state, so if we wanted to add, say, a combat system, we couldn't just throw it into the code we have here. Instead, one of the simplest approaches would be to create a CombatStateMachine
and handle all combat logic there. We would then be using concurrent state machines. This works well when you have clear separation between each state machine.
However, when this isn't the case there is the added challenge of figuring out how to have the two or more state machines that are able to communicate to one another. For example, if I only want to have the player be able to attack when standing but not when jumping or ducking then I would need a way to let the combat state machine know that the player is standing or not in order to allow for an attack. There are a few ways to handle this, including using flags again, just having the state machines directly access each other in some way, or even to use events, but the point is that it's not a cut and dry situation. For some situations you could simply use composite states, which are just states that represent two or more states combined. For example, if you wanted to represent the state of duck-jumping, you could create a DuckAndJumping
state, going through each possible combination available.
One alternative to that would be pushdown automata (PDA). PDAs basically operate as a stack that pushes and pops state as they are added. For example, if you are jumping, ducking, and shooting, then shooting will be placed at the top, followed by ducking, and then jumping. This lets you add unrelated states together without having to think too much about the interactions directly.
Technically you are not in these states in parallel, but due to the nature of games it wouldn't be apparent to the player because it would happen so fast. In comparison, using concurrent FSMs would truly be parallel because you can handle multiple states at the same time. PDAs and some of the other alternatives to FSMs, like behavior trees, are primarily used for other purposes, such as language processing or artificial intelligence, but you could theoretically use them for managing game state as well.
At the end of the day, however, finite state machines are still probably the easiest and most direct way to organize state, and with the state pattern you are pretty well-equipped to handle most situations.
Final Thoughts
One of the main things to understand about programming is that there isn't just a once-size-fits-all solution for every problem, and while many people may feel perfectly fine with any of the ideas presented here, some might have totally different approaches. Game development, and even programming in general, is a very creative endeavour, so always keep an open mind and adapt things to your own particular situation.
Footnotes
Bitfields
Another alternative to using booleans is to use bit fields. Bit fields are not always readily available in every programming language, but you can always create your own functions to manipulate data at the bit level. One of the primary benefits of using them is that they can represent each flag in a single bit, allowing you to pack all of your flags in a much smaller piece of memory, which can be beneficial when trying to optimize for certain performance characteristics or for networking, as it lowers the amount data being sent over the wire.
A great example of using bit fields is the Quake 3 source code, which is a great place to study some interesting game programming techniques.
In the case of Quake 3, the player has a "container" for all of its flags called
pm_flags
, which is simply anint
on the structplayerState_s
defined here and holds all of the possible state combinations in this single field. Using bit fields can signficantly reduce the amount of memory used to maintain the fields for an entity, but they are more complicated and may require more planning; a big benefit is that for networked games it makes a lot of sense to pack as much data as possible to lower the amount of packets sent over the wire, and in the case of Quake 3 and all of the games that use its engine, it possibly have a big impact on the netplay experience.Here is an example from Quake 3's source code:
If we take a look at the code for
PM_CheckDuck
here, we can see a function example of how it works:... if else ...
If you're not familiar with bitwise operations, the main thing to understand is that most of the how you would handle flags using booleans will be very similar.
Another example is limiting the speed while ducking:
... // clamp the speed lower if ducking if ...
Here, the
if
statement is checking if the entity is ducked, and if so, it is clamping its speed to either the current speed or the speed with the ducking movement penalty applied. Again, using bit fields is mostly the same process as using booleans but with bitwise operators.If you want to learn more about the older code from id software games, definitely checkout the books by Fabien Sanglard, Game Engine Black Book: Wolfenstein and Game Engine Black Book: Doom; his blog has a wealth of information, including some interesting deep dives into other older games.