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 ]
Using The State Pattern To Simplify Your Game States

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 Godot;

namespace StatePatternExample.Basic;

/// <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 class PlayerBasic : CharacterBody2D
{
    private const float Speed = 300.0f;
    private const float JumpVelocity = -650.0f;
    private const float DiveVelocity = -JumpVelocity * 0.75f;

    private bool _isDiving;
    private bool _isDucking;
    private bool _isJumping;

    private AnimatedSprite2D _sprite2D;

    public override void _Ready()
    {
        _sprite2D = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
    }

    public override void _PhysicsProcess(double delta)
    {
        var velocity = Velocity;

        if (!IsOnFloor())
        {
            velocity += GetGravity() * (float)delta;
        }

        var direction = Input.GetAxis("move_left", "move_right");

        if (Input.IsActionJustPressed("jump") && !_isDucking)
        {
            velocity.Y = JumpVelocity;
            _sprite2D.Play("jump");
            _isJumping = true;
        }
        else if (Input.IsActionPressed("duck"))
        {
            if (_isJumping && !_isDiving)
            {
                _sprite2D.Play("dive");
                velocity.X = 0;
                velocity.Y += DiveVelocity;
                _isDiving = true;
            }

            if (_isDiving)
            {
                if (IsOnFloor())
                {
                    _isDiving = false;
                    if (Input.IsActionPressed("duck"))
                    {
                        _sprite2D.Play("duck");
                    }
                    else
                    {
                        _sprite2D.Play("stand");
                        velocity = Vector2.Zero;
                    }

                }
            }
            else if (!_isJumping)
            {
                _sprite2D.Play("duck");
                velocity = Vector2.Zero;
                _isDucking = true;
            }
        }
        else if (Input.IsActionJustReleased("duck") && _isDucking)
        {
            _isDucking = false;
        }
        else if (IsOnFloor() && Mathf.IsZeroApprox(direction) && !_isDucking)
        {
            velocity = Vector2.Zero;
            _sprite2D.Play("stand");
            _isDiving = false;
            _isDucking = false;
            _isJumping = false;
        }
        else
        {
            if (IsOnFloor())
            {
                _isDiving = false;
                _isJumping = false;
            }

            if (!_isJumping)
            {
                _sprite2D.Play("move");
            }

            velocity.X = direction * Speed;
            _sprite2D.FlipH = direction < 0;
        }

        Velocity = velocity;
        MoveAndSlide();
    }
}

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:

image

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 Godot;

namespace StatePatternExample.Enum;

/// <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 class PlayerEnum : CharacterBody2D
{
    private const float Speed = 300.0f;
    private const float JumpVelocity = -650.0f;
    private const float DiveVelocity = -JumpVelocity * 0.75f;

    private AnimatedSprite2D _sprite2D;
    private State _state = State.Standing;

    public override void _Ready()
    {
        _sprite2D = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
    }

    public override void _PhysicsProcess(double delta)
    {
        var velocity = Velocity;

        if (!IsOnFloor())
        {
            velocity += GetGravity() * (float)delta;
        }

        var direction = Input.GetAxis("move_left", "move_right");

        switch (_state)
        {
            case State.Standing:
                if (Input.IsActionJustPressed("jump"))
                {
                    velocity.Y = JumpVelocity;
                    _state = State.Jumping;
                }

                if (Input.IsActionJustPressed("duck"))
                {
                    _sprite2D.Play("duck");
                    _state = State.Ducking;
                }
                else if (!Mathf.IsZeroApprox(direction))
                {
                    _sprite2D.Play("move");
                    velocity.X = direction * Speed;
                    _sprite2D.FlipH = direction < 0;
                    _state = State.Moving;
                }

                break;
            case State.Jumping:
                if (IsOnFloor())
                {
                    _sprite2D.Play("stand");
                    velocity = Vector2.Zero;
                    _state = State.Standing;
                }
                else if (Input.IsActionJustPressed("duck"))
                {
                    _sprite2D.Play("dive");
                    velocity.X = 0;
                    velocity.Y += DiveVelocity;
                    _state = State.Diving;
                }
                else
                {
                    velocity.X = direction * Speed;
                    _sprite2D.FlipH = direction < 0;
                }

                break;
            case State.Ducking:
                velocity = Vector2.Zero;
                if (Input.IsActionJustReleased("duck"))
                {
                    _sprite2D.Play("stand");
                    _state = State.Standing;
                }

                break;
            case State.Diving:
                if (IsOnFloor())
                {
                    if (Input.IsActionPressed("duck"))
                    {
                        _sprite2D.Play("duck");
                        _state = State.Ducking;
                    }
                    else
                    {
                        _sprite2D.Play("stand");
                        velocity = Vector2.Zero;
                        _state = State.Standing;
                    }
                }

                break;
            case State.Moving:
                if (Input.IsActionJustPressed("jump"))
                {
                    velocity.Y = JumpVelocity;
                    _state = State.Jumping;
                }
                else if (Input.IsActionPressed("duck"))
                {
                    _sprite2D.Play("duck");
                    _state = State.Ducking;
                }
                else if (Mathf.IsZeroApprox(direction))
                {
                    _sprite2D.Play("stand");
                    _state = State.Standing;
                    velocity = Vector2.Zero;
                }
                else
                {
                    velocity.X = direction * Speed;
                    _sprite2D.FlipH = direction < 0;
                }

                break;
        }

        Velocity = velocity;
        MoveAndSlide();
    }

    private enum State
    {
        Standing,
        Jumping,
        Ducking,
        Diving,
        Moving
    }
}

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 Godot;

