01 Hybrid Mini Game

Difficulty: Intermediate

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

Namespace: Animancer.Examples.AnimatorControllers

This example demonstrates how you can use a HybridAnimancerComponent to play a default Animator Controller for some things and individual separate AnimationClips for others. Specifically, it uses an Animator Controller for the character's main actions such as locomotion for "regular" gameplay and then when they enter the Golf Mini Game it uses direct references to other AnimationClips which have nothing to do with regular gameplay. This means that you do not see those other animations while working on the character's main movement mechanics so you do not need to worry about accidentally breaking an unrelated part of the game.

Controller States and HybridAnimancerComponents are Pro-Only Features. You can try them out in the Unity Editor with Animancer Lite, but they are not available in runtime builds unless you purchase Animancer Pro.

Animator Controller

The Animator Controller only contains a Locomotion Blend Tree for movement since that is all this example needs, but if you were using a hybrid approach in a real game you would put all your character's regular gameplay animations in it:

To play it, we use a HybridAnimancerComponent instead of a regular AnimancerComponent. It inherits the Animations array from NamedAnimancerComponent and also adds a Controller field where we can assign the Animator Controller which it will automatically play on startup (unless you assign a different AnimationClip to the Play Automatically field).

Animator Hybrid Animancer Component

Note that the Controller field in the Animator component is blank. You must assign it in the HybridAnimancerComponent to use it with Animancer.

Scripts

There are only a few scripts in this example because it reuses scripts from several other examples:

  • The GolfHitController script comes from the Golf Events example. The mini game demonstrated here is essentially just that example.
  • The Creature, CreatureBrain, and CreatureState scripts come from the Brains example.
  • The LocomotionState script is mostly identical to the same script from the Brains example except for the UpdateAnimation method which controls the Locomotion Blend Tree in the Animator Controller instead of directly referencing AnimationClips. This Blend Tree also includes the Idle animation where the other example had a separate IdleState script, so we are using it directly as the Creature.Idle state.

    using Animancer;
    using Animancer.Examples.StateMachines.Brains;
    using UnityEngine;
    
    public sealed class LocomotionState : CreatureState
    {
        [SerializeField] private float _Acceleration = 3;
    
        private float _MoveBlend;
    
        private void OnEnable()
        {
            Animancer.TransitionToController();
            _MoveBlend = 0;
        }
    
        // Identical to StateMachines.Brains.LocomotionState.
        private void Update()
        {
            UpdateAnimation();
            UpdateTurning();
        }
    
        private void UpdateAnimation()
        {
            float targetBlend;
            if (Creature.Brain.MovementDirection == Vector3.zero)
                targetBlend = 0;
            else if (Creature.Brain.IsRunning)
                targetBlend = 1;
            else
                targetBlend = 0.5f;
    
            _MoveBlend = Mathf.MoveTowards(_MoveBlend, targetBlend, _Acceleration * Time.deltaTime);
            Animancer.SetFloat("MoveBlend", _MoveBlend);
        }
    
        // Identical to StateMachines.Brains.LocomotionState.
        private void UpdateTurning()
        {
            var movement = Creature.Brain.MovementDirection;
            if (movement == Vector3.zero)
                return;
    
            var targetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg;
            var turnDelta = Creature.Stats.TurnSpeed * Time.deltaTime;
    
            var transform = Creature.Animancer.transform;
            var eulerAngles = transform.eulerAngles;
            eulerAngles.y = Mathf.MoveTowardsAngle(eulerAngles.y, targetAngle, turnDelta);
            transform.eulerAngles = eulerAngles;
        }
    
        // Identical to StateMachines.Brains.LocomotionState.
        private void FixedUpdate()
        {
            var direction = Creature.Brain.MovementDirection;
            direction.y = 0;
            direction = Vector3.ClampMagnitude(direction, 1);
    
            var speed = Creature.Stats.GetMoveSpeed(Creature.Brain.IsRunning);
    
            Creature.Rigidbody.velocity = direction * speed;
        }
    
        // Normally the Creature class would have a reference to the specific type of AnimancerComponent we want,
        // but for the sake of reusing code from the earlier example, we just use a type cast here.
        private new HybridAnimancerComponent Animancer
        {
            get { return (HybridAnimancerComponent)Creature.Animancer; }
        }
    }
    

Golf Mini Game

The main logic of this example is in the GolfMiniGame script which is a CreatureBrain so it can take control of the Creature and prevent them from performing other actions while allowing the GolfHitController script to control their animations without even knowing anything about the Creature or its state machine:

using Animancer;
using Animancer.Examples.StateMachines.Brains;
using Animancer.FSM;
using UnityEngine;

public sealed class GolfMiniGame : CreatureBrain
{
    [SerializeField] private Events.GolfHitController _GolfHitController;
    [SerializeField] private Transform _GolfClub;
    [SerializeField] private Transform _ExitPoint;
    [SerializeField] private GameObject _RegularControls;
    [SerializeField] private GameObject _GolfControls;

    private Vector3 _GolfClubStartPosition;
    private Quaternion _GolfClubStartRotation;
    private CreatureBrain _PreviousBrain;

    private enum State { Entering, Turning, Playing, Exiting, }
    private State _State;

    private void Awake()
    {
        _GolfClubStartPosition = _GolfClub.localPosition;
        _GolfClubStartRotation = _GolfClub.localRotation;
    }

    private void OnTriggerEnter(Collider collider)
    {
        if (enabled)
            return;

        var creature = collider.GetComponent<Creature>();
        if (creature == null ||
            !creature.Idle.TryEnterState())
            return;

        _State = State.Entering;
        _PreviousBrain = creature.Brain;
        Creature = creature;
    }

    private void FixedUpdate()
    {
        switch (_State)
        {
            case State.Entering:
                if (MoveTowards(_GolfHitController.transform.position))
                    StartTurning();
                break;

            case State.Turning:
                if (Quaternion.Angle(Creature.Animancer.transform.rotation, _GolfHitController.transform.rotation) < 1)
                    StartPlaying();
                break;

            case State.Playing:
                break;

            case State.Exiting:
                if (MoveTowards(_ExitPoint.position))
                    Creature.Brain = _PreviousBrain;
                break;
        }
    }

    private bool MoveTowards(Vector3 destination)
    {
        var step = Creature.Stats.GetMoveSpeed(false) * Time.deltaTime;
        var direction = destination - Creature.Rigidbody.position;
        var distance = direction.magnitude;
        MovementDirection = direction / distance;// Normalize.
        return distance <= step;
    }

    private void StartTurning()
    {
        _State = State.Turning;
        MovementDirection = _GolfHitController.transform.forward;

        Creature.Rigidbody.velocity = Vector3.zero;
        Creature.Rigidbody.isKinematic = true;
        Creature.Rigidbody.position = _GolfHitController.transform.position;
    }

    private void StartPlaying()
    {
        _State = State.Playing;

        var rightHand = Creature.Animancer.Animator.GetBoneTransform(HumanBodyBones.RightHand);
        rightHand = rightHand.Find("Holder.R");
        _GolfClub.parent = rightHand;
        _GolfClub.localPosition = Vector3.zero;
        _GolfClub.localRotation = Quaternion.identity;

        _GolfHitController.gameObject.SetActive(true);

        _RegularControls.SetActive(false);
        _GolfControls.SetActive(true);
    }

    public void Quit()
    {
        _State = State.Exiting;

        _GolfHitController.gameObject.SetActive(false);
        _RegularControls.SetActive(true);
        _GolfControls.SetActive(false);

        _GolfClub.parent = transform;
        _GolfClub.localPosition = _GolfClubStartPosition;
        _GolfClub.localRotation = _GolfClubStartRotation;

        Creature.Rigidbody.isKinematic = false;

        Creature.Idle.TryReEnterState();
    }
}

Note that despite being a CreatureBrain, this component is not attached to the same object as the Creature or any of its children. It could be in an entirely different scene or prefab because the Creature does not need to know about it until it actually gets used (unlike in an Animator Controller where everything needs to be configured upfront in the same asset).

Fields

Code Inspector
[SerializeField]
private Events.GolfHitController _GolfHitController;

[SerializeField]
private Transform _GolfClub;

[SerializeField]
private Transform _ExitPoint;

[SerializeField]
private GameObject _RegularControls;

[SerializeField]
private GameObject _GolfControls;

The Creature field is inherited from CreatureBrain, but we are not actually assigning it in the Inspector. Instead, we get it when the Creature actually enters the mini game since a real game might not have them both in the same scene at the start.

It also has some non-Serialized Fields:

  • The starting position and rotation of the GolfClub which we store on startup:
private Vector3 _GolfClubStartPosition;
private Quaternion _GolfClubStartRotation;

private void Awake()
{
    _GolfClubStartPosition = _GolfClub.localPosition;
    _GolfClubStartRotation = _GolfClub.localRotation;
}
  • When this script takes control of a `Creature, it first stores the brain they had before so it can return it after the mini game:
private CreatureBrain _PreviousBrain;
  • When the Creature enters the mini game area, we want them to walk over to a specific point and turn to face a specific direction, then walk away when exiting. So we use an enum to manage those states:
private enum State { Entering, Turning, Playing, Exiting, }
private State _State;

On Trigger Enter

We are using an OnTriggerEnter method to start the mini game. By attaching our script to the same object as a Collider (in this case a BoxCollider) with its Is Trigger field enabled, Unity will call our OnTriggerEnter method whenever another object with a Collider component enters the trigger:

private void OnTriggerEnter(Collider collider)
{

Even though this script starts disabled (because it does not need to update when the mini game is not being played), Unity will still give it trigger messages.

If the mini game is already in use, we return immediately to ignore anything else that enters the trigger:

    if (enabled)
        return;

Then we need to determine if the object is actually a Creature who can play the mini game:

  • GetComponent<Creature>() will give us the object's Creature component if it has one, so if it does not then we can return without starting the mini game.
  • If it does have a Creature, then we tell it to enter its Idle state.
    • If it is unable to enter the Idle state, TryEnterState will return false so we can also return from this method instead of continuing to start the mini game.
    • The only state the Creature has in this example is its LocomotionState which covers Idle, Walk, and Run so nothing will prevent it from being in that state. But in a real game, other actions may not want to be interrupted (see the Interrupt Management example) so you could either use ForceEnterState or continue trying every frame for as long as that object remains in the trigger.
    var creature = collider.GetComponent<Creature>();
    if (creature == null ||
        !creature.Idle.TryEnterState())
        return;

Once we know object is a Creature and is in the right state, we can set our state, store the Creature.Brain (so we can return it after the mini game), and set the Creature of this mini game:

    _State = State.Entering;
    _PreviousBrain = creature.Brain;
    Creature = creature;
}

Entering

Since this script inherits from the CreatureBrain script in the More Brains example, setting the Creature will also inform the Creature that this is now its Brain so they are both linked to each other and it will enable this script so that Unity will start calling its FixedUpdate method every physics update:

private void FixedUpdate()
{

Using an enum to keep track of the current _State of the mini game means we can use a switch statement to do different things depending on its current value:

    switch (_State)
    {
        case State.Entering:
            if (MoveTowards(_GolfHitController.transform.position))
                StartTurning();
            break;

        ...

So when the Creature is just Entering the mini game, we want them to MoveTowards the position we want them to stand every frame. Then when they reach that point, MoveTowards will return true so we call StartTurning to do several things including changing the _State for the next FixedUpdate.

private bool MoveTowards(Vector3 destination)
{

As the character moves, it is very unlikely that they will ever reach the exact target position so we first calculate how far they will step in this single update:

    var step = Creature.Stats.GetMoveSpeed(false) * Time.deltaTime;

Use the Difference between the Creature's position and the destination to get a vector pointing between them then get its magnitude which represents the distance between those two points:

    var direction = destination - Creature.Rigidbody.position;
    var distance = direction.magnitude;

We do not want the Creature's speed to vary based on the remaining distance so we Normalize it by dividing by the distance;

    MovementDirection = direction / distance;

And then we return true if the Creature is within one step of the destination (otherwise false):

    return distance <= step;
}

Turning

When the Creature reaches the destination, we want them to face a specific direction but stop moving so we simply set that as the MovementDirection and make their Rigidbody Kinematic so it no longer gets affected by physics:

private void StartTurning()
{
    _State = State.Turning;
    MovementDirection = _GolfHitController.transform.forward;

    Creature.Rigidbody.velocity = Vector3.zero;
    Creature.Rigidbody.isKinematic = true;
    Creature.Rigidbody.position = _GolfHitController.transform.position;
}

Since we know the Creature is close enough to the destination that they could have reached it in a single update, we can simply teleport them there.

And now that we are in the Turning state, the FixedUpdate method can check the Creature's rotation every frame to wait until they are facing the desired direction then call StartPlaying:

private void FixedUpdate()
{
    switch (_State)
    {
        ...

        case State.Turning:
            if (Quaternion.Angle(Creature.Animancer.transform.rotation, _GolfHitController.transform.rotation) < 1)
                StartPlaying();
            break;

        ...

Playing

private void StartPlaying()
{
    _State = State.Playing;

Now that the Creature is in position and facing the right direction, we can put the GolfClub in their hand as a child of the Holder.R object (which is a child of the right hand positioned correctly for holding objects):

    var rightHand = Creature.Animancer.Animator.GetBoneTransform(HumanBodyBones.RightHand);
    rightHand = rightHand.Find("Holder.R");
    _GolfClub.parent = rightHand;
    _GolfClub.localPosition = Vector3.zero;
    _GolfClub.localRotation = Quaternion.identity;

Then activate the GolfHitController and swap the displayed controls:

    _GolfHitController.gameObject.SetActive(true);
    _RegularControls.SetActive(false);
    _GolfControls.SetActive(true);
}

While we are in the Playing state, the FixedUpdate method can simply do nothing and allow the GolfHitController to do whatever it wants:

private void FixedUpdate()
{
    switch (_State)
    {
        ...

        case State.Playing:
            break;

        ...

Also note that we have already given the GolfHitController a reference to the Creature using the Inspector so that we do not need to modify the Golf Events example to allow it to be set in code. In a real game you would likely want to pass the Creature who triggered the mini game onto that controller.

Exiting

To get out of the mini game, we have a Quit method which is called by a UI Button which basically just undoes everything StartTurning and StartPlaying did:

public void Quit()
{
    _State = State.Exiting;

    _GolfHitController.gameObject.SetActive(false);
    _RegularControls.SetActive(true);
    _GolfControls.SetActive(false);

    _GolfClub.parent = transform;
    _GolfClub.localPosition = _GolfClubStartPosition;
    _GolfClub.localRotation = _GolfClubStartRotation;

    Creature.Rigidbody.isKinematic = false;

The Creature is still in its Idle state (the LocomotionState component), but since the GolfHitController played other animations we need to have it re-enter that state to play the Idle/Walk/Run Blend Tree again (where TryEnterState would do nothing if that state is already active):

    Creature.Idle.TryReEnterState();
}

And while we are in the Exiting state, we move the Creature towards the exit point:

private void FixedUpdate()
{
    switch (_State)
    {
        ...

        case State.Exiting:
            if (MoveTowards(_ExitPoint.position))
                Creature.Brain = _PreviousBrain;
            break;
    }
}

Once it reaches the exit point, we give it back the brain it had before the GolfMiniGame took over. This will re-establish the Creature.Brain and Brain.Creature links and set the GolfMiniGame.Creature to null, which will disable it so it stops receiving FixedUpdates and so OnTriggerEnter can let the mini game start again.