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.
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)?
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.