Player Finite State Machine: Exit one state and enter another mid-animation?

Godot Version

4.3 (stable)

Question

Hi all, I’m implementing a 2D game and for my player I’m using a finite state machine. I have states for idle, walk, attack, stun and death. The asset pack I’m using has a sprite sheet for attacks and a sprite sheet for all the other states, so I’m using two Sprite2D nodes for this. I am using one AnimationPlayer node to switch the sprites off and on, depending on the animation.

I’ve noticed an issue where if I get stunned by an enemy mid-attack, the attack state doesn’t seem to exit correctly to move to the stun state, which causes the attack animation to “freeze” and leaves the attack sprite frozen on top of the other sprite sheet.

To play animations I’m calling the play method on the animation player.

Here’s my player attack script:

class_name State_Attack extends Player_State

@onready var audio: AudioStreamPlayer2D = $"../../Audio/AudioStreamPlayer2D"
@onready var walk: Player_State = $"../Walk"
@onready var idle: Player_State = $"../Idle"
@onready var hurt_box: HurtBox = %AttackHurtBox
@export var attack_sound: AudioStream
@export_range(1, 20, 0.5) var decelerate_speed: float = 5.0

var is_attacking: bool = false

func enter() -> void:
       # Calls animation_player.play()
	player.update_animation("attack")
	is_attacking = true
	player.animation_player.animation_finished.connect(end_attack)
	
	audio.stream = attack_sound
	audio.pitch_scale = randf_range(1, 1.3)
	audio.play()
	
	await get_tree().create_timer(0.075).timeout
	if is_attacking:
		hurt_box.monitoring = true
	pass
	
func exit() -> void:
	is_attacking = false
	hurt_box.monitoring = false
	print("exit player attack")
	player.attack_sprites.visible = false
	player.sprite.visible = true
	# connect to the animation player
	player.animation_player.animation_finished.disconnect(end_attack)
	pass
	
func process(_delta: float) -> Player_State:
	player.velocity -= player.velocity * decelerate_speed * _delta
	
	if is_attacking == false:
		if player.direction == Vector2.ZERO:
			return idle
		else:
			return walk
	return null
	
func physics(_delta: float) -> Player_State:
	return null
	
func handle_input(_event: InputEvent) -> Player_State:
	return null
	
func end_attack(_new_anim_name: String) -> void:
	print("sometimes this is never called!")
	is_attacking = false
	pass

And here’s my player stun script:

class_name State_Stun extends Player_State

@export var knockback_speed: float = 25.0
@export var decelerate_speed: float = 20.0
@export var invulnerable_duration: float = 1.0

var hurt_box: HurtBox
var direction: Vector2
var next_state: Player_State = null

@onready var idle: Player_State = $"../Idle"
@onready var death: Player_State = $"../Death"

func init() -> void:
	player.player_damaged.connect(_player_damaged)

func enter() -> void:
	next_state = null
	await player.animation_player.animation_finished.connect(_animation_finished)
	
	direction = player.global_position.direction_to(hurt_box.global_position)
	player.velocity = direction * -knockback_speed
	
	player.set_direction()
	
	player.update_animation("stun")
	player.make_invulnerable(invulnerable_duration)
	pass
	
func exit() -> void:
	player.animation_player.animation_finished.disconnect(_animation_finished)
	pass
	
func process(_delta: float) -> Player_State:
	player.velocity -= player.velocity * decelerate_speed * _delta
	return next_state
	
func physics(_delta: float) -> Player_State:
	return null
	
func _player_damaged(_hurt_box: HurtBox) -> void:
	hurt_box = _hurt_box
	player_state_machine.change_state(self)
	pass
	
func _animation_finished(_a: String) -> void:
	next_state = idle
	if player.hit_points <= 0:
		next_state = death
	pass

in your stun script’s enter() you have to add a line to stop the previous attack animation. I think player.current_animation.stop() should do the trick.