namespace StatePatternExample.StatePattern;

/// <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 class PlayerStatePattern : CharacterBody2D
{
    public const float Speed = 300.0f;
    public const float JumpVelocity = -650.0f;
    public const float DiveVelocity = -JumpVelocity * 0.75f;

    public AnimatedSprite2D Sprite2D { get; private set; }
    private State? _currentState { get; set; }

    public float Direction => Input.GetAxis("move_left", "move_right");

    public override void _Ready()
    {
        Sprite2D = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
        _currentState = new StandingState();
    }

    public override void _PhysicsProcess(double delta)
    {
        if (!IsOnFloor())
        {
            Velocity += GetGravity() * (float)delta;
        }

        var newState = _currentState?.PhysicsProcess(this, delta);
        if (newState is not null && newState != _currentState)
        {
            _currentState = newState;
            _currentState.Enter(this);
        }

        MoveAndSlide();
    }
}
...

using Godot;

namespace StatePatternExample.StatePattern;

/// <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 class State
{
    public abstract void Enter(PlayerStatePattern player);
    public abstract State? PhysicsProcess(PlayerStatePattern player, double delta);

    protected static readonly StandingState StandingState = new();
    protected static readonly JumpingState JumpingState = new();
    protected static readonly DuckingState DuckingState = new();
    protected static readonly DivingState DivingState = new();
    protected static readonly MovingState MovingState = new();
}

public class StandingState : State
{
    public override void Enter(PlayerStatePattern player)
    {
        player.Sprite2D.Play("stand");
        player.Velocity = Vector2.Zero;
    }

    public override State? PhysicsProcess(PlayerStatePattern player, double delta)
    {
        if (Input.IsActionJustPressed("jump"))
        {
            return JumpingState;
        }

        if (Input.IsActionJustPressed("duck"))
        {
            return DuckingState;
        }

        if (!Mathf.IsZeroApprox(player.Direction))
        {
            return MovingState;
        }

        return null;
    }
}

public class JumpingState : State
{
    public override void Enter(PlayerStatePattern player)
    {
        player.Velocity = player.Velocity with { Y = PlayerStatePattern.JumpVelocity };
        player.Sprite2D.Play("jump");
    }

    public override State? PhysicsProcess(PlayerStatePattern player, double delta)
    {
        if (player.IsOnFloor())
        {
            return StandingState;
        }

        if (Input.IsActionJustPressed("duck"))
        {
            return DivingState;
        }

        player.Velocity = player.Velocity with { X = player.Direction * PlayerStatePattern.Speed };
        player.Sprite2D.FlipH = player.Direction < 0;

        return null;
    }
}

public class DuckingState : State
{
    public override void Enter(PlayerStatePattern player)
    {
        player.Sprite2D.Play("duck");
        player.Velocity = Vector2.Zero;
    }

    public override State? PhysicsProcess(PlayerStatePattern player, double delta)
    {
        if (Input.IsActionJustReleased("duck"))
        {
            return StandingState;
        }

        return null;
    }
}

public class DivingState : State
{
    public override void Enter(PlayerStatePattern player)
    {
        player.Velocity = new Vector2(0, player.Velocity.Y + PlayerStatePattern.DiveVelocity);
        player.Sprite2D.Play("dive");
    }

    public override State? PhysicsProcess(PlayerStatePattern player, double delta)
    {
        if (!player.IsOnFloor())
        {
            return null;
        }

        if (Input.IsActionPressed("duck"))
        {
            return DuckingState;
        }

        return StandingState;
    }
}

