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. The following table summarises the differences between them and the other event types in Animancer.

System 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 an animation's time passes a specific point and every frame after that for as long as the animation continues playing.
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.

Despite the name, End Events don't inherently do anything to end the animation. You can do anything you want using the callback, but if Animancer isn't able to detect that you have done something to end the animation (usually by playing something else) then it will log OptionalWarning.EndEventInterrupt to help identify a potential issue in your setup. If you intend to use the event for something else then you can simply disable that warning as it explains.

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 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.