Is it normal for a state machine to keep running the enter function of an old state?

Godot Version

4.2.2

Question

This is my state machine:

class_name StateMachine

extends Node

@export var animation: AnimationPlayer
@export var current_state: State
var states: Dictionary = {}

func _ready():
	for i in get_children():
		if i is State:
			states[i.name] = i
			i.transition.connect(_on_transition)
		else:
			push_warning("State machine contains child which is not 'State'")
	
	current_state.enter()

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

func _physics_process(delta):
	current_state.physics_update(delta)

func _on_transition(new_state_name: String):
	var new_state = states.get(new_state_name)
	print("old state: " + str(current_state) + "new state: " + str(new_state))
	if new_state != null:
		if new_state != current_state:
			current_state.exit()
			if animation.is_playing():
				await animation.animation_finished
			new_state.enter()
			current_state = new_state
	else:
		push_warning("Called transition on a state that does not exist")

I basically copied it from a tutorial with a few tweaks to make it fit my specific needs, and for the most part it works quite well. The only issue I’ve run into with the state machine itself is that, if the condition for the state to transition is met before the enter function finishes, the old state’s enter function will keep running until it finishes at the same time as the new state’s code.

This usually doesn’t cause a problem, except for in the specific circumstance where I want to be able to transition to a new state in either the enter function or in the update function, depending on what condition is met. This is most often the case when I want an enemy to transition to a new state either after a certain amount of time or if the player does something. For example, in this state:

func enter():
	animation.play("Staggered")
	if actor.direction == 1:
		riposte.position.x = 31
	else:
		riposte.position.x = -31
	await get_tree().create_timer(stagger_timer).timeout
	transition.emit("MinotaurIdleState")

func update(delta):
	velocity_component.apply_friction(delta)
	if actor.riposte_ready:
		arrow.show()
		if Input.is_action_just_pressed("attack"):
			actor.handle_riposte()
			transition.emit("MinotaurIdleState")
	else:
		arrow.hide()

func exit():
	arrow.hide()
	animation.play("RESET")

As you can probably see, the problem here is that if the player ripostes the enemy it’ll transition to the idle state twice; once right away, and once when the enter code completes. I did figure out a workaround:

var ready_to_transition

func enter():
	ready_to_transition = false
	animation.play("Staggered")
	if actor.direction == 1:
		riposte.position.x = 31
	else:
		riposte.position.x = -31
	await get_tree().create_timer(stagger_timer).timeout
	ready_to_transition = true

func update(delta):
	velocity_component.apply_friction(delta)
	if actor.riposte_ready:
		arrow.show()
		if Input.is_action_just_pressed("attack"):
			actor.handle_riposte()
			transition.emit("MinotaurIdleState")
	else:
		arrow.hide()
	if ready_to_transition:
		transition.emit("MinotaurIdleState")

func exit():
	arrow.hide()
	animation.play("RESET")

but honestly it feels clunky to have to do this in every state that has this problem. Is there a better way to resolve the issue? Is it just a bad idea to use the await keyword in the enter function? And if so, how should I implement a state that automatically transitions after a set amount of time? I know it’s just as bad if not worse to use await in the update/process functions.

It would be nice if there was a way to just make the state machine stop or “return” the old state’s enter function when it transitions to the new state, but from what I’ve found online that doesn’t seem to be possible.

I’m not an expert, but I’m thinking yeah it’s a bad idea to use await in enter unless you’re extremely careful, but the real issue is probably changing state from within either enter or exit. As you’re pointing out, you’re creating race conditions by letting multiple threads change the state. There just has to be a cleaner way to implement it. I don’t think I’d ever have a state enter or exit function trigger a state change. It also should not be a problem to let a state transition to itself if you want to run the exit/enter loop and reset the state.

1 Like