public class MovingState : State
{
    public override void Enter(PlayerStatePattern player)
    {
        player.Sprite2D.Play("move");
    }

    public override State? PhysicsProcess(PlayerStatePattern player, double delta)
    {
        if (Input.IsActionPressed("jump"))
        {
            return JumpingState;
        }

        if (Input.IsActionPressed("duck"))
        {
            return DuckingState;
        }

        if (Mathf.IsZeroApprox(player.Direction))
        {
            return StandingState;
        }

        player.Velocity = player.Velocity with { X = player.Direction * PlayerStatePattern.Speed };
        player.Sprite2D.FlipH = player.Direction < 0;

        return null;
    }
}

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 Godot;

namespace StatePatternExample.Final;

/// <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 class PlayerFinal : CharacterBody2D
{
    public const float Speed = 300.0f;
    public const float JumpVelocity = -650.0f;
    public const float DiveVelocity = -JumpVelocity * 0.75f;

    private Label _stateLabel;
    private PlayerStateMachine PlayerStateMachine;
    public AnimatedSprite2D Sprite2D { get; private set; }

    public float Direction => Input.GetAxis("move_left", "move_right");

    public override void _Ready()
    {
        Sprite2D = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
        PlayerStateMachine = new PlayerStateMachine(this);
        _stateLabel = GetNode<Label>("Label");
    }

    public override void _PhysicsProcess(double delta)
    {
        if (!IsOnFloor())
        {
            Velocity += GetGravity() * (float)delta;
        }

        PlayerStateMachine.PhysicsProcess(delta);
        _stateLabel.Text = PlayerStateMachine.CurrentState?.Name;

        MoveAndSlide();
    }
}
...
using Godot;

namespace StatePatternExample.Final;

/// <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 class State<TOwner>(StateMachine<TOwner> parent)
{
    protected readonly StateMachine<TOwner> Parent = parent;

    public abstract string Name { get; }
    public abstract void Enter(TOwner owner);
    public abstract void PhysicsProcess(TOwner owner, double delta);
}

public class StandingState(StateMachine<PlayerFinal> parent) : State<PlayerFinal>(parent)
{
    public override string Name => nameof(StandingState);

    public override void Enter(PlayerFinal player)
    {
        player.Sprite2D.Play("stand");
        player.Velocity = Vector2.Zero;
    }

    public override void PhysicsProcess(PlayerFinal player, double delta)
    {
        if (Input.IsActionJustPressed("jump"))
        {
            Parent.TransitionTo<JumpingState>();
            return;
        }

        if (Input.IsActionJustPressed("duck"))
        {
            Parent.TransitionTo<DuckingState>();
            return;
        }

        if (!Mathf.IsZeroApprox(player.Direction))
        {
            Parent.TransitionTo<MovingState>();
        }
    }
}

public class JumpingState(StateMachine<PlayerFinal> parent) : State<PlayerFinal>(parent)
{
    public override string Name => nameof(JumpingState);

    public override void Enter(PlayerFinal player)
    {
        player.Velocity = player.Velocity with { Y = PlayerFinal.JumpVelocity };
        player.Sprite2D.Play("jump");
    }

    public override void PhysicsProcess(PlayerFinal player, double delta)
    {
        if (player.IsOnFloor())
        {
            Parent.TransitionTo<StandingState>();
            return;
        }

        if (Input.IsActionJustPressed("duck"))
        {
            Parent.TransitionTo<DivingState>();
            return;
        }

        player.Velocity = player.Velocity with { X = player.Direction * PlayerFinal.Speed };
        player.Sprite2D.FlipH = player.Direction < 0;
    }
}

public class DuckingState(StateMachine<PlayerFinal> parent) : State<PlayerFinal>(parent)
{
    public override string Name => nameof(DuckingState);

    public override void Enter(PlayerFinal player)
    {
        player.Sprite2D.Play("duck");
        player.Velocity = Vector2.Zero;
    }

    public override void PhysicsProcess(PlayerFinal player, double delta)
    {
        if (Input.IsActionJustReleased("duck"))
        {
            Parent.TransitionTo<StandingState>();
        }
    }
}

public class DivingState(StateMachine<PlayerFinal> parent) : State<PlayerFinal>(parent)
{
    public override string Name => nameof(DivingState);

    public override void Enter(PlayerFinal player)
    {
        player.Velocity = new Vector2(0, player.Velocity.Y + PlayerFinal.DiveVelocity);
        player.Sprite2D.Play("dive");
    }

