04 Brains

Difficulty: Intermediate - Recommended after Walk and Run and Characters

Location: Assets/Plugins/Animancer/Examples/06 State Machines/04 Brains

Namespace: Animancer.Examples.StateMachines.Brains

This example expands upon the Characters example to demonstrate how you can control a character with keyboard input, then the Brain Transplants example expands it further to demonstrate how each character can be controlled differently even though they share the same scripts for actually performing actions.

There are quite a few scripts in this example so it is not really worth trying to summarise them without a full explanation. They are all in the Animancer.Examples.StateMachines.Brainsnamespace to differentiate them from the other State Machine examples. The term "Brain" was chosen because it accurately describes the purpose of this system, which is to control the behaviour of a Character. There are many other common terms though, such as "Character Controller" or "Character Input", so feel free to use terminology that suits you.

Characters

This Character is very similar to the previous Characters.Character, but it also has a Rigidbody because this example will involve movement and a Brain to control its actions:

using Animancer;
using Animancer.FSM;
using System;
using UnityEngine;

public sealed class Character : MonoBehaviour
{
    [SerializeField]
    private AnimancerComponent _Animancer;
    public AnimancerComponent Animancer => _Animancer;

    [SerializeField]
    private CharacterState _Idle;
    public CharacterState Idle => _Idle;

    [SerializeField]
    private Rigidbody _Rigidbody;
    public Rigidbody Rigidbody => _Rigidbody;

    [SerializeField]
    private CharacterBrain _Brain;
    public CharacterBrain Brain => _Brain;

    public StateMachine<CharacterState> StateMachine { get; private set; }

    public Action ForceEnterIdleState { get; private set; }

    private void Awake()
    {
        ForceEnterIdleState = () => StateMachine.ForceSetState(_Idle);

        StateMachine = new StateMachine<CharacterState>(_Idle);
    }
}

And the CharacterBrain is a simple abstract class to define what a brain needs to specify, i.e. the direction it wants to move and whether or not it wants to run:

using UnityEngine;

public abstract class CharacterBrain : MonoBehaviour
{
    [SerializeField]
    private Character _Character;
    public Character Character => _Character;

    public Vector3 MovementDirection { get; protected set; }

    public bool IsRunning { get; protected set; }
}

Note that the actual scripts have setters for Character.Brain and CharacterBrain.Character which are used by the Brain Transplants example to change the assigned brain at runtime.

Base State

Now that we actually want to implement useful behaviour in the states, we need the base CharacterState to be a different from the previous Characters.CharacterState.

The class is now abstract. It does not make sense to attach a CharacterState component to an object because it will not do anything. You should only be able to attach components that Inherit from it like IdleState or LocomotionState.

using Animancer;
using Animancer.FSM;
using UnityEngine;

public abstract class CharacterState : StateBehaviour, IOwnedState<CharacterState>
{

We need a public Character Character property to allow other scripts to access the private serialized _Character. We could make it protected to only allow inheriting scripts to access it, but that would also allow those scripts to set it accidentally and there is no reason why other scripts should not be able to check which character owns a state:

    [SerializeField]
    private Character _Character;
    public Character Character
    {
        get => _Character;

We will not be changing the Character of any states at runtime in these examples, but sometimes you might have states that get created separately and need to have their Character assigned. Since we are using a property rather than a simple public field, we can ensure that any time the Character is changed the state is not left as the active state on the previous Character:

        set
        {
            if (_Character != null &&
                _Character.StateMachine.CurrentState == this)
                _Character.Idle.ForceEnterState();

            _Character = value;
        }
    }

To save a bit of effort when adding new states to an object, we use the Reset method to look for a reference to a character in any parent or child of the current GameObject. Unity calls Reset when you first add a component to an object in Edit Mode so this will usually save us from needing to assign the Character manually in the Inspector:

#if UNITY_EDITOR
    protected void Reset()
    {
        _Character = gameObject.GetComponentInParentOrChildren<Character>();
    }
#endif

Since every state has a reference to the character that owns it, we can implement the IOwnedState<CharacterState> interface and give it a property to get its OwnerStateMachine so that we can use the various extension methods in the StateExtensions class when working with these states. This means we will be able to call Character.Idle.TryEnterState() instead of Character.StateMachine.TryEnterState(Character.Idle). Both would do the same thing, but this is a bit shorter (see Owned States for more details).

    public StateMachine<CharacterState> OwnerStateMachine => _Character.StateMachine;
}

