The State pattern allows to better organize and reason about the logic of your game. It can be used in multiple contexts:
- Player and NPC movements
- AI
- Animations
- Game UI
Each state is contained in its own class and a shared state machine script is responsible to execute the current state and transition between states.
Example of state machine with 3 states and their transitions. Let's learn how to implement this state machine in this post π
In this post we learn how to implement a state machine in Godot:
- How to implement a generic and reusable state machine
- How to define each state and their interactions
- How to transition between states using signals
- How to solve the most common issues with state machines
State script
Each state machine contains a collection of states.
State
is a script that all the states in the game will extend (extends State
)
class_name State
extends Node
signal transitioned(new_state_name: StringName)
func Enter() -> void:
pass
func Exit() -> void:
pass
func Update(delta: float) -> void:
pass
func Physics_update(delta: float) -> void:
pass
A State
contains the following methods:
Enter
: Execute some logic when the machine enters the state (initialize variables, trigger one-time actions, update current animation)Exit
: Execute some logic when the machine exists the state (clean up resources, reset variables, remove nodes)Update
: Execute some logic at every frame (equivalent to_process
)Physics_update
: Execute some logic at fixed intervals (equivalent to_physics_process
)
Each State
also has a signal
that triggers a state change. This signal accepts the name of the next state as parameter. The state machine will react to the signal to update the current state.
State machine in Godot
A state machine is implemented using a Node
. Each child of this node represents a possible state of the machine:
- The parent
Node
has astate_machine.gd
script attached, which is responsible to manage the current state and switch between states - Each child
Node
has a script thatextends State
and implements the logic of the state it represents
Example of state machine in Godot: a parent node represents the state machine, while each child node represents a unique state
Note: By convention all the states have a suffix based on the state machine their are part of (
-JumpState
in the example)
State machine implementation
The script for the state machine is unique and will be shared by all the state machines in the project.
The responsibility of the state machine is to:
- Store and execute the
current_state
by callingUpdate
andPhysics_update
- Switch between states by calling
Enter
andExit
when atransitioned
signal is emitted
class_name StateMachine
extends Node
@export var current_state: State
var states: Dictionary = {}
The state machine script contains two variables:
current_state
: represents the current state. It uses@export
to assign the initial state from the editorstates
: aDictionary
containing all the child nodes (states) indexed by their name
In the _ready
function we initialize states
, we connect the machine to each transitioned
signal, and we start the execution of the initial state:
func _ready():
for child in get_children():
if child is State:
# Add the state to the `Dictionary` using its `name`
states[child.name] = child
# Connect the state machine to the `transitioned` signal of all children
child.transitioned.connect(on_child_transitioned)
else:
push_warning("State machine contains child which is not 'State'")
# Start execution of the initial state
current_state.Enter()
The on_child_transitioned
function gets the name of the next state and executes the transition from the current state (calling Exit
) to the new state (calling Enter
):
func on_child_transitioned(new_state_name: StringName) -> void:
# Get the next state from the `Dictionary`
var new_state = states.get(new_state_name)
if new_state != null:
if new_state != current_state:
# Exit the current state
current_state.Exit()
# Enter the new state
new_state.Enter()
# Update the current state to the new one
current_state = new_state
else:
push_warning("Called transition on a state that does not exist")
Finally, the state machine is also responsible to call Update
and Physics_update
for the current state:
func _process(delta):
current_state.Update(delta)
func _physics_process(delta):
current_state.Physics_update(delta)
Putting all together, the complete script for state_machine.gd
is the following:
class_name StateMachine
extends Node
@export var current_state: State
var states: Dictionary = {}
func _ready():
for child in get_children():
if child is State:
states[child.name] = child
child.transitioned.connect(on_child_transitioned)
else:
push_warning("State machine contains child which is not 'State'")
current_state.Enter()
func _process(delta):
current_state.Update(delta)
func _physics_process(delta):
current_state.Physics_update(delta)
func on_child_transitioned(new_state_name: StringName) -> void:
var new_state = states.get(new_state_name)
if new_state != null:
if new_state != current_state:
current_state.Exit()
new_state.Enter()
current_state = new_state
else:
push_warning("Called transition on a state that does not exist")
Notice how this script is completely generic, and therefore it can be used for all the state machines in the game
There is more π€©
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it
Jumping state machine
We are now going to implement some states for a jumping action.
The first step is defining all the possible states. The machine contains 3 states:
Idle
: The player is on the ground, ready to jumpJump
: The player jumped and it is now in the airDoubleJump
: The player can jump a second time while in the air
The second step is defining the initial state and the transitions between each state:
- The initial state is
Idle
- From
Idle
the machine can transition toJump
when the jump button is pressed - When in the
Jump
state the machine can transition toDoubleJump
if the jump button is pressed again - From both
Jump
andDoubleJump
the machine transitions back toIdle
when the player touches the ground
The state machine contains 3 states (Idle, Jump, DoubleJump) and 4 transitions
We create 3 Node
s and we assign them as children of the parent node containing the state machine:
The parent node represents the state machine, while each child node represents a unique state
We then attach the state_machine.gd
script we implemented before to the parent Node
, and we create and attach a new script for each of the child Node
s.
Implementing the states
The main details of each state implementation are:
- Every state script extends the
State
script (state.gd
) - A state is responsible to emit the
transitioned
signal to perform a transition
In this article we are not interested in the specific jump implementation details, but more on the state pattern
An example of implementation for the Idle
state looks as follows:
class_name IdleJumpState
extends State
@export var actor: CharacterBody2D
func Physics_update(_delta):
var is_jump_just_pressed: bool = Input.is_action_just_pressed("jump")
if is_jump_just_pressed:
if actor.is_on_floor():
transitioned.emit("JumpingJumpState")
else:
transitioned.emit("DoubleJumpingJumpState")
The Jump
state may be implemented as follows:
class_name JumpingJumpState
extends State
@export var actor: CharacterBody2D
@export var jump_velocity: int = -350
func Enter():
## Apply velocity to jump only once when entering the state
actor.velocity.y = jump_velocity
func Physics_update(_delta):
var is_jump_just_pressed: bool = Input.is_action_just_pressed("jump")
if is_jump_just_pressed and actor.velocity.y > jump_velocity:
transitioned.emit("DoubleJumpingJumpState")
if actor.is_on_floor():
transitioned.emit("IdleJumpState")
And finally the DoubleJump
:
class_name DoubleJumpingJumpState
extends State
@export var actor: CharacterBody2D
@export var jump_velocity: int = -350
@export var double_jump_velocity_scale: float = 0.9
func Enter():
actor.velocity.y = jump_velocity * double_jump_velocity_scale
func Physics_update(_delta):
if actor.is_on_floor():
transitioned.emit("IdleJumpState")
Advantages of state machines
- A state implementation is simple compared to the same implementation without state machine (where all the code is implemented in a single file)
- There are no
bool
variables to define the state (is_jumping
,is_double_jumping
, or similar) - A state transition can only be performed by calling
transitioned.emit
: we know exactly which states are connected just by looking at eachemit
Note how each state defines its own
@export
variables.This pattern is called dependency injection: it makes the same state reusable for multiple scenes (simply change the
actor
parameter from the editor)
Important βοΈ: The name of the state passed to
transitioned.emit
must correspond with the name assigned to the node in the editor Make sure that the name of the node in the editor corresponds to the value passed to transitioned.emit
To make the state completely independent and reusable we would need to inject (
@export
) also the name of the next state used when callingtransitioned.emit
βοΈ
Shared resources: JumpState
A common situation is to have some shared parameters between multiple states.
In some cases it is convenient to define a shared state that exports the required parameters for multiple states.
In this example, the actor
parameter (reference to the player) is required in all the states.
Therefore, we can create a new script jump_state.gd
with the following implementation:
class_name JumpState
extends State
@export var actor: CharacterBody2D
This new JumpState
extends State
and exports all the required parameters for all the states.
We can then use it in the state scripts by updating extends State
to extends JumpState
:
class_name IdleJumpState
extends JumpState
func Physics_update(_delta):
var is_jump_just_pressed: bool = Input.is_action_just_pressed("jump")
if is_jump_just_pressed:
if actor.is_on_floor():
transitioned.emit("JumpingJumpState")
else:
transitioned.emit("DoubleJumpingJumpState")
class_name JumpingJumpState
extends JumpState
@export var jump_velocity: int = -350
func Enter():
## Apply velocity to jump only once when entering the state
actor.velocity.y = jump_velocity
func Physics_update(_delta):
var is_jump_just_pressed: bool = Input.is_action_just_pressed("jump")
if is_jump_just_pressed and actor.velocity.y > jump_velocity:
transitioned.emit("DoubleJumpingJumpState")
if actor.is_on_floor():
transitioned.emit("IdleJumpState")
class_name DoubleJumpingJumpState
extends JumpState
@export var jump_velocity: int = -350
@export var double_jump_velocity_scale: float = 0.9
func Enter():
actor.velocity.y = jump_velocity * double_jump_velocity_scale
func Physics_update(_delta):
if actor.is_on_floor():
transitioned.emit("IdleJumpState")
JumpState
extends State
and therefore we still have access to all the methods from State
(enter
, exit
, update
, physics_update
).
On top of that, since each state script extends JumpState
, it has also access to actor
without manually declaring @export
in all scripts.
Concurrent state machines
We now want to implement horizontal movement (walking).
Before adding a new state to the same machine we must think how it interacts with the current states.
In this case, the Walk
state is independent from jumping: the player can be walking and jumping at the same time.
If we try to add a new Walk
state to the same machine this would require duplicating all the states (Walk_Idle
, Walk_Jump
, Walk_DoubleJump
).
When a state is independent from all the others it is more convenient to create a new state machine.
When the machines states are unrelated it is better to create a new state machine instead of extending the current one.
These are called Concurrent state machines.
As we saw before, the code inside state_machine.gd
is completely generic and therefore it can be reused for the new machine.
Create a new state machine containing its own independent states: both JumpStateMachine and PlayerStateMachine have the state_machine.gd attached
This is all you need to implement state machines in Godot!
Some other common patterns are
- Hierarchical State Machines: used to share the same input event between multiple states
- Pushdown Automata: used to remember and come back to previous states
State machines allow to better reason and organize the code. The project becomes more maintainable, bugs are easier to find, and it is now impossible to forget a state or execute the wrong logic between states.
If you are interested in more tutorials on Godot you can check out out all the articles in the blog.
You can also subscribe to the newsletter here below for more updates and exclusive content π
Thanks for reading.
There is more π€©
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it