08-01 Hybrid Character

Location: Samples/08 Animator Controllers/01 Hybrid Character

Recommended After: Basic Character

Learning Outcomes: in this sample you will learn:

How to use a HybridAnimancerComponent.

How to play an Animator Controller alongside Animancer.

How to play an Animator Controller inside Animancer.

How to disable OptionalWarnings when you understand the risks they're warning about.

Summary

This sample demonstrates the same behaviour as the Basic Character sample except that it plays the Idle and Walk animations inside an Animator Controller instead of playing them directly.

Overview

The general code structure is similar to the Basic Character sample, except that it uses a HybridAnimancerComponent to play an Animator Controller containing the Idle and Walk animations instead of referencing those animations directly in the script.

  • It's using the Hybrid setup by default, where a HybridAnimancerComponent references the Animator Controller instead of having a regular AnimancerComponent.
  • You can easily change it to use the Native setup by giving the Animator Controller to the Animator component instead.

HybridCharacterAnimations only references the Action animation since the others are inside the Animator Controller.
[DefaultExecutionOrder(AnimancerComponent.DefaultExecutionOrder - 1000)]
public class HybridCharacterAnimations : MonoBehaviour
{
    public static readonly int IsMovingParameter = Animator.StringToHash("IsMoving");

    [SerializeField] private HybridAnimancerComponent _Animancer;
    [SerializeField] private ClipTransition _Action;

    private State _CurrentState;

    private enum State
    {
        NotActing,
        Acting,
    }

    protected virtual void Awake()
    {
        _Action.Events.OnEnd = UpdateMovement;

        OptionalWarning.NativeControllerHumanoid.Disable();
        OptionalWarning.NativeControllerHybrid.Disable();
    }

    protected virtual void Update()
    {
        switch (_CurrentState)
        {
            case State.NotActing:
                UpdateMovement();
                UpdateAction();
                break;

            case State.Acting:
                UpdateAction();
                break;
        }
    }

    private void UpdateMovement()
    {
        _CurrentState = State.NotActing;

        float forward = SampleInput.WASD.y;
        bool isMoving = forward > 0;

        if (_Animancer.Animator.runtimeAnimatorController != null)
        {
            AnimancerLayer layer = _Animancer.Layers[0];
            if (layer.TargetWeight > 0)
                layer.StartFade(0, 0.25f);

            _Animancer.Animator.SetBool(IsMovingParameter, isMoving);
        }
        else if (_Animancer.Controller.Controller != null)
        {
            _Animancer.PlayController();

            _Animancer.SetBool(IsMovingParameter, isMoving);
        }
        else
        {
            Debug.LogError("No Animator Controller is assigned.", this);
        }
    }

    private void UpdateAction()
    {
        if (SampleInput.LeftMouseUp)
        {
            _CurrentState = State.Acting;
            _Animancer.Play(_Action);
        }
    }
}

The script supports both the Native and Hybrid setups.

Native Hybrid
Animator Controller assigned to the Animator Animator left empty.
HybridAnimancerComponent not used (you would use a regular AnimancerComponent). Animator Controller assigned to the HybridAnimancerComponent

Optional Warnings

In order to demonstrate the difference between the Native and Hybrid approaches to using Animator Controllers, this sample does a few things that would cause OptionalWarnings so the script simply disables them to prevent new users from thinking the sample is broken.

[DefaultExecutionOrder(AnimancerComponent.DefaultExecutionOrder - 1000)]
public class HybridCharacterAnimations : MonoBehaviour
{
    protected virtual void Awake()
    {
        ...

        OptionalWarning.NativeControllerHumanoid.Disable();
        OptionalWarning.NativeControllerHybrid.Disable();
    }
  • The [DefaultExecutionOrder] attribute causes the script to initialize before Animancer so that it can disable those warnings before they would be triggered.
  • OptionalWarning.NativeControllerHumanoid would be triggered for using a Native with a Humanoid Rig because Unity doesn't allow it to blend properly when fading to and from Animancer.
  • OptionalWarning.NativeControllerHybrid would be triggered for having the Animator Controller assigned to the Animator while a HybridAnimancerComponent is present (because using the Hybrid component implies that you intend to give the Animator Controller to it instead).

Parameter Hashes

Parameters and states in an Animator Controller are internally identified by hashes. They can be referred to using strings, but that's inefficient because it needs to recalculate the hash every time. So it's more efficient to store the necesary hashes in static fields like this:

public static readonly int IsMovingParameter = Animator.StringToHash("IsMoving");

That also has the benefit of keeping all the magic strings in one place instead of scattered throughout the script.

Manually writing and maintaining those hashes can be tedious and offers many opportunities for spelling mistakes, so Weaver has a system for procedurally generating them which also creates a HashToString method for converting a hash value back to the string it came from. That system is included in Weaver Lite for FREE.

Fields

To use the Hybrid approach we need a reference to the HybridAnimancerComponent, but if we were using the Native approach we would only need a regular AnimancerComponent.

[SerializeField] private HybridAnimancerComponent _Animancer;
[SerializeField] private ClipTransition _Action;

As noted earlier, the Idle and Walk animations are inside the Animator Controller so the script doesn't know about them except for the IsMovingParameter.

Script

The rest of the script is identical to BasicCharacterAnimations from the Basic Character sample except for the UpdateMovement method:

private State _CurrentState;

private enum State
{
    NotActing,
    Acting,
}

protected virtual void Awake()
{
    _Action.Events.OnEnd = UpdateMovement;

    ...
}

protected virtual void Update()
{
    switch (_CurrentState)
    {
        case State.NotActing:
            UpdateMovement();
            UpdateAction();
            break;

        case State.Acting:
            UpdateAction();
            break;
    }
}

private void UpdateAction()
{
    if (SampleInput.LeftMouseUp)
    {
        _CurrentState = State.Acting;
        _Animancer.Play(_Action);
    }
}

The UpdateMovement method starts out similarly to set the state and check if the character should be moving:

private void UpdateMovement()
{
    _CurrentState = State.NotActing;

    float forward = SampleInput.WASD.y;
    bool isMoving = forward > 0;

Then it branches to control the Animator Controller differently depending on which approach is being used.

Native Hybrid
if (_Animancer.Animator
    .runtimeAnimatorController != null)
{
else if (_Animancer.Controller
    .Controller != null)
{
To return to the Animator Controller after playing something else, we fade out Animancer's layers. To return to the Animator Controller after playing something else, we play the ControllerTransition.
    AnimancerLayer layer = 
        _Animancer.Layers[0];

    if (layer.TargetWeight > 0)
        layer.StartFade(0, 0.25f);
    _Animancer.PlayController();
We control the Animator Controller through the Animator component as we normally would without Animancer. We control the Animator Controller through the HybridAnimancerComponent.
    _Animancer.Animator.SetBool(
        IsMovingParameter,
        isMoving);
}
    _Animancer.SetBool(
        IsMovingParameter,
        isMoving);
}

Native

Using the Native approach causes the Live Inspector to show the Animator Controller above Animancer's usual layers.

  • The Idle and Walk animations play at their regular speed even though Animancer's Speed slider is set to 0.5. That's because the Animator.speed is entirely separate from the AnimancerGraph.Speed.
  • The Animator Controller is still playing during the Shoot animation as if it were on a different Layer.
  • The Shoot animation doesn't fade in because Animancer knows it won't work on a Humanoid Rig and even when the script explicitly tells it to fade out, the character spends the whole fade duration on the last pose of the Shoot animation until Animancer's Weight reaches 0 and the Animator Controller takes effect again.

This approach can potentially allow Animancer to be used alongside other systems that weren't designed for Animancer without needing to modify them, however the inability to smoothly fade between systems often makes this option unattractive for Humanoid characters.

Hybrid

Using the Hybrid approach plays the Animator Controller inside Animancer like any other state.

As noted above, this approach has all the same methods for setting parameters and controlling the Animator Controller in code, but they need to be called on the HybridAnimancerComponent so a system not designed for Animancer will need some modifications to be used in this way.

What Next?

Sample Topic
3D Game Kit Converting the logic from Unity's 3D Game Kit Lite to use Animancer instead. Basically the opposite of this sample and with a much more complex character.