Search Unity

Accurately timing the end of a Mecanim Animation.

Discussion in 'Animation' started by Aedous, Mar 20, 2017.

  1. Aedous

    Aedous

    Joined:
    Jun 20, 2009
    Posts:
    244
    Hello!

    Would like some explaining on this problem that I've got, after looking into the Mecanim system I finally figured out a way to work out when an animation has finished without using events or StateMachineBehaviours. Mainly because both ways are really tedious as you can't dynamically time any animation that you call by script.

    I have a function called SetAnimation which simply calls the "CrossFade" function from the "Animator" component, and then I basically store a record using a struct of the current animation that I'm calling, I store the hash of the animation so that I can clearly tell the difference between two different animations and react accordingly.
    Code (CSharp):
    1. [System.Serializable]
    2. public struct AnimationToken
    3. {
    4.     public string animation_name;
    5.     public string animation_token;
    6.     public string animation_calledname;
    7.     public int animation_hash ;
    8.     public bool tracking; //Is this token being tracked ( false at start )
    9. }
    10.  
    SetAnimation code: ( cut up the junk so you can see what's going on)
    Code (CSharp):
    1.  int animation_state = Animator.StringToHash(animation);
    2.         if(!animator_reference.HasState(animation_layer, animation_state))
    3.         {
    4.             Debug.Log("<b>Animator does not have state for " + gameObject.name + "</b> : " + animation + " on layer " + animation_layer);
    5.             return;
    6.         }
    7.        
    8.         animator_reference.CrossFade(animation, 0, animation_layer, normalizedTime);
    9.  
    10.         //Add the animation to the animation token to track on Update
    11.         AnimationToken calledAnimationToken = new AnimationToken();
    12.         calledAnimationToken.animation_hash = animation_state;
    13.         calledAnimationToken.animation_name = animation;
    14.         calledAnimationToken.animation_calledname = originalAnimationString;
    15.         calledAnimationToken.animation_token = animation_state.ToString() + "_" + Time.deltaTime.ToString();
    16.         calledAnimationToken.tracking = false; //Hasn't been tracked yet until a call to WaitForAnimationToFinish
    17.  
    18.         //Add the token
    19.         if (animationTokens.Count > 0)
    20.         {
    21.             animationTokens.RemoveAt(0);
    22.         }
    23.  
    24.         animationTokens.Add(calledAnimationToken); //Add to my list of current tokens I have
    25.  
    26.         //Set the current tracking token
    27.         currentTrackingToken = calledAnimationToken;
    28.  
    Then in the Update function of the script I get the current state from Mecanim and then set off a timer using the length of the animation as my time. After the timer is up it then calls a delegate event "OnAnimationFinished" that other scripts can subscribe to.
    Code (CSharp):
    1.  
    2.             if (animator_reference) //The Animator Component
    3.             {
    4.                 //Current Base Animation State
    5.                 currentBaseState = animator_reference.GetCurrentAnimatorStateInfo(animation_layer);
    6.  
    7.                 if (currentTrackingToken.animation_hash != -1)
    8.                 {
    9.                     //Check if our tracking token animation is the same as the current animation
    10.                     if (currentBaseState.IsName(currentTrackingToken.animation_name))
    11.                     {
    12.                         //Track it
    13.                         if (!currentTrackingToken.tracking)
    14.                         {
    15.                             currentTrackingToken.tracking = true;
    16.                             if (waitforanimationtofinish != null)
    17.                             {
    18.                                 waitforanimationtofinish.Stop();
    19.                             }
    20.  
    21.  //Start the timer based on the length of the state
    22.                             waitforanimationtofinish = new Task(WaitForAnimationToFinish(currentBaseState.length, currentTrackingToken));
    23.                         }
    24.                     }
    25.                 }
    26.             }
    It works really well as most of the time I can just hook up another function to the delegate on the animation script and then fire what I need to do at the end of it.
    The problem comes in because I guess it's the way Unity works with animations, once a call to CrossFade is made with no transition the current state of the animation isn't updated until the next frame, which I think is causing my timing to be slightly off.

    Was wondering if anybody had any other solution to timing the animation properly? Or even getting the state that you are calling with CrossFade before the animation has even changed. If I could get the state of the animation I'm trying to move to then I'm sure I could just call my tracking code as soon as I call "CrossFade" rather than calling it in the Update function.

    Any help would be greatly appreciated :)
     
  2. DavidGeoffroy

    DavidGeoffroy

    Unity Technologies

    Joined:
    Sep 9, 2014
    Posts:
    542
    Are you running your animator in Animate Physics?
    If you are running Animate Physics, then your code that deals with this animator needs to be in FixedUpdate.

    Otherwise CrossFade should apply in the same frame (but after Update, because any other code could call another CrossFade and change the actual state being played).

    By LateUpdate, your state change should have happened
     
  3. Aedous

    Aedous

    Joined:
    Jun 20, 2009
    Posts:
    244
    @DavidGeoffroy Thanks for the reply, I'm not running the animator on Physics just running it on Normal. It still doesn't trigger straight away and I've tried putting the code in LateUpdate just to see if I can get the timing right but it still gives me the same result as Update ( although it may be slightly different ). I'm using Unity 5.3.5f1 if that makes a difference.
     
  4. DavidGeoffroy

    DavidGeoffroy

    Unity Technologies

    Joined:
    Sep 9, 2014
    Posts:
    542
    No, it will never trigger straight away. As mentioned in the previous comment, any number of scripts can set triggers, and we [don't want/ don't support] 12 different state changes on the same frame.

    For the whole duration of the Update phase, your state will stay the same.
    If you check the current state in LateUpdate (after the StateMachine has evaluated), then you'll get the result of what you've done in the Update phase. (But I imagine it might be too late at that point)

    If you really want that amount of responsiveness, I'd suggest avoiding transitions altogether and directly using Animator.Play or Animator.Crossfade directly, and letting your own code dictate what the current state is / should be.
     
  5. Aedous

    Aedous

    Joined:
    Jun 20, 2009
    Posts:
    244
    Thanks for the info, I think I may have found a way to do it, I record the current time when making a call to SetAnimation and then when calling the Coroutine based on how long the animation is, I subtract how much time has passed from the length.