Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add node changed/started/finished signals for animation trees #102165

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

a-johnston
Copy link
Contributor

@a-johnston a-johnston commented Jan 29, 2025

Closes godotengine/godot-proposals#10253. Open to changing how this works, this is a naive approach having underestimated the complexity of the animation tree code.

This specifically adds the following signals:

  • node_changed(node) to AnimationNodeStateMachinePlayback, emitted each time the current node is set
  • node_started(node) to AnimationTree, emitted specifically when a OneShot or StateMachine animation node starts
  • node_finished(node) to AnimationTree, same as above but when the node finishes

In the case of node_started and node_finished, I'm not sure if there's a better place to put the signals given the restrictions on which nodes they emit for. It would be easy to add emission for animation nodes as well, but I think the other node types don't necessarily make sense with started/finished semantics. Additionally, if a one shot node receives an abort request it does not currently emit node_finished but that could be easily changed.

Another detail I wasn't sure about was currently the node to name lookup is a linear scan within the blend tree so to avoid that (and a Ref check for blend tree specifically) for each signal emission I set the resource name to the name assigned within the blend tree. This has a side effect where if a resource name is manually set on a nested state machine, any containing blend tree will emit signals for that node as well. This does mean there's a string to StringName conversion in the current state. This can be removed/changed if desired. I think it would be reasonable to add a StringName field to the animation node and remove the ChildNode struct but that is a larger change.

In the following example, a one-shot triggers a state machine which itself contains another state machine:

Screenshot 2025-01-29 at 2 04 05 PM

MyStateMachine:

Screenshot 2025-01-29 at 2 04 24 PM

InternalStateMachine:

Screenshot 2025-01-29 at 2 04 35 PM

Connecting all of the new signals to some printouts results in this order of operations:

  Outer node changed: Start
  Outer node changed: InternalStateMachine
    Inner node changed: Start
    Inner node changed: Anim_Knight_Attack01
Node started: MyStateMachine
Node started: OneShot
    Inner node changed: Anim_Knight_Attack02
    Inner node changed: End
  Outer node changed: Anim_Knight_Hit01
  Outer node changed: End
Node finished: MyStateMachine
Node finished: OneShot

Enabling crossfade works for all nodes involved, but does change the order certain things emit. For example with 0.1s crossfade on all connections the inner state machine finishes after the outer one has moved to the next node as expected:

  Outer node changed: Start
  Outer node changed: InternalStateMachine
    Inner node changed: Start
    Inner node changed: Anim_Knight_Attack01
Node started: MyStateMachine
Node started: OneShot
    Inner node changed: Anim_Knight_Attack02
  Outer node changed: Anim_Knight_Hit01
    Inner node changed: End
  Outer node changed: End
Node finished: MyStateMachine
Node finished: OneShot

@TokageItLab
Copy link
Member

TokageItLab commented Jan 30, 2025

See also:

As I have pointed out in the past with those, it should be implemented so that the ancestor StateMachine returns the nested paths when in Grouped mode (https://godotengine.org/article/migrating-animations-from-godot-4-0-to-4-3/#grouped). In other words, it must be implemented in such a way that the signal is propagated to the ancestor State as special case for the Grouped mode.

@a-johnston
Copy link
Contributor Author

Thanks for the links, I hadn't seen those PRs. Do you think it would be worth adding fading_from_state_changed to this pr? From the comments, a few things jumped out at me

  • Regarding performance concerns, is it more in the sense that adding new signals adds XYZ overhead even if there are no listeners, or have there been related performance issues in the past related to signal use which should be avoided? I can set up a synthetic case that triggers the new emit calls a ton if we want that measured.
  • I hadn't considered the option of saving a state machine node for reuse; I'll change the code to not use Resource.name and instead opt for a reverse node -> name map on the tree. Also I don't know if or when this would be useful, but there is an existing odd edge case where the same node resource could be added twice to a tree which would cause get_node_name to return the wrong name / cause the wrong node to be updated on a rename callback within the editor. This could be detected; it might be worth adding a warning message or something I'm not sure.

As I have pointed out in the past with those, it should be implemented so that the ancestor StateMachine returns the nested paths when in Grouped mode [..]. In other words, it must be implemented in such a way that the signal is propagated to the ancestor State as special case for the Grouped mode.

I'll work on adding this. Just to be sure I understand correctly, the changes I'll make would change the example root sm node_changed values, assuming InternalStateMachine is set to grouped, to be the following:

Start
InternalStateMachine/Anim_Knight_Attack01
InternalStateMachine/Anim_Knight_Attack02
Anim_Knight_Hit01
End

Also regarding a comment you left on another PR the user cannot use the child StateMachine's StateMachinePlayback directly, it is possible right now to grab the playback for the grouped state machine and listen to the signal from within user code, even if control is not allowed. I'll filter events in the root sm to skip the grouped Start/End node but it'll still be possible for users to directly listen to the grouped sm to detect those states.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Add node_started and node_finished signals to AnimationTree
3 participants