I use nodes. Then if I want a state to be available, I just attach it StateMachine node.
class_name StateMachine extends Node
## This node is intended to be attached to a character and manage the various
## states of the character. If a charatcer has a state it should be added as a
## child node of the state machine.
## The initial CharacterState for the character when it is added to the game.
@export var starting_state: CharacterState
# The current state of the character. Initially defaults to the first node it
# finds beneath itself if starting_state is not defined.
@onready var _current_state: CharacterState = starting_state if starting_state != null else self.get_child(0)
## Sets up all the states for this character.
func _ready() -> void:
switch_state(_current_state)
for state in get_children():
if state is CharacterState:
state.activate_state()
self.connect("child_entered_tree", _on_state_added)
self.connect("child_exiting_tree", _on_state_removed)
## Switch to the target state from the current state. Fails if:
## 1. The character does not have the passed state.
## 2. The character is already in that state.
## 3. The current state won't allow a transition to happen.
## 4. The target state won't allow a transition (e.g. cooldown timers).
func switch_state(state: CharacterState) -> void:
if not _character_has_state(state): return
if _current_state == state: return
if not _current_state.can_transition: return
if not state.can_transition: return
_current_state.exit_state()
_current_state = state
_current_state.enter_state()
# Returns whether or not the character has this state.
# (A character has a state if the state is a child node of this StateMachine.)
func _character_has_state(state: CharacterState) -> bool:
for element in get_children():
if element == state:
return true
return false
# Activates a state.
# (Called when a node enters the tree as a child node of this StateMachine.)
func _on_state_added(node: Node) -> void:
if not node is CharacterState:
return
node.activate_state()
# Deactivates a state.
# (Called when a child node of this StateMachine leaves the tree.)
func _on_state_removed(node: Node) -> void:
if not node is CharacterState:
return
node.deactivate_state()
It handles what state the character in, but switching in and out of states is handled by the states themselves. I use a base state that is common for the player, enemies and NPCs.
class_name CharacterState extends Node
# A virtual state for states to inherit.
## Stores a reference to the character to which this state is attached.
@onready var character: Character = get_owner()
## Set to false if a character cannot use an ability or take an action,
## for example when waiting for a cooldown timer to expire.
var can_transition = true
## Initialize the state. Process mode is set so the state can be paused.
## Then all processing (including input and physics) is turned off.
func _ready() -> void:
process_mode = ProcessMode.PROCESS_MODE_PAUSABLE
set_physics_process(false)
set_process(false)
set_process_input(false)
set_process_unhandled_input(false)
## Turn processing on for everything but physics when the state is active.
func activate_state() -> void:
set_process(true)
set_process_input(true)
set_process_unhandled_input(true)
## Turn processing off when the state is deactivated.
func deactivate_state() -> void:
set_process(false)
set_process_input(false)
set_process_unhandled_input(false)
set_physics_process(false)
## Virtual function for inherited classes to implement, for example starting an animation.
func enter_state() -> void:
set_physics_process(true)
## Virtual function for inherited classes to implement.
func exit_state() -> void:
set_physics_process(false)
Then a specific state (which inherits from PlayerState which inherits from CharacterState) handles all the things that happen in the state, as well as when it can be entered and exited.
class_name PlayerJumpState extends PlayerState
# If the player presses the Jump action, switch to the Jump state.
func _process(_delta: float) -> void:
if Input.is_action_just_pressed("jump") and character.is_on_floor():
character.state_machine.switch_state(self)
character.velocity.y = character.jump_velocity
# Process the jump every frame and trigger the landing when we land.
func _physics_process(_delta: float) -> void:
if character.is_on_floor():
character.animation_state.travel("Jump_Land")
character.move_and_slide()
# Starts the jump animation, and turns off the ability to transition to
# another state mid-jump.
func enter_state() -> void:
super()
character.animation_tree.connect("animation_finished", _on_animation_finished)
character.animation_state.travel("Jump_Start")
can_transition = false
func exit_state() -> void:
super()
character.animation_tree.disconnect("animation_finished", _on_animation_finished)
# Allows state transition only after the initial Jump_Start animation has been called. This is
# because otherwise the landing animation is called because the first physics frame this class runs
# is from the floor and so is_on_floor() is still true.
func _on_animation_finished(animation_name: String) -> void:
match animation_name:
"Jump_Start":
can_transition = true
This model uses composition and inheritance. Composition is adding to nodes to enable functionality. If I wanted, for example, to make the jump ability a discoverable power, I can only add it to the player when they discover it in game. All the code managing it is encapsulated in that one node. If I want to disable jumping, I can just take the node away, or even move it out from under the StateMachine node temporarily.
StateMachines are at the heart of game development. If you were to code one from scratch, your first state machine would be the game loop that runs, checking to make sure that everything happens every frame: player input, enemies taking actions, etc. Then the player and enemies would all have their own state machines as well. Godot’s game loop is such that you can tap into it any time with the _process() and _physics_process() functions on any node.
There is no usual per se. Whatever works best for you is what’s right for your game.
An enum-based state machine will be faster in theory. But it depends on how you use it. Nodes are optimized by the Godot developers. So while it may seem like nodes are heavier-weight than an enum, you’re better off leaving optimization up to the developers of the language and framework until you run into issues.
For example, every once in a while, I see someone asking about storing ints as 32-bit numbers instead of 64-bit numbers because they want to increase performance. However modern 64-bit processors are optimized to work with 64-bit numbers. If you give it a number in a smaller container, even if it’s just var blah: int = 1
, you are actually slowing the processor down.
So don’t worry about performance until you come across performance issues. Also, if you do come across performance issues the answer to your problem is more likely going to be solved by writing code in C# or C++ to take advantage of compiled code.
TLDR: Use whatever makes your code clear to you. Obfuscating things to try and optimize performance in the beginning will slow you down and likely not matter in the long run.