Godot Version
Godot v4.4.1.stable - Windows 10 (build 19045) - Multi-window, 2 monitors - Vulkan (Forward+) - dedicated AMD Radeon RX 5600 XT (Advanced Micro Devices, Inc.; 32.0.21025.1024) - AMD Ryzen 9 3900X 12-Core Processor (24 threads)
Prefix
I’ve been having issues with the AnimationTree recently - trying to trigger a separate state machine to revert back to the idle state after the roll animation finished.
Sounds pretty straightforward, but it’s forced me to look into possible issues with the Animation Tree.
My Original Problem
Before I get into the issues, here’s the general outline I want:
- Player is in idle state
- Player inputs a ‘roll’, moving the state machine to the roll state
- The roll state waits until the roll animation is finished before moving back to the idle state (I would like to trigger this by connecting to the
animation_finished
signal on the AnimationMixer class)
The problem, is that this is VERY inconsistent, and will get ‘stuck’ in the roll state about 5% of the time.
This lead me to revert back to idle based on a timer instead, but I’ve now encountered some other timing issues with that system as well, which makes me want to just use the signal since it makes more since and keeps things tight.
Test Outline
So I made a simple project with the goal of testing the animation_finished
signal, and I found some very concerning info:
Very briefly, my setup has a Timer node, and an AnimationTree node.
The Timer is on an automatic0.1
second timeout interval, and the AnimationTree node has basically the same setup, but is instead using 2 separate animations; each with a length of0.1
.
The idea is that thetimeout
signal and theanimation_finished
signal should happen at the same interval, and in turn, happen the same number of times.
News flash, this is far from the truth lol
Test Results
- 100 cycles: Test Completed in 10.0085865555556 seconds:
- AnimationPlayer finished 85 times, with an average wait time of 0.11715984183007 (total time was: 9.95858655555555)
- Timer finished 100 times, with an average wait time of 0.10008586555556 (total time was: 10.0085865555555)
- 500 cycles: Test Completed in 50.0056820000008 seconds:
- AnimationPlayer finished 426 times, with an average wait time of 0.11734510798122 (total time was: 49.9890159999998)
- Timer finished 500 times, with an average wait time of 0.100011364 (total time was: 50.0056819999997)
- 1000 cycles: Test Completed in 100.015247666666 seconds:
- AnimationPlayer finished 851 times, with an average wait time of 0.11744839130435 (total time was: 99.9485810000013)
- Timer finished 1000 times, with an average wait time of 0.10001524766667 (total time was: 100.015247666668)
In general, it looks like the AnimationPlayer only fires it’s
animation_finished
signal at around 85% the rate of the Timer, which is a pretty large difference imo…
NOTE: I also got similar results when using different animation/timer lengths.
My Questions
- Am I missing something on the docs? Is the AnimationTree simply slower, leading to this? If not, I’m kind of thinking that the
animation_finished
signal could be firing at the correct rate, but maybe it’s just “missing” or not being triggered at all at times…? This would explain the “getting stuck in the roll state” that I mentioned earlier.
My Code (lmk if I did something wrong with this test)
Here’s my code if you care to read through it and check everything, but I think it’s all pretty straightforward.
extends Node2D
@export var timer: Timer
@export var animation_tree: AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback = animation_tree["parameters/playback"]
var anim_finished_counter: int = 0: set = _on_anim_finished_counter_set
var timer_finished_counter: int = 0: set = _on_timer_finished_counter_set
var anim_delta: float = 0.0
var timer_delta: float = 0.0
var anim_times: Array[float] = []
var timer_times: Array[float] = []
var test_count: int = 1000
var total_time_taken: float = 0.0
func _ready() -> void:
animation_tree.animation_finished.connect(func(name: String):
anim_finished_counter += 1)
timer.timeout.connect(func():
timer_finished_counter += 1)
func _process(delta: float) -> void:
anim_delta += delta
timer_delta += delta
if anim_finished_counter == test_count or timer_finished_counter == test_count:
print("Test Completed in %s seconds:
- AnimationPlayer finished %s times, with an average wait time of %s (total time was: %s)
- Timer finished %s times, with an average wait time of %s (total time was: %s)" %
[total_time_taken,
anim_finished_counter, get_average(anim_times), get_average(anim_times) * anim_finished_counter,
timer_finished_counter, get_average(timer_times), get_average(timer_times) * timer_finished_counter])
get_tree().paused = true
total_time_taken += delta
func get_average(numbers: Array) -> float:
var sum: float = 0.0
for n in numbers:
sum += n
return sum / numbers.size()
func _on_anim_finished_counter_set(new_value: int) -> void:
anim_finished_counter = new_value
anim_times.append(anim_delta)
anim_delta = 0
func _on_timer_finished_counter_set(new_value: int) -> void:
timer_finished_counter = new_value
timer_times.append(timer_delta)
timer_delta = 0
Finally, I may be over-engineering my state machine, so I’d also love if anyone has any input on ways to make it simpler (maybe I’m overthinking it?).
I do need it to work with different behavior trees though, so I’m pretty sure it needs to stay the way it is.
Thanks for helping if you’ve made it this far!
EDIT: yes the code is inneficient, but I’m tired and just trying to figure this bug out