godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.12k stars 69 forks source link

AnimationTree -> AnimationNodeStateMachinePlayback: travel/start from a specific position #9175

Open artvel opened 6 months ago

artvel commented 6 months ago

Describe the problem or limitation you are having in your project

I am having an animation that is visualizing a magical cast. The animation is 5s+ looping in a casting position type and after that it is visualizing the execution of the cast.

I would like to take the execution time(5.26) and run the animation like:

var startTime:float = executionTime - castTime
player.travel("myAnim", true, startTime)

I don't see how I could solve this in a smarter way right now besides cutting the animations perfectly so it fits to every ability cast time. That would be a lot of initial and maintenance work.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Having the start position parameter enables us to have some animation types and just match the start position without creating the same kind of animation multiple times with different timings.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

void start(node: StringName, reset: bool = true, start_position:float = 0)
void travel(to_node: StringName, reset_on_teleport: bool = true, start_position:float = 0)

# Example:
var player: AnimationNodeStateMachinePlayback
player.start("myAnim", true, 5.26)
player.travel("myAnim", true, 5.26)

If this enhancement will not be used often, can it be worked around with a few lines of script?

No

Is there a reason why this should be core and not an add-on in the asset library?

NA

artvel commented 6 months ago

Just found another case that requires this feature.

If you have an AnimationTree that blends the same animation fully(while standing) and partially(while walking). You can't have a transition if you want to blend between fully and partially as you can't provide the start time.

So in other words, when you executed the animation fully while standing and while the animation is playing you started walking. You would want to take the current play time from fully and start playing partially from the same position.

artvel commented 6 months ago

Hey @jmarceno thanks for bringing up ideas. I just had a look at it and so far I don't see how to actually solve it. Maybe I am missing something.

This is how it looks:

Screenshot 2024-03-14 at 11 53 13
artvel commented 6 months ago

I would handle the transition through code as I have a movement state machine in gdscript. So what I would do is when A started while in Standing state. I would check in the start of any moving state like Walking, Jumping or Falling if A is running. In case of true, I would call something like:

var current_play_time:float = lowerBody.get_current_play_position()
animation_tree.set("parameters/BlendCasts/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
upperBody.travel("A", true, current_play_time)

If there is a better way of handling this, I'd love to here it.

artvel commented 6 months ago

Okay, thanks. I'll look into that.

About your point on the global TimeScale. I think, there are enough ways to handle the scale globally already. For example here:

Screenshot 2024-03-14 at 13 23 42
hsandt commented 2 months ago

I have a different issue where I want to force play an animation node from the start, when traveling to the same node as the current node.

But that's relevant to this issue because it raises the question of whether start_position=0 should be considered like "keeping current state" (current behavior, so retrocompatible) or "force restart from zero" (what I need).

Real-world case: if hero damages enemy, enemy plays Hurt animation. Hurt animation automatically reverts to Idle at the end. But if hero damages enemy again right on the last frame of Hurt animation, AnimationTree Hurt animation will

Indeed, currently, if current playback node is A, and I travel to A, nothing happens. Unlike AnimationPlayer I cannot force play from 0 by calling stop first: AnimationNodeStateMachinePlayback.stop has a different meaning and will simply stop running the Animation Tree state machine, keeping the last state (last animation on its last frame) instead of reverting it to the first frame and acknowledging the next travel as a brand new animation to play. It seems that the very nature of travel makes it hard to tell it to replay the same node twice.

Inspired by AnimationPlayer play(&"RESET") + advance(0) hack (https://github.com/godotengine/godot-proposals/issues/6417), I tried other hacks such as:

state_machine.travel(&"Idle")
state_machine.next()
state_machine.travel(animation_name)
state_machine.next()

to no avail.

The only call that actually forced node change was start:

var last_animation := state_machine.get_current_node()
if last_animation == animation_name:
    # When already playing the same animation
    # (including when chaining from last frame)
    # we need to force restart (travel would not restart the animation
    # and unlike AnimationPlayer, stop only pauses so travel would
    # not work after stop either)
    state_machine.start(animation_name)
else:
    state_machine.travel(animation_name)

It's not that bad, but it needs an extra check on last animation (state_machine.get_current_node()) while a from_start flag would be explicit on what I want, and start_position=0 would also work, but when using a float there is no way to indicate that we want to preserve the last state (unless we encode start_position=-1 but I find it less intuitive).

Should I make a different issue to suggest either a parameter or different method restart to force play the same node from the start, then?