I'd like to hear your opinion on this. Every tutorial about statemachines I saw used nodes for each state.

I didn't like adding nodes for each state and decided to derive the state from RefCounted.
According to the docs those get automatically freed once there is no reference to them.
I tested it by adding a print to the function that is being called (forgot its name) before the state gets freed from memory.
And it worked as expected.

I like that I don't have to use nodes and have some type safety because the switch functions needs a PlayerState. No possible typos when passing the statename as string.

Downsides are:

  • Can't use export var (not sure if I need them)
  • Can cause memory leak if RefCounted doesn't get freed

Need help:
I'd prefer to cache the states I've used. So switching states doesn't create new instances and therefore eliminate memory leaks I guess?
But I haven't figured out if it is possible to pass a type (PlayerState) as argument in the change_state function, then check a dictionary or array if an instance of this type already exists and then create a new one or use the existing one.
The next problem is that when checking any State derived from PlayerState with is will return PlayerState and not PlayerMoveState for example. So I wouldn't be able to find existing states of that type in a dict.

Do you have any ideas or suggestions? 😒

EDIT: Also does it make sense to use the root node (CharacterBody3D) as statemachine? All tutorials added a childnode to function as statemachine. I don't see why.

Base State:

class_name PlayerState extends RefCounted

var player : CharacterBody3D

func _init(player_instance : CharacterBody3D) -> void:
	player = player_instance

func enter():
	pass
	
func update(_delta: float):
	pass
	
func physics_update(_delta: float):
	pass
	
func exit():
	pass

Player (StateMachine)

class_name Player extends CharacterBody3D

var current_state : PlayerState

func _ready() -> void:
	current_state = PlayerMoveState.new(self)
	current_state.enter()

func _process(delta : float):
	current_state.update(delta)

func _physics_process(delta: float) -> void:
	current_state.physics_update(delta)

func change_state(new_state: PlayerState):
	if new_state != null:
		if new_state != current_state:
			if current_state != null:
				current_state.exit()
			new_state.enter()
			current_state = new_state
	else:
		push_error("State is null!")
  • xyz replied to this.

    xyz Okay, this looks really great. I will experiement with that. Thank you! 🙂

    Well, I ended up using nodes for now. But I restructured them so I don't have so many states.

    One other thing I tried was to load all state scripts from a folder and store their instances in a dictionary. That worked too.
    But somehow everything felt just not right. The node way doesn't feel right as well, but somehow more comprehensible. 😐

    • xyz replied to this.

      trizZzle Why just not use callables in the same script? Why do you need each state to be an object (either Node or RefCounted) or worse - a class? After all, each state is just a different configuration of the same object's data. Why introduce a bunch of superfluous objects to the system? It goes against the principle of Occam's razor 🙂

        xyz I guess it's personal taste and I'm still figuring some things out.
        Occam's razor? Never heard of that before. Sounds way cooler than KISS. 😃

        • xyz replied to this.

          trizZzle Sounds way cooler than KISS

          Not only cooler but the razor precedes kiss by at least 7 centuries.