Learn what a state machine is and how to implement a complete state machine in Godot. Learn how to organize nodes for a state machine, how to define each state, and how to implement transitions and parameters.
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 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 StateMachineextends Node@export var current_state: Statevar 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:
Putting all together, the complete script for state_machine.gd is the following:
class_name StateMachineextends Node@export var current_state: Statevar 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
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 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 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 IdleJumpStateextends State@export var actor: CharacterBody2Dfunc 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 JumpingJumpStateextends State@export var actor: CharacterBody2D@export var jump_velocity: int = -350func Enter(): ## Apply velocity to jump only once when entering the state actor.velocity.y = jump_velocityfunc 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 DoubleJumpingJumpStateextends State@export var actor: CharacterBody2D@export var jump_velocity: int = -350@export var double_jump_velocity_scale: float = 0.9func Enter(): actor.velocity.y = jump_velocity * double_jump_velocity_scalefunc 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.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 JumpStateextends 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 IdleJumpStateextends JumpStatefunc 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 JumpingJumpStateextends JumpState@export var jump_velocity: int = -350func Enter(): ## Apply velocity to jump only once when entering the state actor.velocity.y = jump_velocityfunc 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 DoubleJumpingJumpStateextends JumpState@export var jump_velocity: int = -350@export var double_jump_velocity_scale: float = 0.9func Enter(): actor.velocity.y = jump_velocity * double_jump_velocity_scalefunc 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!
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.