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

Allow AnimationStateMachine / AnimationNode to restart when transitioning to the same state #71418

Merged
merged 2 commits into from
Jan 19, 2023

Conversation

TokageItLab
Copy link
Member

@TokageItLab TokageItLab commented Jan 14, 2023

Closes godotengine/godot-proposals#5965
Fixed #69570
Fixed #69382
Fixed #71195

Allow AnimationStateMachine / AnimationNode to restart when transitioning to the same state. Also, allow NodeTransition to use String values.

Containing #71264. At first I was working with it as same PR #71264, but a few fundamental modifications caused break compatibility, so I separated this PR.

StateMachine

Make it restart when it tries to travel() to the same state. This can be resolved with one line change.

NodeTransition / NodeOneShot

For this, there was a problem with the original implementation, and 2 fundamental modifications are necessary.

1:
Currently they detect state changes by whether the property has changed from the previous frame, but this does not allow changes to the same state.

This means that they need an independent property as a request, like a StateMachine. So, implement the following properties for state changes.

NodeTransition:

# Property for the transition state requests. It will be cleared immediately in the next animation tree process.
anim_tree["parameters/TransitionNode/transition_request"] = "Input 2" # It is changed to String from int, to fix #69382.

# To avoid finding for an index every frame and to keep better performance, separate the following:
print(anim_tree["parameters/TransitionNode/current_state"]) # String value as state name for display. Read-only.
print(anim_tree["parameters/TransitionNode/current_index"]) # Int value as state index for internal use. Read-only.

NodeOneShot:

# Property for the transition state requests. It will be cleared immediately in the next animation tree process.
anim_tree["parameters/OneShotNode/request"] = AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE
anim_tree["parameters/OneShotNode/request"] = AnimationNodeOneShot.ONE_SHOT_REQUEST_ABORT

print(anim_tree["parameters/OneShotNode/active"]) # Read-only.

In addition, since there was only one active flag, there was no way to stop auto restart from the property, but now auto restart is stopped by an abort request.

2:
Some properties need to be changed only by request to prevent to make broken state, but the current status should be retrievable, so it should be PROPERTY_USAGE_READ_ONLY, not PROPERTY_USAGE_NONE.

However, the entity of the AnimationNode property is stored in the AnimationTree's map. During playback, the AnimationNode fetches the properties from the map, which means that read-only cannot be set using the USAGE flag.

Therefore, it is necessary to add a bool value to the AnimationTree map to implement read-only for the AnimationNode's parameter.

Video

Godot.2023.01.15.-.02.43.27.02.mp4
Godot.2023.01.15.-.02.43.27.03.mp4

restart_test.zip

If approved, I will update the document as soon as possible.

@TokageItLab TokageItLab added this to the 4.0 milestone Jan 14, 2023
@TokageItLab TokageItLab requested review from a team as code owners January 14, 2023 18:00
@TokageItLab TokageItLab force-pushed the restart-anim-tree branch 4 times, most recently from e605d12 to 9685429 Compare January 15, 2023 12:28
eh-jogos added a commit to eh-jogos/godot4_method_track_bugs that referenced this pull request Jan 17, 2023
Works, but crashes whenever I select the AnimationPlayer node.
Copy link
Member

@SaracenOne SaracenOne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a chance to sit down with this properly now, and I think it's fine. The only thing I would still potentially recommend changing is the name of the 'interrupt' which I think might be too generic and could mean other things depending on the context. My current recommendation is changing it to the more literal 'skip_to_next_state'.

@TokageItLab
Copy link
Member Author

@SaracenOne I get the feeling that "skip" is going to ignore xfade. So how about the following:

  • step()
  • forth()
  • ahead()
  • go_next()

@TokageItLab
Copy link
Member Author

TokageItLab commented Jan 18, 2023

For now, I renamed interrupt() to next(). Possibly, it could be mixed up with the List iterator on cpp, but for gdscript and general users, I guess it is no problem.

@reduz
Copy link
Member

reduz commented Jan 19, 2023

This looks mostly good to me, would be good to agree a proper name with @SaracenOne before merging

@@ -342,12 +342,17 @@ void EditorPropertyTextEnum::_notification(int p_what) {
}