    public override void PhysicsProcess(PlayerFinal player, double delta)
    {
        if (!player.IsOnFloor())
        {
            return;
        }

        if (Input.IsActionPressed("duck"))
        {
            Parent.TransitionTo<DuckingState>();
            return;
        }

        Parent.TransitionTo<StandingState>();
    }
}

public class MovingState(StateMachine<PlayerFinal> parent) : State<PlayerFinal>(parent)
{
    public override string Name => nameof(MovingState);

    public override void Enter(PlayerFinal player)
    {
        player.Sprite2D.Play("move");
    }

    public override void PhysicsProcess(PlayerFinal player, double delta)
    {
        if (Input.IsActionJustPressed("jump"))
        {
            Parent.TransitionTo<JumpingState>();
            return;
        }

        if (Input.IsActionPressed("duck"))
        {
            Parent.TransitionTo<DuckingState>();
            return;
        }

        if (Mathf.IsZeroApprox(player.Direction))
        {
            Parent.TransitionTo<StandingState>();
            return;
        }

        player.Velocity = player.Velocity with { X = player.Direction * PlayerFinal.Speed };
        player.Sprite2D.FlipH = player.Direction < 0;
    }
}
...
using Godot;
using System;
using System.Collections.Generic;

namespace StatePatternExample.Final;

/// <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 class StateMachine<TOwner>(TOwner owner)
{
    protected Dictionary<Type, State<TOwner>> _states { get; init; } = new();
    public State<TOwner>? CurrentState { get; private set; }
    private TOwner Owner => owner;

    public void TransitionTo<TState>() where TState : State<TOwner>
    {
        var state = typeof(TState);
        if (!_states.TryGetValue(state, out var newState))
        {
            GD.PrintErr($"Could not find {typeof(TState).Name}"); // typically you might want to handle this instead of just logging it
            return;
        }

        if (CurrentState == newState)
        {
            return;
        }

        CurrentState = newState;
        CurrentState?.Enter(Owner);
    }

    public void PhysicsProcess(double delta)
    {
        CurrentState?.PhysicsProcess(Owner, delta);
    }
}

public class PlayerStateMachine : StateMachine<PlayerFinal>
{
    public PlayerStateMachine(PlayerFinal owner) : base(owner)
    {
        _states = new Dictionary<Type, State<PlayerFinal>>
        {
            { typeof(StandingState), new StandingState(this) },
            { typeof(JumpingState), new JumpingState(this) },
            { typeof(DuckingState), new DuckingState(this) },
            { typeof(DivingState), new DivingState(this) },
            { typeof(MovingState), new MovingState(this) }
        };

        TransitionTo<StandingState>();
    }
}

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.

pushdown automata example

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 an int on the struct playerState_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:

#define PMF_DUCKED              1
#define PMF_JUMP_HELD  2
#define PMF_BACKWARDS_JUMP 8  // go into backwards land
#define PMF_BACKWARDS_RUN 16  // coast down to backwards run
#define PMF_TIME_LAND  32  // pm_time is time before rejump
#define PMF_TIME_KNOCKBACK 64  // pm_time is an air-accelerate only time
#define PMF_TIME_WATERJUMP 256  // pm_time is waterjump
#define PMF_RESPAWNED  512  // clear after attack and jump buttons come up
#define PMF_USE_ITEM_HELD 1024
#define PMF_GRAPPLE_PULL 2048 // pull towards grapple location
#define PMF_FOLLOW         4096 // spectate following another player
#define PMF_SCOREBOARD  8192 // spectate as a scoreboard
#define PMF_INVULEXPAND  16384 // invulnerability sphere set to full size

If we take a look at the code for PM_CheckDuck here, we can see a function example of how it works:

...
if (pm->cmd.upmove < 0)
 { // duck
  pm->ps->pm_flags |= PMF_DUCKED;
 }
 else
 { // stand up if possible
  if (pm->ps->pm_flags & PMF_DUCKED)
  {
   // try to stand up
   pm->maxs[2] = 32;
   pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, pm->ps->origin, pm->ps->clientNum, pm->tracemask );
   if (!trace.allsolid)
    pm->ps->pm_flags &= ~PMF_DUCKED;
  }
 }
...

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 ( pm->ps->pm_flags & PMF_DUCKED ) {
  if ( wishspeed > pm->ps->speed * pm_duckScale ) {
   wishspeed = pm->ps->speed * pm_duckScale;
  }
 }
...

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.