Preventing AnimationEvent during State Transition By Changing AnimationEvent Time
To prevent event from firing when animator is transitioning to another state, one can utilize the StateMachineBehaviour
public class StateListener : StateMachineBehaviour
Since there’s no OnStateChange function in StateMachineBehaviour, we need to check everyframe if the animator.GetNextAnimatorClipInfo clip is changed inside the OnStateUpdate function.
First, set the currentClip inside the OnStateEnter hook. Note that we only process the State only when current Layer weight is not 0
AnimationEvent currentClip;
AnimationEvent prevClip;
public override void OnStateEnter(Animator animator, AnimatorStateInfo animatorStateInfo, int layerIndex)
{
if(animator.GetLayerWeight(layerIndex) == 0)
return;
eventsToInvoke.Clear();
for(int i = 0; i < animator.GetNextAnimatorClipInfo(layerIndex).Length; i++)
{
AnimationClip clip = animator.GetNextAnimatorClipInfo(layerIndex)[i].clip;
currentClip = clip;
}
}
Then, check for any change in the animation clip inside the OnStateUpdate function. If the clip changed, iterate through the clip’s animation event to check if the event function name is inside the skippable event list. If it’s inside the list, then negate the animation event time by multiplying with -1 to prevent it from firing.
Note that using OnStateUpdate means that it will check everyframe. To avoid this, we can also use alternative function to check for state change [Triggering from Other State’s OnEnterState](Detecting Animation State Change in StateMachineBe 68604e4ad1a34952bc54e7e5b3262f4f.md)
public override void OnStateUpdate(Animator animator, AnimatorStateInfo animatorStateInfo, int layerIndex)
{
if(animator.GetLayerWeight(layerIndex) == 0)
return;
for(int i = 0; i < animator.GetNextAnimatorClipInfo(layerIndex).Length; i++)
{
AnimationClip clip = animator.GetNextAnimatorClipInfo(layerIndex)[i].clip;
if(currentClip != clip)
{
if(currentClip?.events != null)
{
//Copy animationEvents to a list
modifiedAnimationEvents = currentClip.events.ToList();
foreach(string skippableEvent in skippableEvents)
{
int indexToBeRemoved = modifiedAnimationEvents.FindIndex(e => string.Compare(e.functionName, skippableEvent) == 0);
//if animation event is in skippable list, negate the event firing time so it will never be fired
if(indexToBeRemoved >= 0)
modifiedAnimationEvents[indexToBeRemoved].time = -Mathf.Abs(modifiedAnimationEvents[indexToBeRemoved].time);
}
currentClip.events = modifiedAnimationEvents.ToArray();
}
prevClip = currentClip;
currentClip = clip;
}
}
}
Then revert back the animationEvent firing time on exiting the State. Note that we use prevClip since the currentClip had been swapped in the OnStateUpdate above
public override void OnStateExit(Animator animator, AnimatorStateInfo animatorStateInfo, int layerIndex)
{
//Restore prevclip events time;
if(prevClip)
{
AnimationEvent[] events = prevClip.events;
foreach(AnimationEvent animationEvent in events)
animationEvent.time = Mathf.Abs(animationEvent.time);
prevClip.events = events;
}
}
Assigning script to Animator State
To use the script, we need to select the animator, be it an asset or already attached to gamObject, then select the AnimationState that need the behaviour and click on Add Behaviour. Then add the Event function name that need to be prevented from firing into Skippable Event list.
Heads Up
Note that using this method modifies the animationClip directly, which means that any animator that share the same animationClip will be affected too.
To work around this, we can duplicate the animationClip at runtime on OnStateEnter hook, and assign it into the AnimatorOverrideController then assign the animatorOverrideController into animator’s runtime animator controller.
public override void OnStateEnter(Animator animator, AnimatorStateInfo animatorStateInfo, int layerIndex)
{
if(animator.GetLayerWeight(layerIndex) == 0)
return;
eventsToInvoke.Clear();
for(int i = 0; i < animator.GetNextAnimatorClipInfo(layerIndex).Length; i++)
{
AnimationClip clip = animator.GetNextAnimatorClipInfo(layerIndex)[i].clip;
currentClip = clip;
//Only duplicate clip that hasn't been duplicated yet (not in the list)
if(duplicatedClip.FindIndex(o => o == currentClip) < 0)
{
if(animator.runtimeAnimatorController is AnimatorOverrideController)
{
AnimatorOverrideController newController = new AnimatorOverrideController(animator.runtimeAnimatorController);
List<KeyValuePair<AnimationClip, AnimationClip>> clips = new List<KeyValuePair<AnimationClip, AnimationClip>>(newController.overridesCount);
((AnimatorOverrideController) animator.runtimeAnimatorController).GetOverrides(clips);
//Find clip that needs to be overwritten with duplicated clip
int clipIndex = clips.FindIndex(o => o.Value == currentClip || o.Key == currentClip);
//Duplicate clip
if(clipIndex >= 0)
clips[clipIndex] = new KeyValuePair<AnimationClip, AnimationClip>(clips[clipIndex].Key, Object.Instantiate(currentClip));
newController.ApplyOverrides(clips);
animator.runtimeAnimatorController = newController;
//replace currentClip with the duplicated one
currentClip = clips[clipIndex].Value;
//Add duplicated clip to list so it wont get duplicated again
duplicatedClip.Add(currentClip);
}else
Debug.LogError("Animator does not have AnimatorOverrideController assigned. Please assign AnimatorOverrideController into animator.runtimeAnimatorController"); }
foreach (AnimationEvent animEvent in clip.events)
{
if(skippableEvents.IndexOf(animEvent.functionName) < 0)
eventsToInvoke.Add(animEvent);
}
}
}
Note : for the script above to work, the animator need to have AnimatorOverrideController assigned into animator.runTimeController
[SerializeField] AnimatorOverrideController animatorOverrideController
void Start()
{
GetComponent<Animator>().runtimeAnimatorController = animatorOverrideController;
}
Pros :
- Easier to use for gameplay programmer.
Cons :
- Need to have AnimatorOverrideController assigned into animator.runTimeController
- Higher memory usage due to duplicated animationClip(s)
No Comments