The base state class no longer has an AnimationClip field so it is up to each child class to get the animation or animations it needs. In particular, the Locomotion state has _Walk and _Run animations rather than a single "standard" _Animation.

The full CharacterState script looks like this:

using Animancer;
using Animancer.FSM;
using UnityEngine;

public abstract class CharacterState : StateBehaviour, IOwnedState<CharacterState>
{
    [SerializeField]
    private Character _Character;
    public Character Character
    {
        get => _Character;
        set
        {
            if (_Character != null &&
                _Character.StateMachine.CurrentState == this)
                _Character.Idle.ForceEnterState();

            _Character = value;
        }
    }

#if UNITY_EDITOR
    protected override void Reset()
    {
        base.Reset();
        _Character = gameObject.GetComponentInParentOrChildren<Character>();
    }
#endif

    public StateMachine<CharacterState> OwnerStateMachine => _Character.StateMachine;
}

Idle

With that base class in place, we can create IdleState to Inherit from it. This state only needs to do two things:

  1. Play an animation when the state is entered, just like the BasicCharacterState from the Characters example:
using Animancer;
using UnityEngine;

public sealed class IdleState : CharacterState
{
    [SerializeField] private AnimationClip _Animation;

    private void OnEnable()
    {
        Character.Animancer.Play(_Animation, 0.25f);
    }
  1. Constantly clear the Rigidbody.velocity to ensure that the character doesn't slide or get pushed around too easily. It is generally recommended that you use Rigidbody.AddForce instead of setting the velocity directly, but for this example we just want something simple while we demonstrate the animation and state machine systems:
    private void FixedUpdate()
    {
        Character.Rigidbody.velocity = default;
    }
}

Communication

Using separate scripts for decision making and actually acting on the decisions means those scripts need to communicate with each other somehow. There are two general approaches to achieve this:

  • Ask: other scripts check what the Brain wants to do.
  • Tell: the Brain issues commands to other scripts.

Both approaches have their merits and can be better for certain tasks, so you will often end up using them both in the same game.

For example, with the Ask approach you could have your base Brain class expose a DesiredState property which it can set as necessary, then have the Character attempt to enter that state every Update. This is often not ideal because it makes it a bit harder to verify that the desired state has actually been entered, so the Tell approach may be better. Simply give the Brain access to the character's StateMachine so it can call TrySetState whenever it wants to and check whether it returns true if it needs to make sure the state was entered.

However, for something like a movement direction vector the other approach may be better. Telling the character's current state which direction to move would mean that every state has its own movement vector even though only one will be used at a time and most states do not use it anyway. You could put the movement vector on the Character, but if for whatever reason the Brain got destroyed, that would leave the movement vector at its current value (unless you also clear it). In this case, it would likely make more sense to just put the movement vector in the Brain class and have states like Locomotion Ask the brain which way to move each frame.

Stats

To implement locomotion (the ability for the character to move around), we will need some Serialized Fields to specify the speeds we can move at: walking, running, and turning. There are generally two places that would be appropriate for those fields, each with different advantages:

In the "Locomotion" state itself In a central "Stats" class
  • Self-contained. Each state has the fields it needs.
  • If a character has no locomotion state, then they have no speed stat. Otherwise they could potentially have a speed value despite not actually being able to use it.
  • Anything that needs to know how fast the character can move needs a reference to the locomotion state. This could be done by simply giving the Character that reference.
  • Everything can access the speed and other stats of the character without getting access to the actual states. Anything might need to know how fast the character can move, but only the brain actually needs to be able to tell it to move.
  • Brains can directly reference the states they use. If a brain does not use locomotion then it will not show a field for that state when you add it to a character, making it clear that such a state will not be used.
  • All stats are in one place so they can be easily serialized if they need to be saved to a file or sent over a network.

Either approach could work fine, but for this example we are using a central class (with appropriate Units Attributes on its fields):

using Animancer.Units;
using System;
using UnityEngine;

[Serializable]
public sealed class CharacterStats
{
    [SerializeField, MetersPerSecond]
    private float _WalkSpeed = 2;
    public float WalkSpeed => _WalkSpeed;

    [SerializeField, MetersPerSecond]
    private float _RunSpeed = 4;
    public float RunSpeed => _RunSpeed;

    public float GetMoveSpeed(bool isRunning)
    {
        return isRunning ? _RunSpeed : _WalkSpeed;
    }

    [SerializeField, DegreesPerSecond]
    private float _TurnSpeed = 360;
    public float TurnSpeed => _TurnSpeed;
}

Then we just need to give the Character a new serialized field with an accessor property:

