01 Hybrid Basics

Difficulty: Beginner - Recommended after Playing and Fading

Location: Assets/Plugins/Animancer/Examples/09 Animator Controllers/01 Hybrid Basics

Namespace: Animancer.Examples.AnimatorControllers

This example demonstrates how you can manage some animations inside an Animator Controller while also using other separate Animation Clips on the same character. It compares the Native and Hybrid approaches described on the Animator Controllers page. The Hybrid Mini Game example extends this concept further.

Pro-Only Features are used in this example: Animator Controllers. Animancer Lite allows you to try out these features in the Unity Editor, but they are not available in runtime builds unless you purchase Animancer Pro.

The HybridBasics script simply exposes some public methods for various UI Buttons to call. All of the characters use the same script which calls regular functions on the Animator except for the Humanoid Hybrid character which calls similar functions on its HybridAnimancerComponent.

using Animancer;
using UnityEngine;

public sealed class HybridBasics : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private AnimationClip _SeparateAnimation;

    private static readonly int MoveParameterID = Animator.StringToHash("Move");

    public void SetMove(bool move)
    {
        if (_Animancer is HybridAnimancerComponent hybrid)
            hybrid.SetBool(MoveParameterID, move);
        else
            _Animancer.Animator.SetBool(MoveParameterID, move);
    }

    public void PlaySeparateAnimation()
    {
        _Animancer.Play(_SeparateAnimation);
    }

    public void PlayAnimatorController()
    {
        if (_Animancer is HybridAnimancerComponent hybrid)
            hybrid.Play(hybrid.Controller, 0);
        else
            _Animancer.Stop();
    }

    public void FadeSeparateAnimation()
    {
        _Animancer.Play(_SeparateAnimation, 0.25f);
    }

    public void FadeAnimatorController()
    {
        if (_Animancer is HybridAnimancerComponent hybrid)
            hybrid.PlayController();
        else
            _Animancer.Layers[0].StartFade(0, 0.25f);
    }
}

Creating the Animator Controllers

Creating the Animator Controller assets we need for this example is a relatively simple process:

  1. Right Click in the Project window and use the Create -> Animator Controller function. Give it a name and Double Click on it to open it in the Animator window:

  2. Right Click in the Animator window and use the Create State -> Empty function to create two states. Name them Idle and Walk (though their names aren't important for this example):

  3. Assign animations you want to the Motion field of each state:

  4. Go to the Parameters tab in the left panel and create a Bool parameter named Move (note that the capitalisation must match what you use in your scripts when referring to it):

  5. Right Click on the Idle state and use the Make Transition function then click on the Walk state to set the destination of the transition. Do the same thing again to create a transition from Walk back to Idle:

  6. Select the Walk -> Idle transition and disable its Has Exit Time toggle because we don't want the transition to be based on the state's time. Then scroll down, and add a Condition so that it gets triggered when the Move parameter is set to false. Do the same thing for Idle -> Walk for when the parameter is true:

  7. Select the character in the scene then drag and drop the Animator Controller you just created into the Controller field of their Animator component:

Internal Animations

Native Animator Controllers can be controlled using all the regular methods in the Animator component like Play, SetFloat, and GetCurrentAnimatorStateInfo.

These Animator Controllers have a Move parameter which controls the transitions between their Idle and Walk states which we can set using the Animator.SetBool method. If you already have a script that controls an Animator component directly it can keep doing so, but in this example we only have a reference to each character's AnimancerComponent so we simply access the Animator through it:

[SerializeField] private AnimancerComponent _Animancer;

public void SetMove(bool move)
{
    _Animancer.Animator.SetBool("Move", move);
}

Using the name of the parameter every time we want to access it is a bit inefficient though, so we can use Animator.StringToHash to get its ID on startup and use that value instead:

private static readonly int MoveParameterID = Animator.StringToHash("Move");

public void SetMove(bool move)
{
    _Animancer.Animator.SetBool(MoveParameterID, move);
}

Toggle

The other methods in this example are set up to be called by UI Buttons, but SetMove is actually called by a Toggle which works slightly different. Instead of an On Click event like Buttons have, it has an On Value Changed event with a bool parameter to indicate what value it was set to. This means that when selecting which method you want it to call, the SetMove method will appear twice:

Dynamic Bool Static Parameters
This is the one we want because it will pass the value of the Toggle component into the method. This one shows a toggle in the event itself and will pass that value into the method, regardless of the state of the Toggle component.

So the Toggle component's On Value Changed looks like this to call the SetMove method on all 4 characters in the scene:

Notice how those functions are out of order compared to the characters in the scene. The order they are listed is the order in which they will be called when the event is triggered which doesn't matter in this case, but if you want a serializable event system that allows you to easily reorder your functions (among many other improvements) you should check out UltEvents (a free replacement for UnityEvents).

External Animations

The Animator Controller doesn't have a Run state, but we can easily play a separate Run animation with Animancer just like in any of the other examples:

[SerializeField] private AnimationClip _SeparateAnimation;

public void PlaySeparateAnimation()
{
    _Animancer.Play(_SeparateAnimation);
}

Note how the AnimancerComponent Inspector shows that its "Playable is not initialised ..." until that method gets called, which creates a State managed by Animancer.

Then, when we want to go back to the Animator Controller we can just Stop Animancer:

public void PlayAnimatorController()
{
    _Animancer.Stop();
}

Specifically, the fact that Animancer's layers are now at 0 Weight is what prevents it from affecting the output anymore. So if you wanted to leave Animancer's animation playing in the background you could directly set the layer Weight instead of calling Stop.

Fading

We can also Fade in the external animation over time just like in the Playing and Fading example:

public void FadeSeparateAnimation()
{
    _Animancer.Play(_SeparateAnimation, 0.25f);
}

And Fade back to the Animator Controller by fading the Layer out just like in the Layers example:

public void FadeAnimatorController()
{
    _Animancer.Layers[0].StartFade(0, 0.25f);
}

Hybrid

Unfortunately, blending between a Native Animator Controller and Animancer is only possible on Generic Rigs. Trying to do it on a Humanoid Rig causes the Animator Controller to fully control the output when Animancer's Weight is at 0 but gives Animancer full control at any other Weight:

That's why there's a second Humanoid character which uses a HybridAnimancerComponent which can be controlled in essentially the same way as a regular Animator component. Normally you would do this by referencing the HybridAnimancerComponent directly, but since we are sharing the same script with all the other characters in this example we simply use Type Casts to access the members we need:

if (_Animancer is HybridAnimancerComponent hybrid)

That expression evaluates to true when the referenced component is a HybridAnimancerComponent and allows us to access its members using the hybrid variable.

So to set the Move parameter, we call SetBool on the HybridAnimancerComponent just like on the Animator:

public void SetMove(bool move)
{
    if (_Animancer is HybridAnimancerComponent hybrid)
        hybrid.SetBool(MoveParameterID, move);
    else
        _Animancer.Animator.SetBool(MoveParameterID, move);
}

Playing a separate animation is exactly the same for all characters:

public void PlaySeparateAnimation()
{
    _Animancer.Play(_SeparateAnimation);
}

Playing the Animator Controller is slightly different since we want Animancer to play the ControllerState rather than stop playing anything. The HybridAnimancerComponent.Controller property is actually a Transition which defines its own Fade Duration (which can be set in the Inspector), so if we want to immediately snap to the Animator Controller without fading we need to specify the fadeDuration as 0 when we call Play:

public void PlayAnimatorController()
{
    if (_Animancer is HybridAnimancerComponent hybrid)
        hybrid.Play(hybrid.Controller, 0);
    else
        _Animancer.Stop();
}

Fading a separate animation is exactly the same for all characters:

public void FadeSeparateAnimation()
{
    _Animancer.Play(_SeparateAnimation, 0.25f);
}

As with PlayAnimatorController above, we can just call hybrid.Play(hybrid.Controller); to have it use the Fade Duration set in the Inspector, but the HybridAnimancerComponent.PlayController method does exactly that with a bit less typing:

public void FadeAnimatorController()
{
    if (_Animancer is HybridAnimancerComponent hybrid)
        hybrid.PlayController();
    else
        _Animancer.Layers[0].StartFade(0, 0.25f);
}

Background

Native Animator Controllers continue executing in the background even while Animancer is overriding the output, which is often a waste of performance:

Note how the Move parameter can be set and its the Animator Controller's state will change, but it isn't affecting the actual output on the character model.

But ControllerStates (and therefore HybridAnimancerComponents) don't run in the background:

The Inspector progress bar on the ControllerState turned yellow to indicate that the state is paused and it has also been set to restart its Time from 0, but since commands on Animator Controllers don't take effect until the next frame, it will look like it's paused until that state next gets used. This can also cause Reliability issues if you try to control it immediately on the same frame you start playing it again because it treats everything since you stopped it as a single frame.

The Move parameter is still being set in this case, but the Animator window simply doesn't display the updated value while the ControllerState is inactive (note how the toggle in the Animator window immediately gets ticked to match the one in the top left when the Fade Animator Controller is clicked at the end).