Godot State Machine
In this tutorial we will explore how to control your game state so that it doesn’t spin out of control. The Finite State Machine (FSM) is a great way to achieve this.
At any point in our game, the game objects will be in a particular State such as Waiting, Jumping, and Running. In order to change State, some event occurs such as a Key Press.
We may plan our game with a diagram that defines the States as Nodes and show connecting arrows that show how transitions between states occur triggered by certain events.
Example diagram showing States and Events causing Transitions between States:
A State Node will have an Entry function, an Exit function (to transition to other States), and internal logic that senses Events and runs code in the Game Loop.
To control the State Machine, we will have a Root Node that has the State Nodes as it’s children and all of the Nodes will have an attached script.
Our StateMachine.gd script will provide functions to change to another State, respond to the various events in Godot such as key presses, and provide a means to step back to the previous State(s).
State Machine Controller Code
extends Node
class_name StateMachine
var current_state: Object
var history = []
var states = {}
func _ready():
for state in get_children():
state.fsm = self
states[state.name] = state
if current_state:
remove_child(state)
else:
current_state = state
current_state.enter()
func change_to(state_name):
history.append(current_state.name)
set_state(state_name)
func back():
if history.size() > 0:
set_state(history.pop_back())
func set_state(state_name):
remove_child(current_state)
current_state = states[state_name]
add_child(current_state)
current_state.enter()
We give the script a class_name so that it will be available as a custom Node in the Editor.
A state variable will hold a reference to the current State Node.
A history variable will hold a history stack (array of State names). The name property of the State Node will be used for the Name of the State.
In the _ready function, the Initial State is set to the first child Node.
The change_to function pushes the previous State name onto the history Stack. Then it calls the set_state function.
The set_state function removes the current state node from the scene tree and adds the new state to the scene tree and calls its enter function.
The back function checks if the history Stack has items and if so, pops the last one off the Stack and enters that new State.
State Handler Code
This script will store a reference back to it’s controller.
The enter function will perform initialisation and run any startup code for the State.
The exit function will call the change_to(next_state_name) method on the controller.
What the State does while it is current depends on it’s internal logic. So maybe we would implement the _process(delta) function or _input(event) function overrides depending on what we want to do. And somewhere within this logic we would call the exit function with the name of a new State to transition to.
So here is a code template for this (state1.gd):
extends Node
var fsm: StateMachine
func enter():
print("Hello from State 1!")
# Exit 2 seconds later
await get_tree().create_timer(2.0).timeout
exit("State2")
func exit(next_state):
fsm.change_to(next_state)
func _unhandled_key_input(event):
if event.pressed:
print("From State1")
This code may be copied and customised for each new State.
There is an example of _unhandled_key_input
to detect key presses and print a message to the output window of the Editor for test purposes.
Here is the code for State2 where we request to go back in history to State1 on exit:
extends Node
var fsm: StateMachine
func enter():
print("Hello from State 2!")
await get_tree().create_timer(2.0).timeout
exit()
func exit():
# Go back to the last state
fsm.back()
func _unhandled_key_input(event):
if event.pressed:
print("From State2")
So this entire example Scene will continually go from State1 to State2 and back to State1 when it is run. It’s just a minimal example of how the State Machine may be set up.
Concurrent State Machines
In your game, you may need to know what the State of another FSM is where the Context is important for decision making. So for this, you could have a dictionary of FSM references and their current State in a global data variable.
So maybe move the state variable out of the scope of the State Machine script and into the Global Context autoload (singleton)?
Conclusion
I think that this is a fairly easy way to set up a flexible Finite State Machine in Godot without too much verbosity in our code.
More solutions
- Godot Keyboard and Mouse Button Input Programming
- Godot Event Handling
- Signals in Godot
- How to Save and Load Godot Game Data
- Godot Timing Tutorial
- Using Anchor Positioning in Godot
- UI Layout using Containers in Godot
- Shaders in Godot
- Godot Behaviour Tree
- Godot Popups
- Parsing XML Data
- Godot Parallax Background
- How to Make a Godot Plugin
- Godot Regex - Regular Expressions
- Random Numbers
- Coroutines, Await and Yield
- GraphNode and GraphEdit Tutorial