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