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

const DEBUG = true

var state: Object

var history = []

func _ready():
	# Set the initial state to the first child node
	state = get_child(0)
	_enter_state()
	
func change_to(new_state):
	history.append(state.name)
	state = get_node(new_state)
	_enter_state()

func back():
	if history.size() > 0:
		state = get_node(history.pop_back())
		_enter_state()

func _enter_state():
	if DEBUG:
		print("Entering state: ", state.name)
	# Give the new state a reference to this state machine script
	state.fsm = self
	state.enter()

# Route Game Loop function calls to
# current state handler method if it exists
func _process(delta):
	if state.has_method("process"):
		state.process(delta)

func _physics_process(delta):
	if state.has_method("physics_process"):
		state.physics_process(delta)

func _input(event):
	if state.has_method("input"):
		state.input(event)

func _unhandled_input(event):
	if state.has_method("unhandled_input"):
		state.unhandled_input(event)

func _unhandled_key_input(event):
	if state.has_method("unhandled_key_input"):
		state.unhandled_key_input(event)

func _notification(what):
	if state && state.has_method("notification"):
		state.notification(what)

We give the script a class_name so that it will be available as a custom Node in the Editor.

There is a DEBUG constant to enable diagnostic messages to the Output Window.

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 takes the new_state Node Reference and sets the Current State to this and pushes the previous State onto the history Stack. Then it calls the _enter_state function

The _enter_state function sets the fsm (Finite State Machine) property of the current State to this State Machine controller so that the State exit function may access the common functions without needing signals or getting it's parent. Then the enter function of the new State is called.

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.

When the game is running, various override methods may be called such as _process(delta). So these are all implemented with a test of if the current State is able to handle the method. In order to handle one of these methods, the State should implement a function of the same name minus the underscore prefix.

This is so that only the current State will respond to the events that trigger these functions.

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) 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 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
	yield(get_tree().create_timer(2.0), "timeout")
	exit("State2")

func exit(next_state):
	fsm.change_to(next_state)

# Optional handler functions for game loop events
func process(delta):
	# Add handler code here
	return delta

func physics_process(delta):
	return delta

func input(event):
	return event

func unhandled_input(event):
	return event

func unhandled_key_input(event):
	return event

func notification(what, flag = false):
	return [what, flag]

The return statements are to suppress compiler warnings where values are unused.

This code may be copied and customised for each new State.

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!")
	yield(get_tree().create_timer(2.0), "timeout")
	exit()

func exit():
	fsm.back()

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.

Access the Code

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.

Support this website!Godot Engine Game Development in 24 Hours*

* As an Amazon Associate I earn from qualifying purchases.