Problems with AnimationTree handling state set by an animation's call method track

Godot Version: 4.4

Hello everyone, I’m new to Godot and gamedev in general and right when I found I was very close to understanding AnimationStateMachines, I found some huge trouble.

I’ll attach a screenshot of my AnimationTree and do my best to explain what’s happening.

My goal: The player has two attack animations and it always starts with attack1, but if another attack is queued before the recovery animation ends, it will use the attack2 animation instead
When I left click I set a variable called is_attacking to true. This causes the StateMachine to switch from idle to attack1.
At the end of the attack1 animation, an animation track calls a method that sets is_attacking to false. attack1 always progresses unconditionally to attack_recovery_buffer, and since is_attacking is now false, the player can click again to set it back to true.
attack_recovery_buffer always progresses unconditionally to attack_recovery; from here it progresses immediately to attack2 if is_attacking == true (priority 0), otherwise it progresses to idle (priority 1).
From here the tree behaves the exact same as the previous part, except after recovery it goes to attack1 (instead of attack2) if the player attacked again.

Now here’s my issue: despite the fact that is_attacking seems to follow the intended state flow, my AnimationTree is not behaving as expected. When it reaches attack_recovery it goes to idle and then immediately to attack1, staying in a loop forever, indicating that is_attacking is still true. However, if is_attacking was true, attack_recovery should have progressed to attack2.
If I put the condition !is_attacking on the transition between attack_recovery and idle, the animation gets stuck on the last frame of attack_recovery, indicating that it can’t progress to attack2 (which requires is_attacking to be true) nor to idle (which requires is_attacking to be false, in this case). Given boolean variables only have two possible values, I guess this is a problem with how/when the StateMachine evaluates switch conditions, but I can’t for the life of me figure out what the problem is precisely.

This is the entirety of the code that handles my player movement:

extends Node2D

@export var player: CharacterBody2D

@export var speed: float = 8.0
@export var jump_power: float = 12

var speed_multiplier: float = 30.0
var jump_multiplier: float = -30.0
var direction: float = 0.0
var friction: float = 3.0

var is_attacking: bool = false

func _input(event: InputEvent) -> void:
	if event.is_action_pressed('attack'):
		is_attacking = true
	elif event.is_action_pressed('jump') and player.is_on_floor():
		player.velocity.y = jump_power * jump_multiplier
		

func _physics_process(delta: float) -> void:
	if not player.is_on_floor():
		player.velocity += player.get_gravity() * delta

	direction = Input.get_axis("move_left", "move_right")
	if direction:
		player.velocity.x = direction * speed * speed_multiplier
	else:
		player.velocity.x = move_toward(player.velocity.x, 0, speed * friction)

	player.move_and_slide()

func set_weapon_collision() -> void:
	# collision logic
	print('collision set')

func unset_weapon_collision() -> void:
	# collision logic
	print('collision unset')

func stop_attacking() -> void:
	is_attacking = false
	unset_weapon_collision()

The method called at the end of attack1/attack2 is stop_attacking. I tried putting the call anywhere, but the result doesn’t change.
Thanks in advance.

Did you set the AnimationTree.advance_expression_base_node to the correct node (the one that has the script attached)?

Did you set the “Condition” or the “Expression” inside the AnimationTree transition? “Condition” must be set through the AnimationTree’s properties i.e.

$AnimationTree.set("parameters/conditions/is_attacking", true)

“Expression” will run as code and automatically transition if true.

Yes, I’m fairly confident this is not the issue as the StateMachine can at least read is_attacking in order to switch from idle to attack1.

I have tried both, neither works

Hey there , I actually have something similar in my project, although I go about it another way.

Here is an excerpt of the code from the attack state in my players state machine:

# checks for input events
#if melee1 is pressed it starts the attack timer which is 0.5 seconds long
func state_input(event : InputEvent): #checks input events
	if (event.is_action_pressed("Melee1")):
		timer.start() #starts attack timer
	if (event.is_action_pressed("Dash")):
		Dash()

#-------------------------------------------
#after starting the attack animation the animation finished checks if timer is stopped
#if the timer is stopped the state machine returns to the return state (ground or air)
#else it proceeds to the next attack animation

# (if the player keeps starting the timer by pressing melee 1 the animations will keep proceeding to the next attack animation) 

func _on_animation_tree_animation_finished(anim_name):
	if (anim_name == Attack1_animation):
		if(timer.is_stopped()):
			next_state = return_state
			playback.travel(return_animation_node)
		else:
			playback.travel(Attack2_node)
	if (anim_name== Attack2_animation):
		if(timer.is_stopped()):
			next_state = return_state
			playback.travel(return_animation_node)
		else:
			playback.travel(Attack3_node)
	if (anim_name == Attack3_animation):
		next_state = return_state
		playback.travel(return_animation_node)

I think this is a solution that could work for your intended purposes.

All suggestions I had for fixing your specific issue has been suggested by the other users :frowning:

Turns out the problem was super simple: is_attacking is a property of the player node, while the script is attached to a child; I was checking is_attacking in the child node instead of the player node, I still don’t understand why it would transition from idle to attack1 but now that I’m checking player.is_attacking it’s working as intended. Sorry for wasting your time, I should have added this detail.

1 Like

Nobody is perfect and this won’t be the last time you forget to add a small (and sometimes seemingly insignificant detail) or you forget to check a button in your code or something small and stupid. (god knows I have made many small and easy to fix mistakes that took way too long to fix)
It’s a learning process and you aren’t wasting anybodys time :slight_smile: