Difficulty: Intermediate - Recommended after Hybrid Basics and Brains
Location: Assets/Plugins/Animancer/Examples/09 Animator Controllers/02 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 HybridAnimancerComponent
s 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:
Since we are using a Humanoid
Rig and we want to blend between the Animator Controller and Animancer, we can't use a Native Animator Controller and must instead use a HybridAnimancerComponent
. 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 |
---|---|
![]() |
![]() |
The Controller field here is blank. |
The Animator Controller is assigned to this Controller instead. |
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
, andCreatureState
scripts come from the Brains example. The
LocomotionState
script is mostly identical to the same script from the Brains example except for theUpdateAnimation
method which controls the Locomotion Blend Tree in the Animator Controller instead of directly referencingAnimationClip
s. This Blend Tree also includes the Idle animation where the other example had a separateIdleState
script, so we are using it directly as theCreature.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 => (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;
const string HolderName = "RightHandHolder";
var rightHand = Creature.Animancer.Animator.GetBoneTransform(HumanBodyBones.RightHand);
rightHand = rightHand.Find(HolderName);
Debug.Assert(rightHand != null, "Unable to find " + HolderName);
_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 |
---|---|
|
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 anenum
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'sCreature
component if it has one, so if it does not then we canreturn
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 returnfalse
so we can alsoreturn
from this method instead of continuing to start the mini game. - The only state the
Creature
has in this example is itsLocomotionState
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 useForceEnterState
or continue trying every frame for as long as that object remains in the trigger.
- If it is unable to enter the Idle state,
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 RightHandHolder object (which is a child of the right hand positioned correctly for holding objects):
const string HolderName = "RightHandHolder";
var rightHand = Creature.Animancer.Animator.GetBoneTransform(HumanBodyBones.RightHand);
rightHand = rightHand.Find(HolderName);
Debug.Assert(rightHand != null, "Unable to find " + HolderName);
_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 FixedUpdate
s and so OnTriggerEnter
can let the mini game start again.