16
\$\begingroup\$

I need a way to detect if animation's clip finished playing and then execute a certain code. Here's some script, that I wrote (called in Update):

    if (Input.GetButtonDown ("Fire1") && pc_atttacking == false && PlayerStats.staminaCurrent != 0) {
        pc_atttacking = true;
        pc_anim.SetBool("attack", true);
    }

    if (pc_anim.GetCurrentAnimatorStateInfo (0).IsName ("attack")) {
        pc_atttacking = false;
        pc_anim.SetBool("attack", false);
    }

But it doesn't work as I want; it executes second code too early - when "attack" animation is at slightly more than half of playing position. I want to execute it when "attack" animation is at last frame.

\$\endgroup\$

6 Answers 6

22
\$\begingroup\$

I usually prefer to use Animation Events for this, so that there's not a check hitting every Update when it's rarely actually needed. You could even make a generic alert system and pass in arguments to handle different animations ending or other events.

Example of Unity's Animation Events used to detect an Animation finishing

In this example, AlertObservers is in a component on the animated object.

    public void AlertObservers(string message)
    {
        if (message.Equals("AttackAnimationEnded"))
        {
            pc_attacking = false;
            pc_anim.SetBool("attack", false);
            // Do other things based on an attack ending.
        }
    }
\$\endgroup\$
2
  • 2
    \$\begingroup\$ I like this approach, although I might add that using Enums is a nice choice as it avoids string comparison and also allows the use of a drop-down menu in the editor \$\endgroup\$ May 21, 2018 at 23:57
  • 1
    \$\begingroup\$ An update, after using this approach for a while: Animation Events can still trigger during animation transitions (i.e. when cross-fading between animations), which can cause unwanted behavior if you aren't careful with the timing. \$\endgroup\$
    – Nate Buck
    Jan 31, 2020 at 19:59
13
\$\begingroup\$

As I was very often in the position to check for an animation when it starts or ends, I came up with simple generic solution. It will dispatch animation start and animation end events for any AnimationClip controlled by the Animator.

using UnityEngine;
using UnityEngine.Events;

[System.Serializable]
public class UnityAnimationEvent : UnityEvent<string>{};
[RequireComponent(typeof(Animator))]
public class AnimationEventDispatcher : MonoBehaviour
{
    public UnityAnimationEvent OnAnimationStart;
    public UnityAnimationEvent OnAnimationComplete;
    
    Animator animator;
    
    void Awake()
    {
        animator = GetComponent<Animator>();
        for(int i=0; i<animator.runtimeAnimatorController.animationClips.Length; i++)
        {
            AnimationClip clip = animator.runtimeAnimatorController.animationClips[i];
            
            AnimationEvent animationStartEvent = new AnimationEvent();
            animationStartEvent.time = 0;
            animationStartEvent.functionName = "AnimationStartHandler";
            animationStartEvent.stringParameter = clip.name;
            
            AnimationEvent animationEndEvent = new AnimationEvent();
            animationEndEvent.time = clip.length;
            animationEndEvent.functionName = "AnimationCompleteHandler";
            animationEndEvent.stringParameter = clip.name;
            
            clip.AddEvent(animationStartEvent);
            clip.AddEvent(animationEndEvent);
        }
    }

