Animancer Events

End Events are available in Animancer Lite but the ability to set a custom end time and use other events will not be available in runtime builds unless you purchase Animancer Pro. See the Feature Comparison for more information.

Animancer includes a custom event system which has various differences from Unity's Animation Events. Events can be configured in the Inspector when using Transitions as well as using Code. Note that there are several differences between regular events and End Events (which are explained on that page).

Events in Transitions

In addition to their other details, the serializable Transitions also allow you to configure Animancer Events in the Inspector:

  • The Transition Preview Window can be useful to see what the animated character will look like when the event occurs.
  • A transition defines how it fades in but not how it fades out (because that gets determined by the next transition), so the Inspector timeline display simply uses a default value. If the end time is less than the animation length, it shows the fade out ending at the end of the animation. Or if the end time is greater than the length, it shows the default 0.25 second fade.
  • Event Time Fields are always serialized as normalized time, regardless of which field you use to enter the value.
  • Every script using a Transition to play a particular animation will have its own AnimancerEvent.Sequence for that animation, but you can also use Transition Assets so that any number of things can reference the same asset and therefore share the same events. This can cause problems when each character wants to assign their own callbacks to the events, so you should generally use their UnShared classes to reference them.

Controls

  • You can select and edit events one at a time, or you can click the foldout arrow next to the timeline to instead show all of the events at once.
  • Double Click in the timeline to add an event at that time.
  • With an event selected:
    • Left and Right Arrows nudge the event time one pixel to the side.
    • Holding Shift moves 10 pixels at a time.
    • Space rounds off the event time by one digit. For example: 0.123 would become 0.12 and 0.999 would become 1.

Events in Code

Animancer Events can also be configured using code like so:

// MeleeAttack.cs:

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

public void Attack()
{
    // Play clears all events from all states (see Auto Clear below for details).
    var state = _Animancer.Play(_Animation);

    // Call OnHitStart to activate the hit box when the animation passes 40% of its length.
    state.Events.Add(0.4f, OnHitStart);

    // Call OnHitEnd to deactivate the hit box when the animation passes 60% of its length.
    state.Events.Add(0.6f, OnHitEnd);

    // Return to Idle when the animation finishes.
    state.Events.OnEnd = EnterIdleState;
}

See End Events for details about how the OnEnd event is different from others.

You can use Lambda Expressions and Anonymous Methods to define the code you want an event to run without needing to create another separate method:

void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
{
    var state = animancer.Play(clip);

    // All of the following options are functionally identical:

    // Lambda expression:
    state.Events.OnEnd = () =>
    {
        Debug.Log(clip.name + " ended");
    };

    // One-line Lambda expression:
    state.Events.OnEnd = () => Debug.Log(clip.name + " ended");

    // Anonymous method:
    state.Events.OnEnd = delegate()
    {
        Debug.Log(clip.name + " ended");
    };
}

AnimancerEvent.CurrentEvent and AnimancerEvent.CurrentState allow you to access the event currently being triggered and the state triggering it:

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

private void Awake()
{
    var state = _Animancer.Play(_Animation);
    state.Events.OnEnd = OnEnd;
}

public void OnEnd()
{
    Debug.Log(AnimancerEvent.CurrentState + " ended due to " + AnimancerEvent.CurrentEvent);
}

Hybrid Events

Setting up Events in Transitions is usually the best way to configure their timing according to the animation, but setting up Events in Code is often better for creating reliable scripts and avoiding bugs. Fortunately, you can have the best of both approaches by using a Transition to configure the Time but simply leaving its Callback blank and assigning it in Code instead.

The most reliable way to set up a hybrid event is generally to give it a Name in the Inspector so that you can identify it in code (consider using the Event Names Attribute):

[SerializeField] private ClipTransition _GolfSwing;

private void Awake()
{
    // Set the event named "Hit" to call the HitBall method when it is triggered.
    _GolfSwing.Events.SetCallback("Hit", HitBall);
}

private void HitBall() { ... }

Without the Name, you can still access the event by index:

_GolfSwing.Events.SetCallback(0, HitBall);

But that hard-codes the assumption that there will not be any other events before the one you want so it may cause a bug later on if you add more events to it.

The Golf Events example demonstrates how to use this feature.

Event Names

The EventNamesAttribute allows you to provide a specific set of names that events are allowed have, replacing the usual text field with a dropdown menu to avoid needing to type same event name in the script and in the Inspector.

Without Attribute With Attribute

Note that values selected using the dropdown menu are still stored as strings. Modifying the names in the script will NOT automatically update any values previously set in the Inspector.

The Avoiding Magic Strings section in the Golf Events example shows how to use it and API documentation for the EventNamesAttribute gives more details about the various different ways it can be used.

Clear Automatically

Calling AnimancerComponent.Play or Stop automatically clears the events of all states unless AnimancerState.AutomaticallyClearEvents is set to false. This ensures that you do not have to worry whether or not other scripts have used the same animation previously. Each script that plays an animation takes responsibility for managing what it expects to happen without worrying about the expectations of other scripts.

For example, if a character has an Attack animation which wants to return to Idle when it finishes but the character gets hit by an enemy in the middle of the Attack, the character will now want to play the Flinch animation and return to Idle after that instead. At that point, we no longer care about the end of the Attack animation. If we want to attack again, we just play the animation and register the callback again. But if the character has a special skill that lets them perform an attack combo which happens to include the same Attack animation followed by several others in sequence, it will not want that animation to still have the old End Event that returns to Idle.

That said, enforcing rules for which animations/actions are allowed to interrupt each other is often very important so that topic is covered in the Interrupt Management example.

The way that events are cleared differs slightly depending on how a state was played:

Direct Transition
AnimationClip clip;
AnimancerComponent animancer;
var state = animancer.Play(clip);
ClipTransition transition;
AnimancerComponent animancer;
var state = animancer.Play(transition);
Without a Transition, the state will have no events by default. When playing a Transition, the state will be given the AnimancerEvent.Sequence from that Transition.
Accessing its Events will give it a spare AnimancerEvent.Sequence from the ObjectPool which you can then modify on the spot. Accessing its Events will refer directly to the AnimancerEvent.Sequence owned by the Transition so any modifications you make will be retained when you play it again. This means modifications should generally only be performed once on startup rather than being repeated every time you play it.
void PlayAnimation()
{
    var state = animancer.Play(clip);
    state.Events.Add(...
}

This creates Garbage every time unless you cache the event callbacks.

void Awake()
{
    transition.Events.Add(...
}

void PlayAnimation()
{
    animancer.Play(transition);
}

When you play something else, the AnimancerEvent.Sequence will be removed from the state, have its contents cleared, and then be returned to the ObjectPool so it can be reused.

If you create a new AnimancerEvent.Sequence() and assign it manually, then it will not be cleared or pooled.
When you play something else, the AnimancerEvent.Sequence will be removed from the state but since it is owned by the Transition it will not be cleared or given to the ObjectPool.

Looping

Events behave differently depending on whether the animation is looping or not:

  • On a Non-Looping animation they will be triggered once on the frame when it passes the specified time.
  • On a Looping animation they will be triggered every loop on the frame when it passes the specified time.
    • If the animation is playing fast enough that multiple loops pass in a single frame, the event will be triggered the appropriate number of times. If you want to ensure that your callback only gets triggered once per frame, you can store the AnimancerPlayable.FrameID and check if it has changed each time your method is called.
    • Looping Events must be within the range of 0 <= normalizedTime < 1 in order to function correctly. Events outside that range will cause an ArgumentOutOfRangeException during the next update.
    • AnimationEvent.AlmostOne is a constant containing the largest possible float value less than 1 in case you want to set an event right at the end of the loop.
  • End Events will be triggered on every frame when it has passed the specified time, regardless of whether the animation is looping or not.

Other Details

This system has several other details worth mentioning:

  • Events are triggered using the AnimancerEvent.Invoke method which sets the static AnimancerEvent.CurrentEvent and AnimancerEvent.CurrentState, allowing anything to access the details of the event and the state that triggered it before being cleared immediately afterwards.
  • Changing the AnimancerState.Time prevents that state from triggering any more events during that frame (since its time will stay at the assigned value for that frame).
  • The AnimancerState.Events sequence can not be modified by its own events.
  • Animancer Events work with Mixers. Blend Trees will trigger regular Animation Events on all of the AnimationClips they contain, but this allows events to be placed on the MixerState itself so they get triggered according to the weighted average normalized time of the mixed states.
  • They also technically work with Controller States, though they are tied to the overall ControllerState and do not check what the Animator Controller is doing internally.
  • If you want to run your own code as part of the animation update, you can implement IUpdatable.

Garbage

Animancer Events use Delegates as a very convenient way to determine what each event actually does, however they also have the downside of needing to be Garbage Collected when they are no longer being used which happens quite often since events are Automatically Cleared whenever a new animation is played.

Garbage Collection can be avoided by "caching" your variables, which in this case means you simply create your delegates once (usually on startup) and reuse the same ones every time:

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

private System.Action _OnAnimationEnd;

void Awake()
{
    _OnAnimationEnd = () => Debug.Log("Animation ended");
}

void PlayAnimation()
{
    _Animancer.Play(_Animation).Events.OnEnd = _OnAnimationEnd;
}

The delegate stored in _OnAnimationEnd will not be garbage collected until either it is set to something else (null or a different method) or until the whole object that it is in gets garbage collected.

Transitions have their own events which do not get cleared and are automatically assigned to the state whenever it is played, so adding your events to them is another form of caching which is often more convenient than having a separate field to store the delegate in:

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

void Awake()
{
    _Animation.Events.OnEnd = () => Debug.Log("Animation ended");
}

void PlayAnimation()
{
    _Animancer.Play(_Animation);
}

You can also make your own AnimancerEvent.Sequence like so:

// MeleeAttack.cs:

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

private AnimancerEvent.Sequence _Events;

private void Awake()
{
    // Specifying the capacity is optional and it will still expand if necessary.
    // But if the number of elements is known, specifying it is more efficient.
    _Events = new AnimancerEvent.Sequence(2);
    _Animation.Events.Add(0.4f, OnHitStart);
    _Animation.Events.Add(0.6f, OnHitEnd);

    // The End Event is not included in the capacity.
    _Animation.Events.OnEnd = EnterIdleState;
}

public void Attack()
{
    var state = _Animation.Play(_Animation);
    state.Events = _Events;
}

Your event sequence will be removed from the state when a different animation is played, but it will not be cleared so you can simply re-assign it next time.

UltEvents

By default, Events in Transitions use UnityEvents to define their callbacks. However, the system can be modified to use any other similar type of events. In particular, UltEvents have some significant advantages over UnityEvents so if you want to use them you can do the following:

  1. Import Animancer Pro and UltEvents into the same project.
  2. Select the Assets/Plugins/Animancer/Animancer.asmdef and add a Reference to the UltEvents Assembly Definition.
  3. Go into the Player Settings of your project and add ANIMANCER_ULT_EVENTS as a Scripting Define Symbol. Or you can simply edit the AnimancerEvent.Sequence.Serializable.cs script to change the event type.

Note that the serialized data structure of UnityEvents and UltEvents is entirely different so swapping between them will cause all existing events to lose their data. If you can think of a better way to implement it so that both systems can be used separately, please use the contact options listed in the Help page.