Godot Recipe: Finite State Machine #1
🙋🏼 A huge thanks goes out to Robert Nystrom for writing the book Game Programming Patterns and thus inspiring me to write this post!
⚠️ This post assumes you have a basic understanding of Nodes and Scenes in Godot and some familiarity with C# syntax if you plan to code along!
Have you ever had your Character Controller code turn into proper spaghetti?
I have – and it’s no fun.
While not a perfect cure, a finite state machine (FSM) can help you untangle a lot of the mess that builds up as you add more complexity to your character controller.
In this recipe series, we’ll build an FSM module for Godot using C#.
The long term goal is to provide a module where an arbitrary State
can control arbitrary Systems
on our player character.
Along the line we’ll make mistakes and questionable design decisions. This is how we learn!
🙋🏼 If you simply want a functioning FSM for your Godot project, I suggest heading over to the Godot Asset Library and searching for available FSM plugins. There is no need to reinvent the wheel, unless you want to!
Scope
At the end of this post, we will end up with a state machine that controls a simple Mover system that pushes a RigidBody3D around.
The FSM will support two states that we can transition between - idle and walking.
In future posts, we will iterate on the module, improving the design and architecture making it more generic and more loosely coupled – so stick around! 🙂
Project setup
Before diving into the code, we head on over to Godot and set up some WASD mappings for movement. We’ll call the actions up
, down
, left
and right
:
Next, we setup a scene (let’s call it main.tscn
):
Add a RigidBody3D to the scene and call it Player
. Then, add some visuals and a collider as children nodes. I’m using a capsule shape for both.
With the rigidbody selected, go to Axis Lock
and check Linear Y
, Angular X
and Angular Z
. This way the Player will ignore gravity and won’t topple over.
In addition to this, set Linear > Damp
to something like 5. This will prevent us from sliding around like crazy!
Finally, add a Camera3D and position it reasonably so it looks slightly down at the world origin. Your scene will look a little like this:
Now that we’re all set up, let’s get codin’!
Defining a State
We decide that our states should be able to handle logic that executes:
- Upon entering the state
- During idle processing –
Node._Process(double)
- During physics processing –
Node._PhysicsProcess(double)
- Upon exiting the state
With this in mind, we sketch up a class that looks like this:
🙋🏼 We will notice this definition changes throughout the series. For now, let’s continue!
Abstract State
Next we create an abstract State class with this definition in our project. I’ll just plop it down right in a scripts folder for now.
We won’t be inheriting from Node, but instead make this a pure C# class. We will also have it perform some basic logging in each method.
Replace all contents of State.cs with the following:
using ;
public abstract
IdleState
In addition to this abstract State, let’s create two concrete states - the IdleState
and the WalkingState
. Both inherit from State
and, for now, does nothing except call the virtual methods on the base class, for now.
Create both scripts and put them in the scripts
folder.
public
WalkingState
For the WalkingState
, we will leave a TODO in the PhysicsTick
function:
public
Fantastic! We now have two states ready for use. To actually use them, we need to create the actual SM of our FSM.
Enter, the state machine!
The State Machine
To enable our character to use our states, we need a controller of sorts to track and handle what state we are in and transitions to other states.
Creating the machine
In its most simple form, a state machine is a state container that can call the state functions and change what state we are in. To make it clearly visible in the scene hierarchy, let’s make it inherit from Node.
Let’s sketch one up!
Similarly, we create a StateMachine.cs
script and dunk it straight into our scripts
folder. The story goes:
- We keep a list of all possible States in the machine and create them in the
_Ready
function. - We decide on a default state for the machine and set it in the
_Ready
function. - We keep track of the current state.
- We’ll call the
Tick
andPhysicsTick
state functions in_Process
and_PhysicsProcess
respectively. ChangeState
handles calling the exit and enter logic on the states we transition between.
It looks something like this:
using ;
// NOTE allow creation in "Create New Node" in Godot
public partial
🙋🏼 For now, let’s keep the concrete states public - this will trick us into a circular dependency very soon, but we’ll break that up in a later part of this series!
Our FileSystem should look like this, at this point:
Next we’ll take our State Machine for a logging spin!
Testing the machine
Add the StateMachine node anywhere in your scene. Hitting play should yield some logs:
While not particularly exciting, we are successfully idling! If you’ve come this far, take a short break and grab your drink of choice before continuing. Well done! ☕
Next, we’ll be tackling transitioning to other states!
State Transitions
We have states and we have a machine - all we need now is transitions between the states. It will take a little refactoring, so buckle up!
Setting up our states and machine for transitioning
The conditions that decide if we should switch to another state will live inside our states. One way to change our state is to let our states call the ChangeState
function on our machine. Let’s do that for now!
Remember that circular dependency I warned about earlier? Don’t blink, because it’s happening now.
We will introduce a StateMachine
reference in our states. It will sit snugly in the abstract State class like so and be assigned in the constructor:
public abstract
🙋🏼 While it’s not pretty, it does the trick for now. I promise you we’ll sort this circular dependency atrocity out in the future!
Lastly, we update our concrete state constructors to pass the StateMachine down the base constructor. We also update our state constructions in StateMachine.cs
:
// StateMachine.cs
public override
{
idleState = ; // Update
walkingState = ; // Update
;
}
// IdleState.cs constructor
public : base { } // NEW!
// WalkingState.cs constructor
public : base { } // NEW!
And with that, we have all the plumbing to perform state transitions! We are nearly there…
Implementing transition conditions
We need to determine when to switch between the Idle and Walking states. For this, we tell IdleState
that if we’re moving with WASD, we should transition to walking.
In Tick
we poll for our transition condition like so:
// IdleState.cs
public override void Tick(double delta)
In WalkingState
, if we’re not pressing WASD, we transition to IdleState
.
// WalkingState.cs
public override void Tick(double delta)
At this point we have two states, a state machine and conditions in the states describing when to switch between the states.
A simple Mover system
We will showcase our FSM using a really basic Mover:
using ;
public partial
Add the Mover
node to the Player
node, and assign the Player
as the rigidbody
in the Inspector. If you haven’t already, this is also the time to add the StateMachine
to your Player
.
Next, add a public export reference to the Mover in our machine so that we can access it from our WalkingState.
Don’t forget to assign the Mover in the Inspector afterwards!
public partial
🙋🏼 In a future post, we will improve the way we get the systems that are used in our states. At this point, it doesn’t have to be perfect!
Finally, update the WalkingState
to perform some movement in PhysicsTick
:
// WalkingState.cs
public override void PhysicsTick(double delta)
Just like that, we’re ready for the big reveal.
The final result
With no fear in your heart, press F5 in Godot and take her for a spin!
There we have it – a functional FSM framework! This can be used to control all possible aspects of a character, if we put some more love into it.
Give yourself a compliment or two, you’ve earned it! 👏🏼
Experiment and play around with the FSM – add some states, a new system, or try to break the circular dependency yourself.
Have fun with it! ☀️
In the next part, I will show you how I solve the circular dependency. We will also improve the way we fetch systems in our states.
All the best,
Nilsiker