godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.1k stars 21.18k forks source link

Issue with Animation Tree transition logic #98617

Closed Phreakyx closed 1 week ago

Phreakyx commented 2 weeks ago

Tested versions

System information

Godot v4.3.stable - Windows 10.0.26100 - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 4080 SUPER (NVIDIA; 32.0.15.6603) - Intel(R) Core(TM) i9-14900K (32 Threads)

Issue description

Me and my team are facing an issue with using a more advanced animation tree with nested state machines and animation blending through fade.

The issue is quite simple actually. When using an Idle state as a conduit to all other states and trying to transition from lets say Crouch state into the Sprinting state we go through the Idle state. When animation xFade time is applied the result is that we fade towards the Idle and then we fade from Idle towards the Sprint state.

Usually what we would expect is for the Animation tree to resolve the final state and fade from Crouch to Sprint directly instead of having to make a separate transition between the two states.

Without this we are forced to make transitions considering every possible combination from our numerous states towards each other and not only does this become harder to maintain and prone to edge cases(at least a lot more than usual) it also makes our animation tree look like a (sorry but couldn't picture it better) spaghetti dish making it very hard to understand what goes where. Another contributing factor is the UI not giving options to have reroute nodes to better place the transition nodes so they look more readable.

For example Unreal does this by default and works perfectly for our case but we decided to switch to Godot in our belief that it is the better engine for its modularity and open-source approach. If we can have this at least as an option somewhere it would be amazing.

We also cannot find a way to have a blend weight to an animation track so that when blending/fading animations for example the animation method call track doesn't trigger on the animation that is being faded out during the blend. But this could be us not finding the proper option in all of these cases so please feel free to correct me.

Thank you in advance and apologies for taking your time!

Steps to reproduce

Create a Project. Add a 3D character with animations and animation tree. Make the animation tree root node a state machine. Create an Idle state. Create 2 more states that transition from and to Idle state but not to each other. State 1 and State 2 Set xFade time on all transitions to 0.2 for example. Higher would exaggerate the issue. Make advanced transition rules. Create a situation which requires State 1 to transition to State 2.

Minimal reproduction project (MRP)

Godot-4.3-Third-Person-Controller.zip

AThousandShips commented 2 weeks ago

Please upload a minimal project to make testing and fixing this easier, to get all the details to match your setup

Phreakyx commented 1 week ago

Please upload a minimal project to make testing and fixing this easier, to get all the details to match your setup

Apologies, I have uploaded a rough example which covers it perfectly. Just start walking and press Shift to sprint and you will see how we pass into the idle state before we go into the Running state Same thing happens when jumping or going from Running back to walking.

TokageItLab commented 1 week ago

It seems that you are simply inexperienced in the way StateMachine is configured.

MRP: image

Since StateMachine is nestable, if you want non-Idle animations to transition between each other without going through Idle, you would normally have the following configuration.

Recommended1:

Also, if you have a large number of Actions, you can have two NestedStateMachines for Actions if you use something like AnyState, and by alternating the transitions between them, you can make transitions between all combinations placed in the StateMachine.

Recommended2:

See also NodeStateMachine - State Machine Type.

Phreakyx commented 1 week ago

Thank you for the information and the article which was really informative and honestly baffled me as it is not present in the docs or I just didn't find it.

However none of the solutions in the article or the comment solves the original issue. It is definitely true that I am still learning how to use the state machines in Godot but I have been doing this in Unreal for a couple of years now and there I have a solution which I still fail to find.

To be more specific of course:

Let me give an example:

This is how our main project which we are currently migrating to Godot looks like in UE5: image

Each of these States is a state machine that has other nested state machines inside it as needed.

Example:

@TokageItLab I am not perfectly acquainted with the Animation Tree so I may be missing something still but if there is a way to achieve the above mentioned functionality please let me know. Thank you in advance!

TokageItLab commented 1 week ago

As I already mentioned above, you should consider "having only one Transition between Idle and Action". And Action is a nested StateMachine, BlendTree or BlendSpace2D, with mutual transitions other than the Idle animation. Unless you include an Idle animation in it, it will not show up unless an Idle animation occurs in top level Idle <=> Action transitions. Simply put, you should think in the way of not including in the transition path any animations that you do not want to be displayed during the transition.

If you are concerned about the number of connections, it is also I say the same thing over again, create two AnyStates in the Action and switch between them, changing the Transition value according to the state. In this configuration, if there are 100 states, you needs only put 100 state + put 2 Any states + connect 2 transitions, not put 100 state + connect 100*100 transitions each other. Or, simply you can create an add-on script for editor to interconnect everything in the nested state machine.

Even when I create a state machine to hold my actions for this given state I still have lets say for example 5 different states with substates and animations in them. Each of these 'Main' states needs to be able to transition from and into one another while resolving the transition from the Source animation to the Final animation.

For example, if you have several more SubStates in a nested StateMachine and you want them to interconnect without displaying an Idle, simply connect the SubStates to each other without going through an Idle.

However, if you want to “directly” connect things in different SubStateMachines, such as things in SubState A and things in SubState B, which are siblings, that means they should not be SubStated in the first place except AnyState approach. To be more specific, if a transition in StateMachine A is in progress (animations in StateMachine A are blended) and an attempt is made to transition to StateMachine B, the final result blends StateMachine A blended result and the current state of StateMachine B, so there is double blending.

Grouped mode solves this to some extent, but it does not support multiple ports of input/output https://github.com/godotengine/godot/issues/88878, so the currently recommended solution is to use AnyState with Nested mode.

Phreakyx commented 6 days ago

I understand, I did some research on the topic of AnyStates as well as asked a friend that works in Unity as it seems that the notion comes from there and there is practically zero documentation about it from the Godot side of things.

I now understand the principle better and think that this may be what I need in animating it but I still struggle to understand from your photo how exactly it works in Godot specifically as in Unity you simply drop the AnyState transition with a condition and it works but here I do not have such a node and your photo shows zero transitions to the states inside the state machine substates.

Before I ask my question I'd like to explain my original idea as it may have been missed. The idea was to make a case where a State lets say Crouch needs to be entered from the animation tree in condition (animstate == Crouch) but the transition out of it is (animstate != Crouch) so if anything else gets set then the crouch simply needs to exit and the anim tree to reevaluate where to go which was the original reason everything was connected to Idle. We go to Idle and reevaluate where to go next based on the value of the enum while being careful to have a state for each enum value.

This approach made it very easy to make transitions into the different Main state machines without making transitions from each one to another and backwards. AnyState at least the Unity one could definitely work the same way and even having Idle be one of the main state instead of everything transitioning to it.

I have also had the idea to create the following.

I have a Root state machine which has a start directly connected to another state machine which has all of my actions inside of it. Then I make a duplicate like in your example and connect the two duplicate state machines with transitions AtEnd style so that I do not break the loops inside since they will always transition into one another and there is no condition for the transitions.

Inside the State Machine duplicates I have transitions from the start node to each state with a condition and transitions from each state to End with a condition as well. This way I can go from one state to the other without relying on Idle but the issue is that each time I change states my character goes through his Reset pose and it is visible for atleast one frame where the char is in TPose I suspect because we go to End in the state machine but if I do not use End then I have no way to stop the current animation as I cannot make a transition from a state to Start(makes sense).

image image

I will upload the modified project for reference.

@TokageItLab As for my question. Are you able to explain how exactly the AnyState is setup in Godot and how exactly it works as there is no documentation about this and it may be the only solution for us. You showed a state machine without transitions inside it but I struggle to understand how the specific state inside this state machine is chosen. Is it a random state?

Of course if there is also another way or if there is a way to make my idea work without the TPose being visible between animations and I can blend them with the XFade then perfect.

I do appreciate the feedback and the suggestions!

Phreakyx commented 6 days ago

Godot-4.3-Third-Person-Controller-afd21891ef0b572f58035cbc23590ddbe9c833f4.zip

TokageItLab commented 6 days ago

The article in NodeStateMachine - State Machine Type is the most detailed so far on the use of AnyState, and it is also part of the documentation because it is linked from the documentation. Also, the original design concept is described in https://github.com/godotengine/godot/pull/75759.

In some cases, StateMachine cannot detect the end time depending on its connection and state. Root mode and Nested mode differ in how they detect end time and how they behave when seeking.

Root

Nested

Note that a Nested mode StateMachine can have a connection with its child states, but if the state is not a dead end and has a connection, it will not detect the end.

This way I can go from one state to the other without relying on Idle but the issue is that each time I change states my character goes through his Reset pose and it is visible for atleast one frame where the char is in TPose I suspect because we go to End in the state machine but if I do not use End then I have no way to stop the current animation as I cannot make a transition from a state to Start(makes sense).

This is expected since there is no longer an Idle state above it and there are no animations available for playback, hence the bone rest is displayed. Reconsider the settings of the connection to the Idle state at the top level.

Or it could be due to a one frame playback delay. In that case, check to see if PR https://github.com/godotengine/godot/pull/94372 helps; a workaround available before https://github.com/godotengine/godot/pull/94372 is merged would be to set a minimum crossfade, such as 0.001 xfade, on the connection of the state that needs immediate playback.

Of course if there is also another way or if there is a way to make my idea work without the TPose being visible between animations and I can blend them with the XFade then perfect.

You can also consider using NodeTransition in the nested BlendTree like:

Root:

Start -> Idle <=> Action

Action(BlendTree):

Run 
Walk ⋺ NodeTransition - Output
Jump
Phreakyx commented 6 days ago

Honestly I tried everything I can. I tried the advice with the XFade time and it did not work no matter where I placed it. I tried to use a blend tree but since the blend value should be controlled by a transition it did not work out at least as I understand it. I've never used blend trees as state machine logic should be all I need to blend the animations.

I even tried to compile your branch from https://github.com/godotengine/godot/pull/94372 and set the advance flag on every single animation and it not improve. As far as I saw from logging the behaviour when I go from one state to the End node and transitioning via AtEnd transition without any condition there are two prints(ticks) where both state machines sit at the End state. This is the reason the character goes into a TPose because the animation tree has not yet transitioned into the new state. If this happened instantly(in the same tick) then the TPose animation would not be shown. I am aware now of the advance(0) function as I mainly write my game logic in C++ but I have no idea how to force that during a transition.

@TokageItLab At this point I apologize as I am taking a lot of your time but I really cannot figure this out yet. Can you look at the second project I uploaded and try to make it work from your end? Consider the above idea that I would ultimately have main states as the screenshot from Unreal and they will be nested state machines with other sub state machines inside them. If it were 3-4 animations like in the example then easily I could connect them but alas it will be much more complex.

TokageItLab commented 6 days ago

Godot-4.3-Third-Person-Controller-fixed.zip

image image image

TokageItLab commented 6 days ago

Above is just recommended structure of statemachine.

So finally, recommended code for AnyState is below.

Godot-4.3-Third-Person-Controller-any-state.zip

@onready var animator : AnimationTree = $AnimationTree
@onready var animator_state : AnimationNodeStateMachinePlayback = animator.get("parameters/SM/Action/playback")
@onready var animator_state_action : Array[AnimationNodeStateMachinePlayback] = [animator.get("parameters/SM/Action/StateMachine/playback"), animator.get("parameters/SM/Action/StateMachine 2/playback")]
@onready var animator_state_action_names: Array[String] = ["StateMachine", "StateMachine 2"]
@onready var animator_state_action_idx : int = 0
@onready var animator_state_current_action : String = ""

func animate(delta):
    var prev_action : String = animator_state_current_action
    animator_state_current_action = ""
    if is_on_floor():
        if velocity.length() > 0:
            if speed == run_speed:
                animator.set("parameters/SM/conditions/idle", false)
                animator.idle = false
                animator_state_current_action = "Run"
            else:
                animator.set("parameters/SM/conditions/idle", false)
                animator.idle = false
                animator_state_current_action = "Walk"
        else:
            animator.set("parameters/SM/conditions/idle", true)
            animator.idle = true
    else:
        animator.set("parameters/SM/conditions/idle", false)
        animator.idle = false
        animator_state_current_action = "Air"
    if prev_action != animator_state_current_action && !animator_state_current_action.is_empty():
            animator_state_action[animator_state_action_idx].start(animator_state_current_action)
            animator_state.travel(animator_state_action_names[animator_state_action_idx])
            animator_state_action_idx = (animator_state_action_idx + 1) % 2
Phreakyx commented 5 days ago

Firstly, thanks once again for the ideas. I was wondering how AnyState worked in here but this example shown is not going to work for me. I have 80 animations and will not be writing code for every single one of them. This example with the 4 animations works but is not my exact use case. The goal is to achieve this through the Animation Tree entirely while outside we only set an enum with the current anim state.

The fixed version has a nice transition from Idle to the Action but when changing states between Actions inside it still goes into TPose during the transition.

image

One way is to call advance(0) when detecting that we are at End in both machines to force the transition to happen this tick. The other way is to call travel("Start") to the currentSM which achieves the same thing with the exception that travel() seems to ignore crossfades entirely.

Is there an easy way to modify the engine code to implement this?

TokageItLab commented 5 days ago

The problem is that if there is a transition in the AnyState, the seek may not work. As explained above, AnyState restarts the current State when it restarts, so transitions to End and Start can cause delays and other problems since the current State is lost.

The latter AnyStateExample sent in https://github.com/godotengine/godot/issues/98617#issuecomment-2463232326 has a different configuration of Fixed and StateMachine on it (no internal connection), but it should work.

image

Firstly, thanks once again for the ideas. I was wondering how AnyState worked in here but this example shown is not going to work for me. I have 80 animations and will not be writing code for every single one of them.

I am not sure what you mean by this. In your code in the past, jump and air have been bool parameterized, so I don't know what the difference is.

The code I have suggested with AnyState makes them into strings, which can be cached as arrays by accessing the StateMachine resource at Ready timing, etc., so it should be possible to automate this to some extent.

What it would require for my case to work would be to have the last Valid State before we got to End and then crossfade into the new state from Start. This way we both fix the TPose and the crossfade between animations. And with this I will be able to set different crossfades for the different States. Unfortunately this does not seem possible at the moment. Otherwise I would accomplish exactly what I needed and it would technically work the same way as in Unreal.

Grouped mode solves this problem to some extent, but as explained in https://github.com/godotengine/godot/issues/88878, it is currently unsafe because it does not have multiple in/out ports.

Phreakyx commented 5 days ago

I understand, I think the anystate even with the automation mentioned would probably be too much work considering I plan on having multiple nested machines so it would probably turn out to be less ideal for the use case. The MRP was created to showcase the issue while in my actual project I have not used a bool parameter to switch animations and only use the advanced expression to check for enum values or a combination of conditions.

With that in mind the classic transition creation for each state to another will have to do.

Last question: Would it be too difficult to implement engine logic that works like so:

The goal being doing things entirely in the animation tree while achieving the original idea of transitioning through multiple states without triggering fade events in between transitions unless told to by a flagon the transition or something like it.

TokageItLab commented 5 days ago

I understand, I think the anystate even with the automation mentioned would probably be too much work considering I plan on having multiple nested machines so it would probably turn out to be less ideal for the use case. The MRP was created to showcase the issue while in my actual project I have not used a bool parameter to switch animations and only use the advanced expression to check for enum values or a combination of conditions.

I don't think there is any difference in labor yet. The only difference is whether the final condition is written in the Transition of the AnimationTree or in the script, and the amount of condition writing has not changed.

When a transition is triggered then in the logic check if afterwards another one is going to also be true that same tick until we end up in a state where no transitions are available anymore and after that simply transition from the current animation to the final one? This happening in one process tick. Would most likely be a PR.

Do you mean that you want to create a path by travel and then transition to the last animation ignoring the middle of it? Technically, it would be possible, but in that case, we would have to create a secure API. For example, if you have a path like:

A-B-C-D :travel_path
 1 2 3  :transitions

If you want to do a transition from A to D without B-C, you will not know which xfade or other parameter to refer to for the transition.

So in that case, I think the proposal/PR would be to implement an xfade parameter for teleportation. This way, after generating the travel path, the user can teleport at optional by retrieving only the end of the path and discarding the travel path.