End Events

The behaviour of an animation when its Time reaches the end of its Length is determined by whether its Loop Time toggle is enabled or not:

  • A looping animation will wrap back to the start to play through repeatedly.
  • A non-looping animation will freeze in place on the last frame.

In both cases, the animation is still playing and its time is still increasing. Animancer won't automatically stop the animation or play something else unless you tell it to, which is what End Events are for.

Event Types

The following table summarises the differences between End Events and the other event types in Animancer.

Event Type Summary
Animancer Events Each state has a list of general purpose events triggered on the frame when an animation's time passes a specific point.
End Events Each state has a single End Event triggered when the animation's time passes a specific point and every frame after that for as long as the animation continues playing. They're intended to let you play something else so the event will usually only trigger once anyway (End Events and Animancer Events don't trigger on states that are fading out unless AnimancerState.RaiseEventsDuringFadeOut is enabled).
ExitEvents A separate system for registering a callback to be triggered when an animation is interrupted. Using them is generally not recommended because most logic management systems such as Animancer's Finite State Machine already have better ways of controlling which states can interrupt each other and responding to interruptions. The ExitEvent API page explains how to use them.

When to use End Events

End Events are triggered every frame after the specified time has passed in order to ensure they can't be missed if the animation has already passed its end when you play the animation or register the event. But any event can stop an animation or play something else so if you don't want the special characteristics of an End Event then you can simply use a regular Animancer Event or Animation Event. For example:

public class DontUseEndEventExample : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private ClipTransition _Animation;

    public bool IsPlayingAnimation { get; private set; }
    
    private void OnAnimationEnd()
    {
        IsPlayingAnimation = false;
    }

The above method isn't actually telling Animancer to do something that would end the animation. Something else will probably check IsPlayingAnimation to determine when to end it, but Animancer has no way of knowing that so using it as an End Event would log OptionalWarning.EndEventInterrupt to indicate that you may have made a mistake in your logic. So you can either disable the warning or use a regular event:

    protected virtual void Awake()
    {
        // Disable the Optional Warning:
        OptionalWarning.EndEventInterrupt.Disable();
        _Animation.Events.OnEnd = OnAnimationEnd;
        
         // Or just use a regular Animancer Event:
        _Animation.Events.Add(1, OnAnimationEnd);
    }
    
    public void PlayAnimation()
    {
        IsPlayingAnimation = true;
        _Animancer.Play(_Animation);
    }
}

End Events in Transitions

If your script contains a Transition which it owns exclusively (not a Transition Asset which could be shared by multiple characters) then its OnEnd callback should be assigned on startup.

public class TransitionEndEventExample : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private ClipTransition _Animation;
    
    protected virtual void Awake()
    {
        _Animation.Events.OnEnd = OnAnimationEnd;
    }
    
    public void PlayAnimation()
    {
        _Animancer.Play(_Animation);
    }
    
    private void OnAnimationEnd()
    {
        Debug.Log(AnimancerEvent.Current.State + " Ended");
    }
}

Transitions also allow you to set their callback in the Inspector if you want to, but that's generally not ideal. End callbacks contain important logic and its usually safest to keep as much logic in your scripts as possible while using the Inspector to configure raw data such as the fade duration and end time.

End Events in States

If your script doesn't own a Transition as described above then an animation's OnEnd callback can be assigned after playing it.

[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private AnimationClip _Animation;

private void OnEndExample()
{
    AnimancerState state = _Animancer.Play(_Animation);
    state.Events(this).OnEnd ??= OnAnimationEnd;
}

private void OnAnimationEnd()
{
    Debug.Log(AnimancerEvent.Current.State + " Ended");
}
Syntax Effect Reason
??= This is a "Compound Assignment" which means it will only assign the callback if the existing value was null. Just using = would waste some performance allocating another delegate for it and causing additional Garbage Collection each time this method runs.
Events(this) Logs an error if any other script already accessed this state's events. If something else had already assigned the OnEnd callback then the ??= would mean this script never assigns its own.

Shared States

If Events(this) logs the error described above, that means you have multiple scripts trying to use the same AnimancerState. That may indicate in a bug in your code, but if it's intentional then you probably want to ensure that whichever script plays the state last assigns its own OnEnd callback.

public static readonly object SharedOwner = new();// Put this somewhere all scripts can access it.

[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private AnimationClip _Animation;

private Action _OnAnimationEnd;

private void OnEndExampleShared()
{
    AnimancerState state = _Animancer.Play(_Animation);
    state.Events(SharedOwner).OnEnd = _OnAnimationEnd ??= OnAnimationEnd;
}

private void OnAnimationEnd()
{
    Debug.Log(AnimancerEvent.Current.State + " Ended");
}
Syntax Effect Reason
public static readonly object SharedOwner = new(); Put this somewhere all scripts can access it so they can use state.Events(SharedOwner) instead of state.Events(this). So each script acknowledges that the state is shared.
private Action _OnAnimationEnd; A field to store this script's OnEnd callback in. To avoid wasting performance on allocating a new delegate and requiring additional Garbage Collection each time that method is called.
_OnAnimationEnd ??= OnAnimationEnd Use a compound assignment like above to create the delegate the first time it's used. Same as above.
...OnEnd = _OnAnimationEnd Use a regular = instead of ??= this time to always assign the field as the callback. This is on the same line as the previous step because C# resolves assignment operators from right to left. You can put them on separate lines if you find that syntax confusing.

You could also use state.OwnedEvents to skip the state ownership check, but that can lead to issues if some scripts are properly claiming ownership but others are ignoring it.

End Event Time

End Events are triggered every frame after the specified time has passed. This ensures that even if the animation has already passed the end when you play the animation or register the event, it will simply trigger next frame instead of not triggering at all and probably leaving the character stuck in that state.

By default, the End Event time is based on the state's speed: positive speed plays forwards and ends at the end of the animation (normalized time 1) while negative speed plays backwards and ends at the start of the animation (normalized time 0). In code, this is represented by the NormalizedEndTime being set to float.NaN but you can always assign a specific value.