I'm trying to animate an expandable staff by travel between nodes in AnimationTree
Like this:
...
tool
export(String,"small", "mid", "full") var staff_mode = "small" setget set_staff_mode;
func set_staff_mode(new_val):
var ani_state;
if(self.has_node("/path/to/AnimationTree")):
ani_state=self.get_node("/path/to/AnimationTree")["parameters/playback"];
ani_state.start(staff_mode);
print(ani_state.is_playing());
ani_state.travel(new_val);
ani_state.stop();
staff_mode=new_val;
I haven't applied autoplay to small
because I don't want a looping animation on the staff
(it only expands or compresses, no idle animation)
but for some reason it gives the error:
Can't travel to 'full' if state machine is not playing. Maybe you need to enable Autoplay on Load for one of the nodes in your state machine or call .start() first?
Edit: I forgot to mention but I don't have any idle animation for my staff so I need to stop the animation after the transition is complete.
small, mid & full
(all of them are static modes of the staff depending upon the game how much the staff should extend)
are all 0.1sec single frame animations and I applied Xfade Time of 0.2 secs to show the transition
I simply need to transition from an existing animation state to another and then stop
Apparently the solution on the old answer does not work for very short animations. And the workarounds begin to seem to much overhead for my taste. So, as alternative, let us get rid of the AnimationTree
and work directly with AnimationPlayer
. To do this, we will:
Now we can simply ask the AnimationPlayer
to play, something like this:
tool
extends Node2D
enum Modes {full, mid, small}
export(Modes) var staff_mode setget set_staff_mode
func set_staff_mode(new_val:int) -> void:
if staff_mode == new_val:
return
if not is_inside_tree():
return
var animation_player := get_node("AnimationPlayer") as AnimationPlayer
if not is_instance_valid(animation_player):
return
var target_animation:String = Modes.keys()[new_val]
animation_player.play(target_animation)
yield(animation_player, "animation_finished")
staff_mode = new_val
property_list_changed_notify()
I have opted to use an enum, because this will also allow me to "travel" between the animations. The idea is that we will make a for loop where we call the animations in order. Like this:
tool
extends Node2D
enum Modes {full, mid, small}
export(Modes) var staff_mode:int setget set_staff_mode
func set_staff_mode(new_val:int) -> void:
if staff_mode == new_val:
return
if not is_inside_tree():
return
var animation_player := get_node("AnimationPlayer") as AnimationPlayer
if not is_instance_valid(animation_player):
return
var old_val := staff_mode
staff_mode = new_val
var travel_direction = sign(new_val - old_val)
for mode in range(old_val, new_val + travel_direction, travel_direction):
var target_animation:String = Modes.keys()[mode]
animation_player.play(target_animation)
yield(animation_player, "animation_finished")
I have also decided to set staff_mode
early so I can avoid property_list_changed_notify
.
Concurrent calls may result in animation stopping early, since calling play
stops the currently playing animation to play the new one. However, I don't think waiting for the current animation to end is correct. Also with so short animations, it should not be a problem.
Using Tween
will give you finer control, but it is also more work, because we are going to encode the animations in code… Which I will be doing with interpolate_property
. Thankfully this is a fairly simple animation, so can manage without making the code too long.
Of course, you need to add a Tween
node. We will not use AnimationPlayer
nor AnimationTree
. Tween
will handle the interpolations (and you can even specify how to do the interpolations by adding the optional parameters of interpolate_property
, which I'm not passing here).
This is the code:
tool
extends Node2D
enum Modes {full, mid, small}
export(Modes) var staff_mode:int setget set_staff_mode
func set_staff_mode(new_val:int) -> void:
if staff_mode == new_val:
return
if not is_inside_tree():
return
var tween := get_node("Tween") as Tween
if not is_instance_valid(tween):
return
var old_val := staff_mode
staff_mode = new_val
var travel_direction = sign(new_val - old_val)
for mode in range(old_val, new_val + travel_direction, travel_direction):
match mode:
Modes.full:
tween.interpolate_property($"1", "position", $"1".position, Vector2.ZERO, 0.2)
tween.interpolate_property($"1/2", "position", $"1/2".position, Vector2(0, -35), 0.2)
tween.interpolate_property($"1/2/3", "position", $"1/2/3".position, Vector2(0, -34), 0.2)
Modes.mid:
tween.interpolate_property($"1", "position", $"1".position, Vector2.ZERO, 0.2)
tween.interpolate_property($"1/2", "position", $"1/2".position, Vector2(0, -35), 0.2)
tween.interpolate_property($"1/2/3", "position", $"1/2/3".position, Vector2.ZERO, 0.2)
Modes.small:
tween.interpolate_property($"1", "position", $"1".position, Vector2.ZERO, 0.2)
tween.interpolate_property($"1/2", "position", $"1/2".position, Vector2.ZERO, 0.2)
tween.interpolate_property($"1/2/3", "position", $"1/2/3".position, Vector2.ZERO, 0.2)
tween.start()
yield(tween, "tween_all_completed")
What you can see here is that I have encoded the values from the tracks of from the AnimationPlayer
in the source code. Using Tween
I get to tell it to interpolate from whatever value the track has to the target position of each state.
I don't know if this performs better or worse compared to AnimationPlayer
.
Alright, there are two sides to this problem:
travel
is supposed to play the animation, so it is not instantaneous. Thus, if you call stop
it will not be able to travel, and you get the error message you got.
Ah, but you cannot call start and travel back to back either. You need to wait the animation to start.
I'll start by not going from one state to the same:
func set_staff_mode(new_val:String) -> void:
if staff_mode == new_val:
return
We are going to need to get the AnimationTree
, so we need to ge in the scene tree. Here I use yield
so the method returns, and Godot resumes the execute after it gets the "tree_entered"
signal:
if not is_inside_tree():
yield(self, "tree_entered")
The drawback of yield
, is that it can cause an error if the Node
is free before you get the signal. Thus, if you prefer to not use yield
, we can do this instead:
if not is_inside_tree():
# warning-ignore:return_value_discarded
connect("tree_entered", self, "set_staff_mode", [new_val], CONNECT_ONESHOT)
return
Here CONNECT_ONESHOT
ensures this signal is automatically disconnected. Also, Godot makes sure to disconnect any signals when freeing a Node
so this does not have the same issue as yield
. However, unlike yield
it will not start in the middle of the method, instead it will call the method again.
Alright, we get the AnimationTree
:
var animation_tree := get_node("/path/to/AnimationTree")
if not is_instance_valid(animation_tree):
return
And get the AnimationNodeStateMachinePlayback
:
var ani_state:AnimationNodeStateMachinePlayback = animation_tree.get("parameters/playback")
Now, if it is not playing, we need to make it playing:
if not ani_state.is_playing():
ani_state.start(new_val)
And now the problem: we need to wait for the animation to start.
In lieu of a better solution, we going to pool for it:
while not ani_state.is_playing():
yield(get_tree(), "idle_frame")
Previously I was suggesting to get the AnimationPlayer
so we can wait for "animation_started"
, but that does not work.
Finally, now that we know it is playing, we can use travel
, and update the state:
ani_state.travel(new_val)
staff_mode = new_val
Don't call stop
.
You might also want to call property_list_changed_notify()
at the end, so Godot reads the new value of staff_mode
, which it might not have registered because we didn't change it right away (instead we yielded before changing it). I suppose you could alternatively change the value earlier, before any yield
.
By the way, if you want the mid
animation to complete in the travel
, change the connections going out of mid
in the AnimationTree
from "Immidiate" to "AtEnd".
Addendum on waiting for travel to end and stop
We can spin wait in a similar fashion as we did to wait for the AnimationNodeStateMachinePlayback
to start playing. This time we need to pool two things:
As long as the animation is not in the final state and as long as it has not reached the end of that animation, we let one frame pass and check again. Like this:
while (
ani_state.get_current_node() != new_val
or ani_state.get_current_play_position() < ani_state.get_current_length()
):
yield(get_tree(), "idle_frame")
Then you can call stop
.
Furthermore, I'll add a check for is_playing
. The reason is that this code is waiting for the AnimationTree
to complete the state we told it to… But if you call travel again before it finished, it will go to new destination, and thus might never reach the state we expected, which result in spin waiting for ever.
And since it might not have arrived to the state we expected, I decided to query the final state instead of setting staff_mode
to new_val
. That part of the code now looks like this:
ani_state.travel(new_val)
while (
ani_state.is_playing() and (
ani_state.get_current_node() != new_val
or ani_state.get_current_play_position()
< ani_state.get_current_length()
)
):
yield(get_tree(), "idle_frame")
ani_state.stop()
staff_mode = ani_state.get_current_node()
property_list_changed_notify()