[SerializeField]
private CharacterStats _Stats;
public CharacterStats Stats => _Stats;

Locomotion

Now that we have those stats, we can implement a LocomotionState class which inherits from CharacterState just like the IdleState we made earlier.

First we need a Mixer for the Walk and Run animations:

using Animancer;
using UnityEngine;

public sealed class LocomotionState : CharacterState
{
    [SerializeField] private LinearMixerTransition _Animation;

When entering this state, we play that mixer and snap its parameter to 1 if running or 0 if walking:

    private void OnEnable()
    {
        Character.Animancer.Play(_Animation);
        _Animation.State.Parameter = Character.Brain.IsRunning ? 1 : 0;
    }

Once it's playing, we don't want to instantly snap its parameter between 0 and 1. Instead, we add a _FadeSpeed field and use Mathf.MoveTowards to move the parameter towards the target value every frame in Update:

    [SerializeField] private float _FadeSpeed = 2;

    private void Update()
    {
        var target = Character.Brain.IsRunning ? 1 : 0;
        _Animation.State.Parameter = Mathf.MoveTowards(_Animation.State.Parameter, target, _FadeSpeed * Time.deltaTime);
    }

Turning

So far the state can Walk and Run in one direction but can't turn. Some games use additional animations to make the turning look more natural (such as leaning over if you turn while running) or the Quick Turn animations used in the 3D Game Kit example, but for this example we are just going to rotate the model around the Y axis. We also want to do this in Update, so first we should split out the method we just wrote to keep the script organised:

private void Update()
{
    UpdateSpeed();
    UpdateTurning();
}

private void UpdateSpeed()
{
    // The Update method we just wrote above.
}

private void UpdateTurning()
{

We need to make sure we have a Character.Brain.MovementDirection, because we do not want to turn even if we somehow end up in this state without the brain trying to move:

    var movement = Character.Brain.MovementDirection;
    if (movement == default)
        return;

Then we need to determine the angle we want to turn towards:

    var targetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg;

Without going into the maths behind it, Mathf.Atan2 gives us the angle of a vector in radians. So we just feed in the x and z values because we want an angle around the y axis, then convert the result to degrees because Transform.eulerAngles uses degrees.

Then we calculate the amount we want to rotate this frame:

    var turnDelta = Character.Stats.TurnSpeed * Time.deltaTime;

Then we get the Transform.eulerAngles, move the y value towards the target angle, and apply those angles back to the Transform:

    var transform = Character.Animancer.transform;
    var eulerAngles = transform.eulerAngles;
    eulerAngles.y = Mathf.MoveTowardsAngle(eulerAngles.y, targetAngle, turnDelta);
    transform.eulerAngles = eulerAngles;
}

Movement

The last thing the locomotion state needs is actual locomotion, so we simply get the desired speed depending on whether the brain wants to run or not and set it as the character's velocity in whatever direction the brain wants to move. Since we do not want to allow flight we need to clear the y component of the direction just in case and we also need to make sure the magnitude is not greater than 1 so the brain cannot make the character go faster by simply setting a longer vector (going slower is fine though):

private void FixedUpdate()
{
    var direction = Character.Brain.MovementDirection;
    direction.y = 0;
    direction = Vector3.ClampMagnitude(direction, 1);

    var speed = Character.Stats.GetMoveSpeed(Character.Brain.IsRunning);

    Character.Rigidbody.velocity = direction * speed;
}

It is generally recommended that you use Rigidbody.AddForce instead of setting the velocity directly, but for this example we just want something simple while we demonstrate the animation and state machine systems.

The full LocomotionState script looks like this

using Animancer;
using UnityEngine;

public sealed class LocomotionState : CharacterState
{
    [SerializeField] private LinearMixerTransition _Animation;
    [SerializeField] private float _FadeSpeed = 0.5f;

    private void OnEnable()
    {
        Character.Animancer.Play(_Animation);
        _Animation.State.Parameter = Character.Brain.IsRunning ? 1 : 0;
    }

    private void Update()
    {
        UpdateSpeed();
        UpdateTurning();
    }

    private void UpdateSpeed()
    {
        var target = Character.Brain.IsRunning ? 1 : 0;
        _Animation.State.Parameter = Mathf.MoveTowards(_Animation.State.Parameter, target, _FadeSpeed * Time.deltaTime);
    }