    public void AnimationStartHandler(string name)
    {
        Debug.Log($"{name} animation start.");
        OnAnimationStart?.Invoke(name);
    }
    public void AnimationCompleteHandler(string name)
    {
        Debug.Log($"{name} animation complete.");
        OnAnimationComplete?.Invoke(name);
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ I think that "Transition Duration", "Transition Offset" and "Exit Time" does probably impact on the "time" of the event. At least on my case, although the event is succesfully created at the end of the clip, it is never called. Do you know the formula to get the correct time on those cases? \$\endgroup\$ May 5, 2022 at 4:47
  • \$\begingroup\$ Well, just to registrate how I managed to make it work... I used a "default" StateMachineBehaviour on my animation states just to get the OnStateExit. (source: gamedev.stackexchange.com/questions/149473/…) \$\endgroup\$ May 5, 2022 at 5:22
9
\$\begingroup\$

You can check if the animation is complete by looking at the normalizedTime property of the Animator's AnimatorStateInfo:

if(pc_anim.GetCurrentAnimatorStateInfo(0).IsName("attack") && 
   pc_anim.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1.0f)
{
    pc_atttacking = false;
    pc_anim.SetBool("attack", false);
}
\$\endgroup\$
1
  • \$\begingroup\$ You do not need the check against the normalizedTime from what I can tell. That is just putting a 1 second delay on whatever logic you want to run after the Animator is in that state. \$\endgroup\$
    – Shadoninja
    Apr 23, 2018 at 17:29
4
\$\begingroup\$

Well, on the other hand sometimes Coroutines would be just like a gift of God for you.

You can use it in more good way.

void Update()
{
    if (Input.GetButtonDown ("Fire1") && pc_atttacking == false && PlayerStats.staminaCurrent != 0) {
        pc_atttacking = true;
        pc_anim.SetBool("attack", true);
        StartCoroutine("OnCompleteAttackAnimation");
    }
}

IEnumerator OnCompleteAttackAnimation()
{
    while(pc_anim.GetCurrentAnimatorStateInfo(0).normalizedTime < 1.0f)
        yield return null;

    // TODO: Do something when animation did complete
}
\$\endgroup\$
9
  • \$\begingroup\$ Should work, but this is a bit smelly in my opinion. \$\endgroup\$
    – Evorlor
    Feb 29, 2016 at 13:58
  • 1
    \$\begingroup\$ @Evorlor I think it is better than adding more lines in Update \$\endgroup\$ Feb 29, 2016 at 13:59
  • \$\begingroup\$ If you are speaking from a performance point of view, I think it's the same. But not sure...now I'm curious. I do agree though that sticking it in the update method isn't nice either. I guess the smelliness is on Unity's end, unless someone comes up with a clean way to do this. \$\endgroup\$
    – Evorlor
    Feb 29, 2016 at 14:01
  • \$\begingroup\$ +1 for an alternative solution, but I hope others still can think of more. This is something that I have always struggled with in Unity \$\endgroup\$
    – Evorlor
    Feb 29, 2016 at 14:02
  • 1
    \$\begingroup\$ I suggest replacing your while loop with: yield return new WaitUntil(() => pc_anim.GetCurrentAnimatorStateInfo(0).normalizedTime < 1.0f); \$\endgroup\$
    – Evorlor
    Feb 29, 2016 at 14:12
4
\$\begingroup\$

Use StateMachineBehavior, there's OnStateExit

override public void OnStateExit (Animator animator, 
                                  AnimatorStateInfo stateInfo, 
                                  int layerIndex)
{
    if(stateInfo.IsName("attack")) return;
    pc_atttacking = false;
    pc_anim.SetBool("attack", false);
}

Create class (a new component) that inherit StateMachineBehaviour with above code, add the component to the gameobject that has the animation.

\$\endgroup\$
2
  • \$\begingroup\$ I could never really get this working on my loopless animation. Start would fire, but Exit would not. \$\endgroup\$
    – Norbert
    Feb 5, 2020 at 8:34
  • \$\begingroup\$ I think Exit will fire if it transitions to another state. Otherwise exit will not fire. So you can introduce a transition to "finished" state and either use "finished" state's enter, or your animation state's exit. \$\endgroup\$
    – hoistyler
    Oct 20, 2023 at 4:08
1
\$\begingroup\$

I normally use Animation Event Handlers

public Animator animator;
public AnimationClip AttackedAnimationClip;

private void Awake()
{
    AttackedAnimationClip.AddEvent(new AnimationEvent()
    {
        time = AttackedAnimationClip.length,
        functionName = "OnCompletedAttackedAnimation"
    });
}

public void OnAttacked()
{
    // this is initiated first
    animator.SetBool("isAttacked", true);
}

public void OnCompletedAttackedAnimation(string message)
{
    // called on completed animation
    animator.SetBool("isAttacked", false);
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .