Search Unity

How to get the length of a not current, animator's state, which contains a blend tree?

Discussion in 'Animation' started by JeffXu717, Nov 2, 2018.

  1. JeffXu717

    JeffXu717

    Joined:
    Oct 18, 2018
    Posts:
    2
    Hello everybody! I have a problem about animator as title represented.

    Actually, I achieved a blend effect of several animations which could be played at every specific timing. And I placed it in a animator's state so I could play it simply by setting trigger. In order to calculate proper timing every time, the length of this state is needed. Every time I need to play it, the parameters differs but I know them before the timing. However, according to the official doc, the length of such a state can vary because of the contained blend tree and the parameters deciding the blend effect. So the length of a state is also different every time and I can only acquire it at Runtime.

    I know if I can get the length through
    AnimatorStateInfo.length
    . But there are only two method can return a
    AnimatorStateInfo
    object, which is
    GetCurrentAnimatorStateInfo()
    and
    GetNextAnimatorStateInfo()
    . Both are not I need because I want to know the length before I set the trigger.

    So there are my two questions:
    1. Does anyone know the algorithm that Unity blend several animations through Blend Tree? Can I calculate the length of blend one by myself?

    2. Why Unity did not offer a method like "GetAnimatorStateInfoByName"? Or does there exist a way to get
    AnimatorStateInfo
    object before enter state?
     
  2. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    1. The user manual for my Animancer plugin has a table listing the interpolation algorithm used by each type of blend tree, but that's only for the blend weights. I haven't reverse engineered the speed/duration modifications Unity does and I'd be quite surprised if anyone else has. You might be able to get a good result by using one of my Animation Mixers and controlling the speed of each animation yourself instead of trying to figure out what Unity calculates. Animancer Lite would let you freely try it out in the editor so you can see how well it works before you decide if you want to buy it or not.

    2. Because they focussed on making a user friendly editor UI and ended up with an inflexible monstrosity on the programming side. You can get access to some additional information in the editor by casting the RuntimeAnimatorController to a UnityEditor.AnimatorController and then serializing whatever you need separately, but I don't think that would help you in this case.
     
    Last edited: Sep 9, 2022
  3. JeffXu717

    JeffXu717

    Joined:
    Oct 18, 2018
    Posts:
    2
    I have viewed the manual page of Animancer Lite and there is a paragraph which caught my attention.
    • By being defined in the Unity Editor, Blend Trees are able to analyse their animations so that their walk cycles can be synchronised (right foot touch down and lift up at the same time, etc.), which is especially important for animations of different lengths. Unfortunately that analysis is not possible at runtime (and would be quite expensive to performance even if it was) so Animancer is unable to do the same thing in AnimationMixers.
    Maybe it is the reason why both Unity and Animancer can not offer that analysis.
    Thank u for reply! Well I think I should find a workaround.
     
  4. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    Code (CSharp):
    1. GetComponent<Animator>().Update(Time.deltaTime);
    One way is to simulate into the next Animation State, get the required info and come back (replay clip/use clone before)
     
  5. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    That would cause it to update twice as fast for that frame since it won't cancel the regular animation update. Even if you pass in 0, you'd still be doubling the performance cost of that object's animation for that frame (more than that because regular animation updates are multi-threaded and manual ones aren't).

    It also fails to meet OP's stated goals: "I want to know the length before I set the trigger."

    I'm not sure how you think you can just "get the required info and come back" because that's not a thing Animator Controllers can do. You could grab the current state hash and time, evaluate, grab the info, then Play the current state hash, but that would leave you one frame behind where it would have been naturally and wouldn't work properly if it was in the middle of a transition when you did that.
     
  6. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    OP is setting a trigger so obviously he knows the name of the state that will play next. One can simply Play that state and update by a frame. The time it takes to update an animator by one frame is hardly anything at all. As to "getting back to where the animator state was" just replay the current animation by
    animator.Play(0, 0, 0);
    not leaving any frames behind, or if it was in the middle of a state, pass in the normalized time in the third parameter
    animator.Play(0, layerIndex, myNormalizedTime);
     
  7. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    What's the connection between that statement and the topic being discussed?
    • OP: "I want to know the length before I set the trigger."
    • You: "You obviously know the name of the state that will play next."
    ???
    Or maybe performance is actually important to OP, we don't know that, so all I can do is point out that your solution is extremely inefficient.
    Like I said: "wouldn't work properly if it was in the middle of a transition when you did that." You can put it back anywhere in a single state, but you can't put it back in the middle of a transition.
     
  8. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    That is an extremely bad assumption.
    Sure they can, by calling
    CrossFade()
     
  9. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    More than doubling the performance cost of a system just to access a value which should be easily accessible for free is extremely inefficient. That's not an assumption, it's a fact.

    Will it cause OP performance problems? No one knows. All I can say is that it's inefficient.
    CrossFade only starts a transition.

    If you were 50% of the way from State A to State B, CrossFade will start a new transition from 0%.

    Then there's also the fact that you also can't check how long the current transition is going to take or how long it has left so you wouldn't even be able to give CrossFade the correct fade duration.

    And if you rely on any Animation Events or State Machine Behaviours, your "solution" would mess with them too.
     
  10. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    Prove it if it's a fact.
    You can, by adding the elapsed time from
    GetNextAnimatorStateInfo()
    and remaining time from
    GetCurrentAnimatorStateInfo()
    you'll get the transition time
    For this, one can create an
    "ignoreAnimationEvents"
    variable that will prevent animation events from updating stuffs.
     
  11. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    You want me to prove that evaluating animations a second time in the same frame will double the cost of evaluating animations in that frame just so that you can say "that's not inefficient, it's hardly anything at all"? No, I don't think I will waste my time on that. A sensible person wouldn't need proof of such a simple concept. Trolling attempt failed.
    GetNextAnimatorStateInfo can only tell you the current animation time. If it started at 0 and speed is always 1 then it will match the transition time, otherwise it won't.

    But either way, GetCurrentAnimatorStateInfo can't tell you the remaining transition time.

    And even if you had those values, CrossFade would still put you back at the start of the transition.
     
  12. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    it can:
    elapsedTime = state.normalizedTime * state.length
    and a similar way for next animationState. If it has speed other than 1, multiply it.
    Next animation time* not "current".
     
  13. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    forgot to reply this, so it requires you to do
    Update()
    on animator after calling crossfade
     
  14. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Me: "GetCurrentAnimatorStateInfo can't tell you the remaining transition time."

    You: "it can: elapsedTime = ..."

    Uhhh ... that's not the remaining transition time.
    So what? You need the elapsed transition time and remaining transition time to return to the same transition values. "Next animation time" is not either of those values so that's a pointless correction.

    Brilliant. Why double the performance cost of the character's animation for a frame when you could quadruple it instead. Pure genius.
    1. Update to get into the BlendTree and get its length.
    2. Call Play to return to the previous State A then Update.
    3. Call CrossFade to restart the transition to State B then Update.
    4. Then the frame's regular animation Update.
    Your idea is pretty straightforward: store some values -> Update to get what OP wants -> do stuff to return the animator to the stored values. I'm confident that it's not possible with Animator Controllers because they don't give you access to the necessary values. But if you still think it's actually possible, you could just write the code instead of posting half-baked snippets that don't actually support your claim.
     
  15. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    Remaining transition time is
    transitionDuration = totalTimeFromCurrent - (elapsedTimeFromCurrent - elapsedTimeFromNext)
    . yeah maybe I explained it bad before but works.
    Your confidence is frightening. You think I didnt try it, yet posting answers? Animator does provide you the values though for "that slight performance cost". i explained the procedure, the code will vary based on purpose so no point in asking for code.
    Edit: had a mistake putting "1" instead of "totalTime"
     
    Last edited: Sep 20, 2022
  16. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Oh look, another half-baked snippet which is missing key information and therefore fails to properly support your claim.

    What's elapsedTimeFromNext? Are you pretending that the time left in the current animation is the same as the time left in the transition? That would be true for an Exit Time transition only if it was set to end at the exact same time as the animation, but would be completely wrong for any other kind of transition (such as one started by CrossFade or any parameter based transition). If you would just post your code, I wouldn't have to keep asking how you're getting these values.
    Yes, I think you didn't try it and every time you comment without posting more than one line of code in one go reinforces the idea that you haven't tried it.

    It seems like a pretty simple situation to me. The character is some unknown percentage of the way into a transition from State A to State B and you need to get the length of a Blend Tree in State C without ruining the A->B transition. It's really not that complicated so the code shouldn't be that complicated either.
     
  17. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    Code (CSharp):
    1. Animator animator = GetComponent<Animator>();
    2.         float currentOffset = animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
    3.         float elapsedTimeInCurrentState = animator.GetCurrentAnimatorStateInfo(0).length * animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
    4.         float elapsedTimeInNextState = animator.GetNextAnimatorStateInfo(0).length * animator.GetNextAnimatorStateInfo(0).normalizedTime;
    5.         float normalizedTransitionStartTime = (elapsedTimeInCurrentState - elapsedTimeInNextState) / animator.GetCurrentAnimatorStateInfo(0).length;
    6.         int oldAnimationHash = animator.GetCurrentAnimatorStateInfo(0).shortNameHash;
    7.  
    8.         animator.Play("New Animation");
    9.         animator.Update(Time.deltaTime);
    10.         float newAnimationLength = animator.GetCurrentAnimatorStateInfo(0).length;
    11.         animator.CrossFade(oldAnimationHash, 1 - normalizedTransitionStartTime, 0, currentOffset);
     
  18. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    As expected, the way you're calculating the fade duration makes no sense whatsoever.

    You're taking the number of seconds that have passed in the current animation and next animation and pretending that those values have some connection to the amount of time left in the transition, but they simply have nothing to do with each other.

    For example:
    • The setup:
      • Both states have length 1.
      • State A is at time 0.2.
      • Start fading to State B over 0.3 seconds.
      • Wait 0.15 seconds.
      • State A Time = 0.35, State B Time = 0.15
      • Currently 50% of the way through a 0.3 second transition.
    • Now we run your code:
      • State A Time = 0.35 ->
        elapsedTimeInCurrentState = 0.35
      • State B Time = 0.15 ->
        elapsedTimeInNextState = 0.15
      • normalizedTransitionStartTime = (0.35 - 0.15) / 1 = 0.2
      • So you're calling
        CrossFade(... 0.2 ...)
      • Now the character is 0% of the way through a 0.2 second transition.
    • Congratulations, the character is now in a completely different pose and will take 0.2 (Edit: actually 0.8, as per my next comment) seconds to finish a transition that was only supposed to last for 0.15 more seconds.
     
    Last edited: Sep 20, 2022
  19. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Actually, I forgot the
    1 -
    part in the CrossFade call, so your new transition will actually take 0.8 seconds instead of the original 0.3 total or 0.15 remaining.
     
  20. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    Yeah that example was assuming the end of transition is at the end of animation. Just noticed you said this before. Anyways, if the transition ends in the middle of first animation state, its still possible to reach the last state

    Code (CSharp):
    1.  
    2.         Animator animator = GetComponent<Animator>();
    3.         float elapsedTimeInCurrentState = animator. GetCurrentAnimatorStateInfo(0).length * animator. GetCurrentAnimatorStateInfo(0).normalizedTime;
    4.         float elapsedTimeInNextState = animator. GetNextAnimatorStateInfo(0).length * animator. GetNextAnimatorStateInfo(0).normalizedTime;
    5.         float normalizedTransitionStartTime = (elapsedTimeInCurrentState - elapsedTimeInNextState) / animator. GetCurrentAnimatorStateInfo(0).length;
    6.         float normalizedTransitionTime = animator. GetCurrentAnimatorStateInfo(0).normalizedTime - normalizedTransitionStartTime;
    7.         float animDuration = animator. GetAnimatorTransitionInfo(0).duration;
    8.         int oldAnimationHash = animator. GetCurrentAnimatorStateInfo(0).shortNameHash;
    9.  
    10. animator. Play("New Animation");
    11. animator. Update(Time.deltaTime);
    12.         float newAnimationLength = animator. GetCurrentAnimatorStateInfo(0).length;
    13. animator. Play(oldAnimationHash, 0, normalizedTransitionStartTime);
    14. animator. CrossFade(oldAnimationHash, animDuration, 0, 0, normalizedTransitionTime);
    15.  
    16.  
    Now you're probably gonna say its not possible to get to the required state if there is an offset for the next animation state. In that case we'd have to store the offset as soon as the transition starts by using
    animator. IsInTransition(0)
     
  21. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    That actually could get you most of the way there. I wasn't aware of GetAnimatorTransitionInfo which looks like it was only added in Unity 2020.3 (which obviously wasn't an option in 2018 when this thread started).

    So yeah, your idea could work ... with over 4x the performance cost of a regular animation update and a bunch of additional hacks to avoid issues with stuff like events and root motion.

    One thing that still wouldn't handle is consecutive transitions:
    • Transition halfway from State A to State B. Call this pose C.
    • Transition halfway from pose C to State D.
    • Then try to run this code.
    That wouldn't work because the C->D transition would have replaced the A->B transition information needed to recreate pose C. I'm not actually sure what the current state info would be in that case, but if I had to guess I'd say it's probably pretending its still in state A with the next state as D and no sign of B or C at all.
     
  22. AB498

    AB498

    Joined:
    May 4, 2019
    Posts:
    17
    I think normally poses should be handled manually through code (lerping local rotations) and not through the animator (crossfade). Anyways I dont think someone would need to calculate blend tree lengths at such complicated situations you stated.