Animancer v6.0 is currently available for testing.

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 sequence of events 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 (though using Shared Event Sequences does have some potential issues to watch out for).

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);
}

Shared Event Sequences

Every object that plays a particular Transition will share the same event sequence, meaning that any modifications made by one object will affect the others as well. This usually happens when using Transition Assets to share the same transition among multiple objects but still needing to give them events specific to each object. Animancer will log a warning if you add the same event to a sequence multiple times with a link leading here so you can try any of the following workarounds:

The simplest workaround is to just Instantiate a copy of the Transition Asset on startup:

[SerializeField] private ClipTransition _Animation;

private void Awake()
{
    _Animation = Instantiate(_Animation);
    // Now you can modify the _Animation.Transition.Events as necessary.
}

That gives each object a separate copy of the transition so they no longer share the same event sequence, but wastes some additional processing time and memory because all the other transition data gets duplicated as well.

A more efficient solution is to make your own copy of the event sequence for each object:

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

private AnimancerEvent.Sequence _Events;

private void Awake()
{
    // Initialise your events by copying the sequence from the transition.
    _Events = new AnimancerEvent.Sequence(_Animation.Transition.Events);
    // Now you can modify the _Events as necessary.
}

private void PlayAnimation()
{
    // Then when you play it, you just replace the transition events with your own:
    var state = _Animancer.Play(_Animation);
    state.Events = _Events;
}

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:

[SerializeField] private ClipState.Transition _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.

Auto Clear

Calling AnimancerComponent.Play or Stop automatically clears the events of all states to ensure that you do not have to worry about other scripts that might 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.

This usually means that only one state on each object will actually have events at any given time so AnimancerEvent.Sequences are recycled in an ObjectPool:

  • Accessing the AnimancerState.Events property will get an event sequence from the pool if it does not already have one.
  • You can assign your own AnimancerEvent.Sequence to that property and it will then be removed from the state when the events are cleared (but since you made the sequence it will not be cleared or added to the pool).
  • Transitions each have their own sequence which they assign whenever they are played.

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 it is covered in the Interrupt Management example.

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 ClipState.Transition _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.

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.

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

UltEvents

By default, this system uses UnityEvents to define the event callbacks. However, it 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 (this requires Animancer Pro):

  1. Import Animancer 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.