How to implement a State Machine in Godot

Sandro Maglione

Sandro Maglione

Games

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 πŸ‘‡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)

state.gd
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 a state_machine.gd script attached, which is responsible to manage the current state and switch between states
  • Each child Node has a script that extends 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 stateExample 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 calling Update and Physics_update
  • Switch between states by calling Enter and Exit when a transitioned signal is emitted
state_machine.gd
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 editor
  • states: a Dictionary 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:

state_machine.gd
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):

state_machine.gd
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:

state_machine.gd
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 build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 400+ readers.

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 jump
  • Jump: The player jumped and it is now in the air
  • DoubleJump: 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 to Jump when the jump button is pressed
  • When in the Jump state the machine can transition to DoubleJump if the jump button is pressed again
  • From both Jump and DoubleJump the machine transitions back to Idle when the player touches the ground

The state machine contains 3 states (Idle, Jump, DoubleJump) and 4 transitionsThe state machine contains 3 states (Idle, Jump, DoubleJump) and 4 transitions

We create 3 Nodes 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 stateThe 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 Nodes.

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:

idle_jump_state.gd
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:

jumping_jump_state.gd
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:

double_jumping_jump_state.gd
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 each emit

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.emitMake 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 calling transitioned.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:

jump_state.gd
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:

idle_jump_state.gd
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")
jumping_jump_state.gd
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")
double_jumping_jump_state.gd
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 attachedCreate 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

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 build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 400+ readers.

πŸ‘‹γƒ»Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 400+ readers.