Skip to content

Instantly share code, notes, and snippets.

@firebelley
Created August 27, 2024 21:18
Show Gist options
  • Save firebelley/96f2f82e3feaa2756fe647d8b9843174 to your computer and use it in GitHub Desktop.
Save firebelley/96f2f82e3feaa2756fe647d8b9843174 to your computer and use it in GitHub Desktop.
class_name CallableStateMachine
var state_dictionary = {}
var current_state: String
func add_states(
normal_state_callable: Callable,
enter_state_callable: Callable,
leave_state_callable: Callable
):
state_dictionary[normal_state_callable.get_method()] = {
"normal": normal_state_callable,
"enter": enter_state_callable,
"leave": leave_state_callable
}
func set_initial_state(state_callable: Callable):
var state_name = state_callable.get_method()
if state_dictionary.has(state_name):
_set_state(state_name)
else:
push_warning("No state with name " + state_name)
func update():
if current_state != null:
(state_dictionary[current_state].normal as Callable).call()
func change_state(state_callable: Callable):
var state_name = state_callable.get_method()
if state_dictionary.has(state_name):
_set_state.call_deferred(state_name)
else:
push_warning("No state with name " + state_name)
func _set_state(state_name: String):
if current_state:
var leave_callable = state_dictionary[current_state].leave as Callable
if !leave_callable.is_null():
leave_callable.call()
current_state = state_name
var enter_callable = state_dictionary[current_state].enter as Callable
if !enter_callable.is_null():
enter_callable.call()
@firebelley
Copy link
Author

firebelley commented Aug 27, 2024

Use like so:

var state_machine: CallableStateMachine = CallableStateMachine.new()


func _ready():
	state_machine.add_states(state_normal, enter_state_normal, Callable())
	state_machine.add_states(state_abnormal, Callable(), Callable())
	state_machine.set_initial_state(state_normal)


func _process(_delta):
	state_machine.update()


func enter_state_normal():
	print("entering state normal")


func state_normal():
	print("doing state normal")
  	if some_condition:
		state_machine.change_state(state_abnormal)


func leave_state_normal():
	# empty, will not be called unless added to the state machine as the third argument to state_machine.add_states
	pass


func state_abnormal():
	print("in state abnormal")

@Snafuey
Copy link

Snafuey commented May 31, 2025

Can this state machine support binding arguments to a certain state?

@mbnewsom
Copy link

mbnewsom commented Jun 1, 2025

I'm new to Godot / GDScript. Coming from C# and Typescript typing is something I hold in high regard, and find some frustrations in GDScripts handling of it. Just tried a small rewrite of this to remove the need for the as Callable casts, and here's what i came up with.

# honestly just using this class to get around 'no nested typed containers' error. 
# Curious if there's a cleaner / less bulky way to do this
class_name State
extends RefCounted

var normal: Callable
var enter: Callable
var leave: Callable

func _init(normal: Callable, enter: Callable, leave: Callable):
	self.normal = normal
	self.enter = normal
	self.leave = normal

class_name CallableStateMachine

var state_dictionary: Dictionary[String, State] = {}
var current_state: String

func add_state(
	normal_state_callable: Callable,
	enter_state_callable: Callable,
	leave_state_callable: Callable
):
	state_dictionary[normal_state_callable.get_method()] = \
			State.new(normal_state_callable, enter_state_callable, leave_state_callable)

func set_initial_state(state_callable: Callable):
	var state_name = state_callable.get_method()
	if state_dictionary.has(state_name):
		_set_state(state_name)
	else:
		push_warning("No state with name " + state_name)


func update():
	if current_state != "":  #null here was a bug. Strings default to "", not null
		state_dictionary[current_state].normal.call()


func change_state(state_callable: Callable):
	var state_name = state_callable.get_method()
	if state_dictionary.has(state_name):
		_set_state.call_deferred(state_name)
	else:
		push_warning("No state with name " + state_name)


func _set_state(state_name: String):
	if current_state:
		var leave_callable = state_dictionary[current_state].leave
		if leave_callable.is_valid():
			leave_callable.call()
	
	current_state = state_name
	var enter_callable = state_dictionary[current_state].enter
	if enter_callable.is_valid():
		enter_callable.call()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment