04-02 Directional Mixers

Location: Samples/04 Mixers/02 Directional Mixers

Recommended After: Linear Mixers

Learning Outcomes: in this sample you will learn:

How to use 2D Mixers to allow blended movement in any direction.

How to use Transition Assets, and Parameters.

How to move around the scene with Root Motion.

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

Summary

This sample uses a 2D Directional Mixer to allow the character to move forwards, backwards, sideways, and anywhere inbetween. It also uses Root Motion to move around the scene.

Overview

In order to keep the character's logic separate from their visuals, it's often a good idea to keep the model (AnimancerHumanoid) as a child of the root object (Character) where the main logical and physics components are located. This sample is still simple enough that there's no real need for that here, but it does anyway for the sake of demonstrating good practices.

FollowMousePosition controls the movement Parameters to make the character follow the mouse cursor around the scene:

using Animancer;
using Animancer.Units;
using UnityEngine;

public class FollowMousePosition : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private StringAsset _ParameterX;
    [SerializeField] private StringAsset _ParameterY;
    [SerializeField, PerSecond] private float _ParameterSpeed = 5;
    [SerializeField, Meters] private float _StopProximity = 0.1f;

    private Parameter<float> _ParameterValueX;
    private Parameter<float> _ParameterValueY;

    protected virtual void Awake()
    {
        _ParameterValueX = _Animancer.Parameters.GetOrCreate<float>(_ParameterX);
        _ParameterValueY = _Animancer.Parameters.GetOrCreate<float>(_ParameterY);
    }

    protected virtual void Update()
    {
        Vector3 movementDirection = GetMovementDirection();

        Vector3 localDirection = transform.InverseTransformDirection(movementDirection);

        float parameterDelta = Time.deltaTime * _ParameterSpeed;
        _ParameterValueX.Value = Mathf.MoveTowards(_ParameterValueX.Value, localDirection.x, parameterDelta);
        _ParameterValueY.Value = Mathf.MoveTowards(_ParameterValueY.Value, localDirection.z, parameterDelta);
    }

    private Vector3 GetMovementDirection()
    {
        Ray ray = Camera.main.ScreenPointToRay(SampleInput.MousePosition);

        if (!Physics.Raycast(ray, out RaycastHit raycastHit))
            Vector3.zero;

        Vector3 direction = raycastHit.point - transform.position;
        
        float squaredDistance = direction.sqrMagnitude;
        if (squaredDistance <= _StopProximity * _StopProximity)
        {
            return Vector3.zero;
        }
        else
        {
            return direction / Mathf.Sqrt(squaredDistance);
        }
    }
}

Idle Blending

This sample only has a single state containing the 2D Directional Mixer, but in a real game it's often a good idea to have a separate state for the Idle animation so that characters standing still cost as little performance as possible instead of constantly running their full locomotion mixer. However, even if a separate Idle state is used, it's still a good idea to keep it in the Mixer as well so that it can give reasonable results at low parameter values. Otherwise trying to stand still would look like this:

Note that the 0.3 shown for the Weight of each of the animations is actually 0.25 rounded to one decimal place so they add up to a total of 1.

Serialized Fields

Code Inspector
class FollowMousePosition : MonoBehaviour
{
    [SerializeField]
    private AnimancerComponent _Animancer;

    [SerializeField]
    private StringAsset _ParameterX;

    [SerializeField]
    private StringAsset _ParameterY;

    [SerializeField, PerSecond]
    private float _ParameterSpeed = 5;

    [SerializeField, Meters]
    private float _StopProximity = 0.1f;

The fields are fairly straightforward: just an AnimancerComponent, some StringAssets for the parameter names and floats to control various details (with appropriate Units Attributes to indicate their units of measurement in the Inspector).

Parameter Initialization

Just like the Float Parameter Slider in the Linear Mixers sample, on startup we need to GetOrCreate the parameters we want to control.

private Parameter<float> _ParameterValueX;
private Parameter<float> _ParameterValueY;

protected virtual void Awake()
{
    _ParameterValueX = _Animancer.Parameters.GetOrCreate<float>(_ParameterX);
    _ParameterValueY = _Animancer.Parameters.GetOrCreate<float>(_ParameterY);
}

Movement Direction

Every Update the first thing we need to do is calculate which direction the character wants to move.

protected virtual void Update()
{
    Vector3 movementDirection = GetMovementDirection();

    ...
}

private Vector3 GetMovementDirection()
{

We could use SampleInput.WASD, but that would be limited to only 8 directions unless we use a controller instead of a keyboard. So instead, this sample will move the character towards the mouse position to properly demonstrate blending in any direction using some Vector Math and Raycasting.

To calculate the world position the mouse is pointing at, we need a Ray from the Camera based on the screen position of the mouse:

    Ray ray = Camera.main.ScreenPointToRay(SampleInput.MousePosition);

Then we can use a Physics.Raycast to find out where that Ray intersects a Collider in the world:

    if (!Physics.Raycast(ray, out RaycastHit raycastHit))
        Vector3.zero;
  • Physics.Raycast returns true if the ray hits something, so the ! means that if it misses then we simply return Vector3.zero as the movement direction.
  • The out parameter works like a returned value to give us the RaycastHit which contains the details of what was hit.

In this case, the detail we need is the raycastHit.point which tells us where the Ray hit something. So the direction we want to move is the vector from our current transform.position to the hit point:

    Vector3 direction = raycastHit.point - transform.position;

Since the character will be moving a certain distance every frame, it's unlikely that they will ever actually reach the exact target position. In a real game, things like physics collisions would make it even less likely. So to avoid causing the character to jitter back and forth over the target position, we simply compare the Squared Magnitude of the direction with a small _StopProximity value to let the charaqcter stop moving if they're already close to the destination.

    float squaredDistance = direction.sqrMagnitude;
    if (squaredDistance <= _StopProximity * _StopProximity)
    {
        return Vector3.zero;
    }

Otherwise, we need to Normalize the direction so that we don't change speed based on distance.

    else
    {
        return direction / Mathf.Sqrt(squaredDistance);
    }
}

Calling direction.Normalize() would do the same thing, but would waste a tiny bit of performance calculating the magnitude again.

Parameter Calculation

Back in the Update method, we now have the direction we want to move:

protected virtual void Update()
{
    Vector3 movementDirection = GetMovementDirection();

That direction is in world space but we need it in the character's local space to calculate the blending parameters to that "right" means "the character's right" instead of "world space right". That can be easily done using Transform.InverseTransformDirection:

    Vector3 localDirection = transform.InverseTransformDirection(movementDirection);

Then we simply move the parameters towards that direction:

  • Parameter X towards Direction X (right/left).
  • Parameter Y towards Direction Z (forwards/backwards).
  • We don't care about Direction Y because the 2D Mixer doesn't include vertical movement.
    float parameterDelta = Time.deltaTime * _ParameterSpeed;
    _ParameterValueX.Value = Mathf.MoveTowards(
        _ParameterValueX.Value,
        localDirection.x,
        parameterDelta);
    _ParameterValueY.Value = Mathf.MoveTowards(
        _ParameterValueY.Value,
        localDirection.z,
        parameterDelta);
}

We could have simply set the parameter values directly, but parameters don't have any inherent smoothing so that could lead to situations where the motion doesn't look smooth because the parameters were changed too sharply. The Mixer Smoothing page explains some other ways of smoothing parameters.

What Next?

Sample Topic
Basic Layers Play multiple animations at the same time on different body parts.