Question about migrating the architecture of state-machines

Question

Hi everyone,

Rather of attempting to transfer the complete design at once, I chose to begin the migration with the most basic component of my state machine system.

Replicating the state machine architecture from a functional “sample” project into a new “target” project is my aim. I’m attempting to replicate the structure of the states, handlers, and actions rather than the gameplay itself .

I am currently concentrating on the states of the player s.

StatePlayerBase.gd

extends StateBase
class_name StatePlayerBase

var player_handler:PlayerStateHandler
var player:GameCharacter

func _init(state_handler:BaseStateHandler) -> void:
	super._init(state_handler)
	player_handler = state_handler as PlayerStateHandler
	player = player_handler.player
	SignalsManager.on_input.connect(on_input)

func on_input(_inputs_dict:Dictionary):
	pass
StatePlayerWalk.gd
extends StatePlayerBase
class_name StatePlayerWalk

var action_walk:ActionWalk
var in_sea:bool

func _init(state_handler:BaseStateHandler) -> void:
	super._init(state_handler)
	action_walk = ActionWalk.new(player)
	SignalsManager.on_entered_sea.connect(on_entered_sea)

func on_input(inputs_dict:Dictionary):
	if handler.current_state == self:
		action_walk.execute(inputs_dict, delta)

func on_entered_sea(body:CharacterBody2D):
	if body == player:
		in_sea = true

func enter_state():
	super.enter_state()
	in_sea = false

func do_state_body():
	super.do_state_body()
	if in_sea:
		return_state = player_handler.state_player_swim

StatePlayerWalk.gd

extends StatePlayerBase
class_name StatePlayerWalk

var action_walk:ActionWalk
var in_sea:bool

func _init(state_handler:BaseStateHandler) -> void:
	super._init(state_handler)
	action_walk = ActionWalk.new(player)
	SignalsManager.on_entered_sea.connect(on_entered_sea)

func on_input(inputs_dict:Dictionary):
	if handler.current_state == self:
		action_walk.execute(inputs_dict, delta)

func on_entered_sea(body:CharacterBody2D):
	if body == player:
		in_sea = true

func enter_state():
	super.enter_state()
	in_sea = false

func do_state_body():
	super.do_state_body()
	if in_sea:
		return_state = player_handler.state_player_swim


StatePlayerSwim.gd

extends StatePlayerBase
class_name StatePlayerSwim

var action_swim:ActionSwim
var outside_sea:bool

func _init(state_handler:BaseStateHandler) -> void:
	super._init(state_handler)
	action_swim = ActionSwim.new(player)
	SignalsManager.on_exited_sea.connect(on_exited_sea)

func on_input(inputs_dict:Dictionary):
	if handler.current_state == self:
		action_swim.execute(inputs_dict, delta)

func on_exited_sea(body:CharacterBody2D):
	if body == player:
		outside_sea = true

func enter_state():
	super.enter_state()
	outside_sea = false

func do_state_body():
	super.do_state_body()
	if outside_sea:
		return_state = player_handler.state_player_walk


How the States Work

The player starts in a walking state.

When the player enters a sea area:

return_state = player_handler.state_player_swim

the state changes from Walk to Swim.

When the player exits the sea area:

return_state = player_handler.state_player_walk

the state changes back from Swim to Walk.

Input is received through a global signal system (SignalsManager) and each state executes its corresponding action (ActionWalk or ActionSwim) only when it is the current active state.

Missing Piece

I suspect the most important file for understanding the architecture is PlayerStateHandler.gd, because it creates the states, stores references to them, and defines the initial state. I can also post that file if needed.

My Questions

1. Do you identify any architectural flaws in these state files that would make it difficult to transfer them to another project?
2. To have a better understanding of the state machine’s construction, would you need to view PlayerStateHandler.gd?

Any guidance would be appreciated.

Best Regards,

Nick

A bunch of things stand out to me.

  1. Instead of just giving us the BaseState code, you described it.
  2. Same with PlayerStateHandler - which sounds like a Manager with another name, and is most likely an anti-pattern. What is the difference between a state handler and a state machine in your mind?
  3. Your SignalsManager is a Manager Anti-Pattern. They tend to break down when things get more complex. You can search the forum for discussions on that.
  4. Adding Base to a name isn’t necessary. It makes polymorphism implementations more wordy to no benefit.
  5. Godot naming convention is to put the base class at the end of the class name, not the beginning. It’s tempting to reverse it to be able to find files, but that’s what folders are for.
  6. When calling super() you do not have to indicate the function name, as you are already in that function.

Here’s a re-write and comments on your first file.

StatePlayerBase.gd

# Make this an abstract class, as that is how you are using it.
@abstract
class_name PlayerState extends State

var player_handler: PlayerStateHandler
var player: GameCharacter


# This should all be in the _ready() function, not
# _init(). You're less likely to run into race
# conditions that way.
func _init(state_handler: BaseStateHandler) -> void:
	super(state_handler)
	# You've already declared this as type
	# PlayerStateHandler, assigning it is casting it,
	# you don't need to explicitly cast it. One of the
	# benefits of polymorphism.
	player_handler = state_handler
	player = player_handler.player
	# Use private functions for signal handling
	# functions.
	SignalsManager.on_input.connect(_on_input)


# Make this an abstract class function.
# Then you don't need "pass".
@abstract
func on_input(_inputs_dict: Dictionary)

StatePlayerWalk.gd

class_name WalkPlayerState extends PlayerState

var action_walk: ActionWalk
var in_sea: bool


func _init(state_handler:BaseStateHandler) -> void:
	super._init(state_handler)
	action_walk = ActionWalk.new(player)
	SignalsManager.on_entered_sea.connect(on_entered_sea)


func _on_input(inputs_dict:Dictionary):
	if handler.current_state == self:
		action_walk.execute(inputs_dict, delta)


# You don't need to pass an argument of the body here.
# You should be checking whether or not this is player-related somewhere higher up.
func _on_entered_sea(_body:CharacterBody2D):
	in_sea = true


func enter_state():
	super()
	# This should probably be set by an
	# _on_exited_sea() function.
	in_sea = false


func do_state_body():
	super()
	if in_sea:
		return_state = player_handler.state_player_swim

Conclusion

There’s a lot of obfuscation in your code. I’m not sure why. You might consider following another tutorial on state machines.