    private void UpdateTurning()
    {
        if (Character.Brain.MovementDirection == default)
            return;

        var targetAngle = Mathf.Atan2(Character.Brain.Movement.x, Character.Brain.Movement.z) * Mathf.Rad2Deg;
        var turnDelta = Character.Stats.TurnSpeed * Time.deltaTime;

        var transform = Character.Animancer.transform;
        var eulerAngles = transform.eulerAngles;
        eulerAngles.y = Mathf.MoveTowardsAngle(eulerAngles.y, targetAngle, turnDelta);
        transform.eulerAngles = eulerAngles;
    }

    private void FixedUpdate()
    {
        var direction = Character.Brain.MovementDirection;
        direction.y = 0;
        direction = Vector3.ClampMagnitude(direction, 1);
        
        var speed = Character.Stats.GetMoveSpeed(Character.Brain.IsRunning);
        
        Character.Rigidbody.velocity = direction * speed;
    }
}

Keyboard Brain

We now have the components for a simple character that can stand still and walk or run around, we just need a script to actually control it so it's time to implement a brain.

The only standard state a Character has is Idle. Since we want this brain to determine how it moves we start with a reference to the locomotion state:

public sealed class KeyboardBrain : CharacterBrain
{
    [SerializeField] private CharacterState _Locomotion;

The rest of the script is the Update method which starts by checking for input to decide whether it wants to be in the Idle or Locomotion state:

private void Update()
{
    var input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
    if (input != default)
    {
        _Locomotion.TryEnterState();
    }
    else
    {
        Character.Idle.TryEnterState();
    }
}

When using the locomotion state, we also need to set the brain's Movement vector for the state to use. We cannot just use the input vector directly because we need it to be relative to whichever direction the camera is facing, so first we need to get the camera's forward and right vectors and flatten them onto the XZ plane:

var camera = Camera.main.transform;

var forward = camera.forward;
forward.y = 0;
forward.Normalize();

var right = camera.right;
right.y = 0;
right.Normalize();

Then we can simply build the Movement vector by multiplying the input by those axes:

Movement =
    right * input.x +
    forward * input.y;

We also need to determine if the player wants to run or not:

IsRunning = Input.GetButton("Fire3");//Left Shift by default.

And lastly, we should clear the Movement vector when we return to Idle using Movement = default;.

The full KeyboardBrain script looks like this

using Animancer.FSM;
using UnityEngine;

public sealed class KeyboardBrain : CharacterBrain
{
    [SerializeField] private CharacterState _Locomotion;

    private void Update()
    {
        var input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
        if (input != default)
        {
            var camera = Camera.main.transform;

            var forward = camera.forward;
            forward.y = 0;
            forward.Normalize();

            var right = camera.right;
            right.y = 0;
            right.Normalize();

            Movement =
                right * input.x +
                forward * input.y;

            IsRunning = Input.GetButton("Fire3");//Left Shift by default.

            _Locomotion.TryEnterState();
        }
        else
        {
            Movement = default;

            Character.Idle.TryEnterState();
        }
    }
}

Scene Setup

After all that scripting it's finally time to set up the scene to see things in action.

Instead of starting the scene from scratch, it would be easier to just duplicate the scene (Ctrl + D on Windows) and delete the Character we are about to rebuild.

  1. Right Click in the Hierarchy and create an empty game object.
  2. Rename it to "Character".
  3. Reset its position to (0, 0, 0) by Right Clicking on the Transform header and selecting the Reset Position function. Or if you have Inspector Gadgets you can Middle Click on any field to reset it (including the Position label to reset its X, Y, and Z fields) or use Ctrl + Shift + Z to reset the position, rotation, and scale all at once.
  4. Add a CapsuleCollider and set its Center to (0, 1, 0) and Height to 2.
  5. Add a Rigidbody and set its Constraints to freeze the rotation on all axes because we are rotating the model in our script so we do not want physics to affect it.

  1. Add a Character and IdleState and assign their references (we do not have them all yet, but there is only one of each type so it should be fairly obvious what goes where). Note how the idle state automatically picks up the character thanks to the Reset method we put in the base state class.

  1. Drag the DefaultHumanoid model (from Assets/Plugins/Animancer/Examples/Art/Default Humanoid) into the Hierarchy as a child of the Character and make sure its position is at (0, 0, 0).
  2. Add an AnimancerComponent to the model and disable Apply Root Motion.
  3. Create another empty game object as a child of the Character and rename it to "Brain".
  4. Add a KeyboardBrain and LocomotionState and assign their references.
  5. Go back to the root Character and assign the Brain and Animancer references.

Now we can enter Play Mode to see the character moving around in response to keyboard input and thanks to the OrbitControls script on the camera we can Right Click and drag to rotate the camera to see that the controls always move the character relative to the camera: