04 Directional Blending

Difficulty: Intermediate

Location: Assets/Plugins/Animancer/Examples/03 Locomotion/04 Directional Blending

Namespace: Animancer.Examples.Locomotion

This example expands upon the Spider Bot example with a 2D Mixer to allow movement in any direction and a Rigidbody so that the bot can follow the mouse cursor around.

Pro-Only Features are used in this example: Mixers. 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.

This example is implemented by the SpiderBotAdvanced script which looks like this (with the comments removed since we are about to explain how it works):

using Animancer;
using UnityEngine;

public sealed class SpiderBotAdvanced : SpiderBot
{
    [SerializeField] private Rigidbody _Body;
    [SerializeField] private float _TurnSpeed = 90;
    [SerializeField] private float _MovementSpeed = 1.5f;
    [SerializeField] private float _SprintMultiplier = 2;

    [SerializeField]
    private MixerState.Transition2D _Move;

    protected override ITransition MovementAnimation
    {
        get { return _Move; }
    }

    private Vector3 _MovementDirection;

    protected override bool IsMoving
    {
        get { return _MovementDirection != Vector3.zero; }
    }

    protected override void Awake()
    {
        base.Awake();
        Animancer.States.GetOrCreate(_Move);
    }

    protected override void Update()
    {
        _MovementDirection = GetMovementDirection();
        base.Update();

        if (_Move.State.IsActive)
        {
            var eulerAngles = transform.eulerAngles;
            var targetEulerY = Camera.main.transform.eulerAngles.y;
            eulerAngles.y = Mathf.MoveTowardsAngle(eulerAngles.y, targetEulerY, _TurnSpeed * Time.deltaTime);
            transform.eulerAngles = eulerAngles;

            _Move.State.Parameter = new Vector2(
                Vector3.Dot(transform.right, _MovementDirection),
                Vector3.Dot(transform.forward, _MovementDirection));

            var isSprinting = Input.GetMouseButton(0);
            _Move.State.Speed = isSprinting ? _SprintMultiplier : 1;
        }
        else
        {
            _Move.State.Parameter = Vector2.zero;
            _Move.State.Speed = 0;
        }
    }

    private Vector3 GetMovementDirection()
    {
        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

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

        var direction = raycastHit.point - transform.position;
        direction.y = 0;

        var distance = direction.magnitude;
        if (distance < _MovementSpeed * _SprintMultiplier * Time.fixedDeltaTime)
        {
            return Vector3.zero;
        }
        else
        {
            return direction / distance;
        }
    }

    private void FixedUpdate()
    {
        _Body.velocity = _MovementDirection * _Move.State.Speed * _MovementSpeed;
    }
}

Fields

Code Inspector
class SpiderBotAdvanced : SpiderBot
{
    [SerializeField]
    private Rigidbody _Body;

    [SerializeField]
    private float _TurnSpeed = 90;

    [SerializeField]
    private float _MovementSpeed = 1.5f;

    [SerializeField]
    private float _SprintMultiplier = 2;

    [SerializeField]
    private MixerState.Transition2D _Move;
}

Inheriting from SpiderBot gives us the same base fields as in the Spider Bot example and allows us to add more. Notably, instead of a ClipState.Transition like SpiderBotSimple used to play a single AnimationClip, this time we are using a MixerState.Transition2D to allow blending between multiple animations on two axes (forward/back and left/right). See Mixers for more information about their different types.

Abstractions

This time we override the MovementAnimation to return the MixerState.Transition2D and we have a private Vector3 _MovementDirection which will be set in the Update method so that IsMoving can determine if it currently wants to move:

[SerializeField]
private MixerState.Transition2D _Move;

protected override ITransition MovementAnimation
{
    get { return _Move; }
}

private Vector3 _MovementDirection;

protected override bool IsMoving
{
    get { return _MovementDirection != Vector3.zero; }
}

Initialisation

The Update and FixedUpdate methods are going to be accessing the _Move.State, so we need to ensure that the state is created on startup even though it is not used yet:

protected override void Awake()
{
    base.Awake();
    Animancer.States.GetOrCreate(_Move);
}

Note that the base SpiderBot.Awake method is virtual and has its own implementation which we still want to use, so we call base.Awake() in our override.

Input

Rather than using keyboard input to control the bot (which would be limited to 8 directions), we are having it follow the mouse cursor to demonstrate movement in any direction. Using some Vector Math and Raycasting.

  1. Get a ray from the main camera in the direction of the mouse cursor:
private Vector3 GetMovementDirection()
{
    var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
  1. Do a raycast with it. The Raycast method will return true if it hits something, so if it does not then we want to stop trying to move. Note that the Physics Layer of the bot in the scene is set to Ignore Raycast so that the raycast will not hit it:
    RaycastHit raycastHit;
    if (!Physics.Raycast(ray, out raycastHit))// Note the exclamation mark !
        return Vector3.zero;
  1. If the ray did hit something, the out raycastHit parameter will contain the details of the hit so we can calculate the horizontal direction from the bot to that point using simple Vector Subtraction then zeroing the y (vertical) value:
    var direction = raycastHit.point - transform.position;
    direction.y = 0;
  1. When it reaches the destination, we want to stop moving. It is very unlikely that it will exactly reach the target point so we need to stop when it gets close and rather than using an arbitrary small threshold like 0.1, we can calculate the distance it would move in a single frame at top speed to determine if it would arrive or pass the destination next frame. Note that Time.deltaTime is the same as Time.fixedDeltaTime during a FixedUpdate, but since this method is called by Update we want to make sure that it compares against the distance it would move during the next physics update (because it is being moved by a Rigidbody) rather than during this rendered frame:
    var distance = direction.magnitude;
    if (distance < _MovementSpeed * _SprintMultiplier * Time.fixedDeltaTime)
    {
        return Vector3.zero;
    }
  1. Otherwise if it is not close to the destination, Normalize the direction so that we do not change speed based on distance. Calling direction.Normalize() would do the same thing, but that would need to calculate the magnitude again so this is simply more efficient:
    else
    {
        return direction / distance;
    }
}

Update

Like with Awake, we want to override the base SpiderBot.Update method to add some additional code.

  1. Determine the direction we want to move using the method we just made and store it in the _MovementDirection field so that the IsMoving property can also check it:
protected override void Update()
{
    _MovementDirection = GetMovementDirection();
  1. Call the base SpiderBot.Update method to wake up or go to sleep if necessary:
    base.Update();
  1. If the movement state is playing and not fading out, then we are allowed to move. This prevents movement while asleep and during the wake up animation:
    if (_Move.State.IsActive)
    {
  1. Rotate towards the same Y angle as the camera. transform.eulerAngles represents the rotation of an object around each axis, so the y value is the rotation around the vertical axis. You can Right Click and drag to rotate the camera using the Orbit Controls script:
        var eulerAngles = transform.eulerAngles;
        var targetEulerY = Camera.main.transform.eulerAngles.y;
        eulerAngles.y = Mathf.MoveTowardsAngle(eulerAngles.y, targetEulerY, _TurnSpeed * Time.deltaTime);
        transform.eulerAngles = eulerAngles;
  1. The movement direction is in world space, so we need to convert it to local space to be appropriate for the current rotation. There are various different ways this calculation could be performed, but a simple one is to use Dot Products to determine how much of the _MovementDirection lies along each of the bot's local axes (transform.right and transform.forward). Then we can just apply that value to the Mixer's Parameter:
        _Move.State.Parameter = new Vector2(
            Vector3.Dot(transform.right, _MovementDirection),
            Vector3.Dot(transform.forward, _MovementDirection));
  1. Set its Speed to sprint if you are holding Mouse Button 0 (Left Click):
        var isSprinting = Input.GetMouseButton(0);
        _Move.State.Speed = isSprinting ? _SprintMultiplier : 1;

        // The ? operator is a shorthand "if" statement which could otherwise look like this:
        // if (isSprinting)
        //     _Move.State.Speed = _SprintMultiplier;
        // else
        //     _Move.State.Speed = 1;
        // This is called the Conditional Operator.
    }
  1. Otherwise if the movement state is not playing or is fading out, stop moving:
    else
    {
        _Move.State.Parameter = Vector2.zero;
        _Move.State.Speed = 0;
    }
}

Fixed Update

The actual movement implementation in FixedUpdate is simple for the sake of keeping the script short, but manipulating the Rigidbody.velocity directly is not generally recommended. A proper implementation would usually use AddForce:

private void FixedUpdate()
{
    _Body.velocity = _MovementDirection * _Move.State.Speed * _MovementSpeed;
}

The Rigidbody.velocity is the direction the object is moving with the Magnitude of the vector determining its speed (distance moved per second).

This all combines to give us a bot that can chase the mouse cursor around the scene as long as it is pointing at an object with a collider for the raycast to hit (the ground plane) and will rotate itself to match the camera (Right Click and drag to rotate the camera using the Orbit Controls script):