EditorPropertyTextEnum::EditorPropertyTextEnum() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to explain what this is for. Maybe some example screen of the change to show the problem?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, wait a second, there are screens that can be broken by it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Here it is. Previous TextEnum had a problem with minimum_size not being obtained correctly since the containers existed in parallel. This PR fixes that by adding a wrapper container.

@akien-mga akien-mga requested a review from SaracenOne January 19, 2023 11:22
r_list->push_back(PropertyInfo(Variant::BOOL, active));
r_list->push_back(PropertyInfo(Variant::BOOL, prev_active, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE));
r_list->push_back(PropertyInfo(Variant::BOOL, active, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_READ_ONLY));
r_list->push_back(PropertyInfo(Variant::INT, request, PROPERTY_HINT_ENUM, ",Fire,Abort"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first one has no name?

Copy link
Member Author

@TokageItLab TokageItLab Jan 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. If there is a string here, the BlendTreeEditor pull-down will display something like "None". I think it's fine to leave it blank, or though it could be "--".

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I'm not familiar enough with the API to say what would make sense but UX wise it doesn't seem immediately obvious why there's an empty drop down list, and what the empty option does compared to the other two.

But I trust your judgment on this. Can always be improved later on if users do report this as confusing :)

@TokageItLab TokageItLab requested a review from akien-mga January 19, 2023 18:34
Copy link
Member

@SaracenOne SaracenOne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this looks good to me!

@akien-mga akien-mga merged commit d919d77 into godotengine:master Jan 19, 2023
@akien-mga
Copy link
Member

Thanks!

@macryc
Copy link

macryc commented Jan 21, 2023

How do I trigger a Fire or Abort request on a OneShot node in the code??
I've tried AnimatrionTree.set("parameters/oneshotanim/request", "Fire") but it doesn't do anything...

@TokageItLab
Copy link
Member Author

TokageItLab commented Jan 21, 2023

It has been described in the description. The document(tutorial) for AnimationTree is overall out of date and needs to be updated later.

# Property for the transition state requests. It will be cleared immediately in the next animation tree process.
anim_tree["parameters/OneShotNode/request"] = AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE
anim_tree["parameters/OneShotNode/request"] = AnimationNodeOneShot.ONE_SHOT_REQUEST_ABORT

anim_tree.set("parameters/OneShotNode/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
anim_tree.set("parameters/OneShotNode/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_ABORT)

print(anim_tree["parameters/OneShotNode/active"]) # Read-only.

@macryc
Copy link

macryc commented Jan 21, 2023

Why can't we just say "Fire" instead of AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE? This is making things more complicated. It's also inconsistent with Transitions where you can set the state parameter via a string...

@TokageItLab
Copy link
Member Author

TokageItLab commented Jan 21, 2023

Transition has a clear list of strings, but OneShot uses just an Enum as an Int for the request.
I don't know of any other method in Godot that allows a specific static const string to be input for a property instead of an Enum. If there is already such a method and it is allowed, I would consider some kind of binding.

@jcarlosrc
Copy link

Any way of checking if StateMachine is already in a given state so not to restart the animation when using travel() to the same state? Maybe the travel() function could have a second parameter (restart = true, false) to avoid restarting so to resemble the previous behavior. Or maybe is about documentation.

@TokageItLab
Copy link
Member Author

TokageItLab commented Jan 22, 2023

if state_machine_playback.get_current_node() != "A":
  state_machine_playback.travel("A")

@TokageItLab
Copy link
Member Author

TokageItLab commented Jan 22, 2023

If there is a long crossfade, it may have already moved to the next state, and we may be better to expose the fading_from. It seems that cpp has a function get_blend_from_node(), but it is not bound and not exposed... I will send a PR.

@TokageItLab
Copy link
Member Author

There is also a second argument, reset_on_teleport, but if it is false, it would certainly make more sense not to restart with travel. I thought I had implemented it that way, but there seems to be some problem and it is not working. I will fix it later.

@TokageItLab
Copy link
Member Author

TokageItLab commented Jan 22, 2023

@jcarlosrc I sent #71840. With that, you can write:

state_machine_playback.travel("A", state_machine_playback.get_current_node() != "A")

It does not allow its own restart. But it allows teleports and resets from other states. Note just that the existing travel path is to be discarded. In order to keep the existing travel path, if statement must be used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment