Search Unity

Force AnimationEvents to Fire

Discussion in 'Animation' started by JoshuaMcKenzie, Feb 8, 2017.

  1. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    I've been scouring around for a way to make unity forcibly fire AnimationEvents. even if the Animation clip is in transition,.. even if the clip was blended... fire the event anyway. suffice to say I couldn't find a built in way to do that.

    then I came across this Unite Talk were the speaker William Armstrong mentioned that he had the same issue but he got around it by simply implementing his own system for dispatching AnimationEvents.

    Time: 21:09 is where he starts talking about it.
    if you look closely on the next slide you can see that it appears that he made his own version of AnimationEvent which he fires manually.

    Looking around I couldn't find if he had made this class public so I decided to write my own today. and I'm sharing it with the community (lord knows I searched all afternoon on these forums for something like this)

    I liked Will's idea that the events are binded to the States, not the animation clips which gave me a little cleaner workflow.Iterating newer clips in often meant I had to add in AnimationEvents again. other times I had to make duplicate clips where one version had Animation events and the other didn't and which clip was used depended on the State that was playing. However binding the events directly to the States gave more import stability and eliminated the duplicate Clips
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. namespace WoofTools.StateMachineBehaviours
    7. {
    8.     /// <summary>
    9.     /// This Class ensures that Animation Events are fired even during transitions and on 0 weight layers
    10.     /// </summary>
    11.     public class SMB_ForceAnimationEvent: StateMachineBehaviour
    12.     {
    13.         [SerializeField]public StateEvent[] OnEnterEvents;
    14.         [SerializeField]public StateEvent[] OnUpdateEvents;
    15.         [SerializeField]public StateEvent[] OnExitEvents;
    16.  
    17.         //ensure OnUnpdateEvents are sorted by order of normalizedTime incase multiple events fire during the same update
    18.         protected virtual void OnEnable()
    19.         {
    20.             if (OnUpdateEvents != null)
    21.                 OnUpdateEvents = OnUpdateEvents.OrderBy(e=>e.NormalizedTime).ToArray();
    22.         }
    23.      
    24.         //the class needs to track the last normalizedTime used so that it can cleanly issue events
    25.         private Dictionary<Animator,float> NormalizedTimePerAnimator = new Dictionary<Animator, float>();
    26.      
    27.         //remove any Animators that were deleted while in this state and thus weren't cleaned up
    28.         private void CleanDictionary()
    29.         {
    30.             NormalizedTimePerAnimator = NormalizedTimePerAnimator.Where(kvp=>(bool)kvp.Key).ToDictionary(kvp=>kvp.Key,KeyValuePair=>KeyValuePair.Value);
    31.         }
    32.         private float GetNormalizedTime(Animator animator)
    33.         {
    34.             float time=-1;
    35.             if(!NormalizedTimePerAnimator.TryGetValue(animator,out time))
    36.             {
    37.                 //if we couldn't find the animator assume its a good time to make sure any null keys are cleaned out
    38.                 CleanDictionary();
    39.              
    40.                 //sets time to -1 to ensure any OnUpdateEvents with a time of 0 will fire
    41.                 NormalizedTimePerAnimator.Add(animator,time);
    42.             }
    43.          
    44.             return time;
    45.         }
    46.         private void SetNormalizedTime(Animator animator, AnimatorStateInfo stateInfo)
    47.         {
    48.             NormalizedTimePerAnimator[animator] = stateInfo.normalizedTime;
    49.         }
    50.         private void SetNormalizedTime(Animator animator, float normalizedTime)
    51.         {
    52.             NormalizedTimePerAnimator[animator] = normalizedTime;
    53.         }
    54.      
    55.         public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    56.         {
    57.             if(animator==null) return;
    58.          
    59.             SetNormalizedTime(animator,  -1);
    60.          
    61.             for(int i=0;i<OnEnterEvents.Length;i++)
    62.             {
    63.                 OnEnterEvents[i].Invoke(animator);
    64.             }
    65.         }
    66.      
    67.      
    68.         public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    69.         {
    70.             if(animator==null) return;
    71.          
    72.             float lastTime = GetNormalizedTime(animator);
    73.             float currentTime = stateInfo.normalizedTime;
    74.          
    75.             //the variable name technically not accurate but more concise than "has passed the first animation iteration"
    76.             // An animation that doesn't have an exit transition and doesn't loop will pause on the last frame, but the
    77.             // normalized time will still rise past 1.0f. that distinction is important in this function
    78.             bool looping = lastTime>1;
    79.          
    80.             if(looping)
    81.             {
    82.                 //if the state doesn't loop and is considered looping then that means
    83.                 // this function should do nothing (all OnUpdateEvent should have already been called)
    84.                 if(!stateInfo.loop) return;
    85.              
    86.                 //modulo the times in a space for the events' times
    87.                 lastTime = lastTime%1 - 1f;// this will make lastTime negative, which is important for the equality checks in the for loop
    88.                 currentTime = currentTime%1;
    89.             }
    90.          
    91.          
    92.             for(int i=0;i<OnUpdateEvents.Length;i++)
    93.             {
    94.                 float eventTime = OnUpdateEvents[i].NormalizedTime;
    95.              
    96.                 //if the event shouldn't fire in a looped animation, skip it
    97.                 if(looping && !OnUpdateEvents[i].RepeatOnLoop) continue;
    98.              
    99.                 //if lastTime is greater than the eventTime then its assumed to have already been fired in some previous frame, so skip it
    100.                 if(lastTime>eventTime) continue;
    101.              
    102.                 //if currentTime is less than the eventTime then the event triggers later, skip
    103.                 if(currentTime<eventTime) continue;
    104.              
    105.                 OnUpdateEvents[i].Invoke(animator);
    106.             }
    107.          
    108.             SetNormalizedTime(animator,  stateInfo);
    109.         }
    110.      
    111.      
    112.      
    113.         public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    114.         {
    115.             if(animator==null) return;
    116.          
    117.             #region call UpdateEvents that need to be fired regardless of exiting
    118.             float lastTime = GetNormalizedTime(animator);
    119.             bool looping = lastTime>1;
    120.          
    121.             if(looping)
    122.                 lastTime = (!stateInfo.loop)?2:lastTime%1 - 1f;
    123.          
    124.             for(int i=0;i<OnUpdateEvents.Length;i++)
    125.             {
    126.                 if(!OnUpdateEvents[i].ForceCallOnExit) continue;
    127.                 if(looping && !OnUpdateEvents[i].RepeatOnLoop) continue;
    128.                 if(lastTime>OnUpdateEvents[i].NormalizedTime) continue;
    129.              
    130.                 OnUpdateEvents[i].Invoke(animator);
    131.             }
    132.             #endregion
    133.          
    134.          
    135.             for(int i=0;i<OnExitEvents.Length;i++)
    136.             {
    137.                 OnExitEvents[i].Invoke(animator);
    138.             }
    139.          
    140.             //we are leaving the state so this animator is no longer being used here, thus the state machine behaviour no longer needs to track it
    141.             NormalizedTimePerAnimator.Remove(animator);
    142.         }
    143.      
    144.         [System.Serializable]public class StateEvent
    145.         {
    146.             public enum ArgumentType{none, boolType, floatType,intType,stringType,UnityObjectType}
    147.  
    148.  
    149.  
    150.             [Tooltip("Should the event be fired? Useful for disabling events for debugging.")]
    151.             [SerializeField]protected bool m_enabled = true;
    152.             [Tooltip("At what point in an StateUpdate should the event fire, only used for OnUpdateEvents.")]
    153.             [SerializeField][Range(0f,1f)]protected float m_normalizedTime = 0.5f;
    154.             [Tooltip("Should the event be called regardless if the state is being exited? only used for OnUpdateEvents")]
    155.             [SerializeField]protected bool m_ForceCallOnExit = true;
    156.             [Tooltip("Should the event be called in each loop or just on the first iteration?")]
    157.             [SerializeField]protected bool m_repeatOnLoop = true;
    158.             [Tooltip("Which function to call when sending the event")]
    159.             [SerializeField]protected string m_FunctionName = string.Empty;
    160.             [Tooltip("the type of Parameter to send")]
    161.             [SerializeField]protected ArgumentType m_parameterType = ArgumentType.stringType;
    162.             [SerializeField]protected bool m_boolParameter = false;
    163.             [SerializeField]protected float m_floatParameter = 0f;
    164.             [SerializeField]protected int m_intParameter = 0;
    165.             [SerializeField]protected string m_stringParameter = string.Empty;
    166.             [SerializeField]protected UnityEngine.Object m_objectParameter = null;
    167.          
    168.             public float NormalizedTime{get{return m_normalizedTime;}}
    169.             public bool RepeatOnLoop{get{return m_repeatOnLoop;}}
    170.             public bool ForceCallOnExit{get{return m_ForceCallOnExit;}}
    171.             public string FunctionName{get{return m_FunctionName;}}
    172.             public ArgumentType ParameterType{get{return m_parameterType;}}
    173.             public bool BoolParameter{get{return m_boolParameter;}}
    174.             public float FloatParameter{get{return m_floatParameter;}}
    175.             public int IntParameter{get{return m_intParameter;}}
    176.             public string StringParameter{get{return m_stringParameter;}}
    177.             public UnityEngine.Object ObjectParameter{get{return m_objectParameter;}}
    178.          
    179.             private static SendMessageOptions sendType = SendMessageOptions.DontRequireReceiver;
    180.          
    181.             public void Invoke(Animator animator)
    182.             {
    183.                 if(!m_enabled) return;
    184.                 if(animator == null) return;
    185.                 if(string.IsNullOrEmpty(m_FunctionName)) return;
    186.              
    187.                 switch(m_parameterType)
    188.                 {
    189.                     case ArgumentType.boolType:
    190.                         animator.SendMessage(m_FunctionName,m_boolParameter,sendType);
    191.                         break;
    192.                     case ArgumentType.floatType:
    193.                         animator.SendMessage(m_FunctionName,m_floatParameter,sendType);
    194.                         break;
    195.                     case ArgumentType.intType:
    196.                         animator.SendMessage(m_FunctionName,m_intParameter,sendType);
    197.                         break;
    198.                     case ArgumentType.stringType:
    199.                         animator.SendMessage(m_FunctionName,m_stringParameter,sendType);
    200.                         break;
    201.                     case ArgumentType.UnityObjectType:
    202.                         animator.SendMessage(m_FunctionName,m_objectParameter,sendType);
    203.                         break;
    204.                     case ArgumentType.none:
    205.                     default:
    206.                         animator.SendMessage(m_FunctionName,sendType);
    207.                         break;
    208.                 }
    209.             }
    210.         }
    211.     }
    212. }
    213.  
    214.  
    it uses a concept of saving the last used normalizedTime (per animator per state behaviour) so that events should fire reliably even if the animation speed is high enough to skip frames, and even fire events with a time set to 0 or 1 (which even normal AnimationEvents occasionally miss).
    Probably not the same implementation that William used for his game but so far this has fixed the issues that I've came across today. So I decided that I release this bit of code to the forums.

    Later I plan to add in Editor Scripting (never actually done that for StateMachineBehaviours) and more invoke options like multiple parameters and more datatypes
     
    Last edited: Mar 26, 2017
  2. cdr9042

    cdr9042

    Joined:
    Apr 22, 2018
    Posts:
    173
    Hi, thank you, this is amazing, it's very useful for me. Did you add in the Editor Scripting for it?
    To fire an event at the last frame, I use the OnUpdateEvents and set the normalized time to 1.0, right?
     
  3. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    I did, the current Editor script is using my Editor Scripting Codebase filled with custom property drawers and Extension methods

    It depends exactly what you mean by last frame. if for example you want to fire the event on the last frame of the clip itself, then yes you can set normalized time to 1 and it should work as you intended.

    However by default if the animation state is exited before the triggering frame, then the event won't fire. This allows for a mechanic where you want to interrupt an enemy that is preparing a powerful move. On the other hand, there is an option to force the event to fire even if interrupted, giving a sort of animation cancel mechanic. And if the animation loops, there is also an option so control if you the event to only fire once (on the first iteration)

    However, if say the animation is a looping animation and you want to always fire the event, and only once when the animation stops (regardless of where in the animation it actually stops), then you are better off using OnExit.
     
    cdr9042 likes this.
  4. k_dunlop

    k_dunlop

    Joined:
    May 10, 2019
    Posts:
    16
    just popping in to say THANKS! your script is extremely handy.
     
  5. razzraziel

    razzraziel

    Joined:
    Sep 13, 2018
    Posts:
    396
    Yeah useful class to build on. Adding this comment to look later if I need StateMachineBehaviours