Question about state-machines.

As a beginner game dev, I have been learning about state-machines recently, and seeing all the variations. I’ve come to a question: “Are signals needed?”

class_name State
extends Node

@export
var animation_name: String

@export
var move_speed: float = 5.0
@export
var jump_power: float = 7.0
@export
var fall_gravity: float = 2.0
@export
var air_control : float = 0.6

var gravity: int = ProjectSettings.get_setting("physics/3d/default_gravity")

var parent: Player
var camera: Camera3D

func enter() -> void:
	pass

func exit() -> void:
	pass

func process_input(event: InputEvent) -> State:
	return null

func process_frame(delta: float) -> State:
	return null

func process_physics(delta: float) -> State:
	return null
extends Node

@export
var starting_state: State

var current_state: State

# Initialize the state machine by giving each child state a reference to the
# parent object it belongs to and enter the default starting_state.
func init(parent: Player, camera: Camera3D) -> void:
	for child in get_children():
		child.parent = parent
		child.camera = camera

	# Initialize to the default state
	change_state(starting_state)

# Change to the new state by first calling any exit logic on the current state.
func change_state(new_state: State) -> void:
	if current_state:
		current_state.exit()

	current_state = new_state
	current_state.enter()
	
# Pass through functions for the Player to call,
# handling state changes as needed.
func process_physics(delta: float) -> void:
	var new_state = current_state.process_physics(delta)
	if new_state:
		change_state(new_state)

func process_input(event: InputEvent) -> void:
	var new_state = current_state.process_input(event)
	if new_state:
		change_state(new_state)

func process_frame(delta: float) -> void:
	var new_state = current_state.process_frame(delta)
	if new_state:
		change_state(new_state)

I’ve been doing this, by TheShaggyDev tutorials, but I saw some state-machines have signal related stuff like:

class_name State
extends Node
signal transition(_new_state_name: StringName)
func enter() -> void:
	pass
func exit() -> void:
	pass
func update(_delta: float) -> void:
	pass
func update_physics(_delta: float) -> void:
	pass

" signal transition(_new_state_name: StringName) " Would using signals be more appropriate? what difference would it bring? what edge?

If I have a few types of nodes, a couple called toaster with a function called “power_on”, and another node called outlet with the function called “turn_on_all_toasters” which calls “power_on” for every toaster in the scene. If I was using functions I would have to have a variable containing all the toasters so I can call the “power_on” function on each. If I used the signal instead, each toaster would have to have access to the outlet. You would connect the “power_on” function to the “turn_on_all_toasters” signal, and you would be able to “turn on all the toasters” without knowing which ones specifically inside the “outlet” script.

tl;dr Signals can be received with only access to the state machine in each node, if you want to call a function you must do it for each individual node.

It’s up to you to decide what works best for your situation.

So there is no single way to do state machines, and different people like to use them in different ways.

Either way is fine. Using direct calls to the state machine functions or by emitting a signals to the state machine.

For instance, I personally would not include the input management in my state machine directly. However others do. I suppose it all depends on your tree structures and usage etc.

I know this is not the answer you may have been looking for, but it is true. Without the context of your entire game setup, how you do your particular state machine is genuinely up to you. Yes there are more flexible approaches, or more efficient approaches, but it really all depends on how you are planning to use the state machine and your game setup. Sorry if this is a non-definitive answer. Hope it helped a bit.

4 Likes