States not properly transitioning from one to the other

Godot Version

4.2

Question

I recently learned how to use states, which has been super helpful, and I’ve been trying to implement them in one of the enemies in my 2D platformer. It’s a stationary worm enemy that comes out of the ground and attacks the player when they’re near, which will either lead to a cooldown state or a parried state if the player parries it. All of these states work and transition from one to the next perfectly except for the “parried” state.

This is the parried state code:

class_name WormParriedState

extends State

@export var animation_component: AnimationComponent
@export var actor: CharacterBody2D

@export var parry_timer: float

func enter():
	animation_component.play_unlooped_animation(actor.facing_direction, "parried")

func update(_delta):
	actor.parried = false
	await get_tree().create_timer(parry_timer).timeout
	transition.emit("WormCooldownState")

func exit():
	animation_component.play_unlooped_animation(actor.facing_direction, "recover")

And the code for the attack state that’s supposed to transition to the parried state:

class_name WormAttackState

extends State

@export var actor: CharacterBody2D
@export var animation_component: AnimationComponent
@export var attack_hitbox: CollisionShape2D
@export var attack: Area2D

func enter():
	if animation_component.animation.animation == "emerge":
		await animation_component.animation.animation_finished
	attack.scale.x = actor.facing_direction
	animation_component.play_unlooped_animation(actor.facing_direction, "attack")

func update(delta):
	if animation_component.animation.animation == "attack":
		if animation_component.animation.frame == 3:
			attack_hitbox.set_deferred("disabled", false)
		if animation_component.animation.frame == 5:
			attack_hitbox.set_deferred("disabled", true)
			transition.emit("WormCooldownState")
	if actor.parried:
		animation_component.stop_animation()
		transition.emit("WormParriedState")

func exit():
	attack_hitbox.set_deferred("disabled", true)

The way that it’s supposed to work is that the state machine iterates from the attack state to the cooldown state to the active state, which then checks if the player is near or not, and determines based on that if it should transition to the idle state or back to the attack state. If the enemy gets parried in the attack state, it instead transitions to the parried state, where it’s supposed to play an animation for a second before returning to the cooldown state, thus (in theory) returning it to the loop.

The problem is that the first iteration of the loop after the enemy is parried becomes somewhat broken for some reason; it plays the attack animation but doesn’t enable the attack hitbox, and if the player leaves its before the first loop finishes it’ll play its animation for returning into the ground twice, and not deactivate its hitbox, so the player can still take damage from it. If the player doesn’t leave its range before the first loop finishes, everything returns to normal for the following loops.

I’ve tried a hundred different changes to make it work, from adjusting the if statement at the beginning of the attack state to having the parried state transition to the active state, or directly to the attack state. These changes have all either had no effect or made it worse. Please help!

First of all, it would help if we could see how your state machine works. Second, is your update function called like a process function? If yes, you shouldnt use the “await” keyword inside of it. especially if you create a timer inside of it. Third, you are setting the hitbox disabled in code, so im assuming you are using a animatedsprite2D? If you are using an animationplayer you can just set the hitbox disabled in the animation itself with keyframes. All in all helping you will only be possible if we can see how your states and state machine work

I actually just managed to get it working by using await in the exit function, but I’m not entirely sure what fixed it lol. If you’re curious, this is the code for my state machine:

class_name StateMachine

extends Node

@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)
	if new_state != null:
		if new_state != current_state:
			current_state.exit()
			new_state.enter()
			current_state = new_state
	else:
		push_warning("Called transition on a state that does not exist")

as you can see, it does call update in the process function. Why shouldn’t you use await in process? And for your final question, I am using an animatedsprite2D, I know I probably should switch to using an animationplayer but I haven’t gotten around to learning how it works yet.

the process function gets called 60 times per second → you have several runs of your update-Functions running at the same time because they all wait for the await and in your code its even worse since they all create a timer for themselves → using this with an await for 1 seconds leads to 60 runs of the process-function all creating a timer and after the timer they all emit your signal causing the signal to emit 60 times technically, if im correct

1 Like