03 Brains

Difficulty: Intermediate

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

Namespace: Animancer.Examples.StateMachines.Brains

This example expands upon the Creatures example to demonstrate how you can control a creature with keyboard input, then the More Brains example expands it further to demonstrate how each creature 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 Creature. There are many other common terms though, such as "Creature Controller" or "Creature Input", so feel free to use terminology that suits you.

Creatures

This Creature is very similar to the previous Creatures.Creature, 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 Creature : MonoBehaviour
{
    [SerializeField]
    private AnimancerComponent _Animancer;
    public AnimancerComponent Animancer { get { return _Animancer; } }

    [SerializeField]
    private CreatureState _Idle;
    public CreatureState Idle { get { return _Idle; } }

    [SerializeField]
    private Rigidbody _Rigidbody;
    public Rigidbody Rigidbody { get { return _Rigidbody; } }

    [SerializeField]
    private CreatureBrain _Brain;
    public CreatureBrain Brain { get { return _Brain; } }

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

    public Action ForceEnterIdleState { get; private set; }

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

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

And the CreatureBrain 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 CreatureBrain : MonoBehaviour
{
    [SerializeField]
    private Creature _Creature;
    public Creature Creature { get { return _Creature; } }

    public Vector3 MovementDirection { get; protected set; }

    public bool IsRunning { get; protected set; }
}

Note that the actual scripts have setters for Creature.Brain and CreatureBrain.Creature which are used by the More Brains 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 CreatureState to be a different from the previous Creatures.CreatureState.

The class is now abstract. It does not make sense to attach a CreatureState 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 CreatureState : StateBehaviour<CreatureState>,
    IOwnedState<CreatureState>
{

We need a public Creature Creature property to allow other scripts to access the private serialized _Creature. 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 creature owns a state:

    [SerializeField]
    private Creature _Creature;
    public Creature Creature
    {
        get { return _Creature; }

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

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

            _Creature = 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 creature 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 Creature manually in the Inspector:

#if UNITY_EDITOR
    protected void Reset()
    {
        _Creature = Editor.AnimancerEditorUtilities.GetComponentInHierarchy<Creature>(gameObject);
    }
#endif

Since every state has a reference to the creature that owns it, we can implement the IOwnedState<CreatureState> 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 Creature.Idle.TryEnterState() instead of Creature.StateMachine.TryEnterState(Creature.Idle). Both would do the same thing, but this is a bit shorter (see Owned States for more details).

    public StateMachine<CreatureState> OwnerStateMachine { get { return _Creature.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 CreatureState script looks like this:

using Animancer;
using Animancer.FSM;
using UnityEngine;

public abstract class CreatureState : StateBehaviour<CreatureState>,
    IOwnedState<CreatureState>
{
    [SerializeField]
    private Creature _Creature;
    public Creature Creature
    {
        get { return _Creature; }
        set
        {
            if (_Creature != null &&
                _Creature.StateMachine.CurrentState == this)
                _Creature.Idle.ForceEnterState();

            _Creature = value;
        }
    }

#if UNITY_EDITOR
    protected override void Reset()
    {
        base.Reset();
        _Creature = AnimancerUtilities.GetComponentInHierarchy<Creature>(gameObject);
    }
#endif

    public StateMachine<CreatureState> OwnerStateMachine { get { return _Creature.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 BasicCreatureState from the Creatures example:
using Animancer;
using UnityEngine;

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

    private void OnEnable()
    {
        Creature.Animancer.Play(_Animation, 0.25f);
    }
  1. Constantly clear the Rigidbody.velocity to ensure that the creature 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()
    {
        Creature.Rigidbody.velocity = Vector3.zero;
    }
}

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 Creature 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 creature'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 creature'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 Creature, 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 creature 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 creature can move needs a reference to the locomotion state. This could be done by simply giving the Creature that reference.
  • Everything can access the speed and other stats of the creature without getting access to the actual states. Anything might need to know how fast the creature 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 creature, 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:

using System;
using UnityEngine;

[Serializable]
public sealed class CreatureStats
{
    [SerializeField]
    private float _WalkSpeed = 2;
    public float WalkSpeed { get { return _WalkSpeed; } }

    [SerializeField]
    private float _RunSpeed = 4;
    public float RunSpeed { get { return _RunSpeed; } }

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

    [SerializeField]
    private float _TurnSpeed = 360;
    public float TurnSpeed { get { return _TurnSpeed; } }
}

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

[SerializeField]
private CreatureStats _Stats;
public CreatureStats Stats { get { return _Stats; } }

Locomotion

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

First we need the Walk and Run animations:

using Animancer;
using UnityEngine;

public sealed class LocomotionState : CreatureState
{
    [SerializeField] private AnimationClip _Walk;
    [SerializeField] private AnimationClip _Run;
}

Then in Update we want to play those animations and keep them synchronised exactly like the IdleAndWalkAndRun script in the Walk and Run example, with two changes:

  • We now check the Creature.Brain.IsRunning instead of directly checking Input.GetButton("Fire3") so that this script does not actually care why it is running, it just does whatever the brain wants. The brain manages input and this script only manages locomotion.
  • We can also slow down the animation if the brain wants to walk even slower by using the Creature.Brain.MovementDirection.magnitude to set the AnimancerState.Speed of both animations.
private void Update()
{
    AnimationClip playAnimation, otherAnimation;

    if (Creature.Brain.IsRunning)
    {
        playAnimation = _Run;
        otherAnimation = _Walk;
    }
    else
    {
        playAnimation = _Walk;
        otherAnimation = _Run;
    }

    var playState = Animancer.Play(playAnimation, 0.25f);

    var speed = Mathf.Min(Creature.Brain.MovementDirection.magnitude, 1);
    playState.Speed = speed;

    var otherState = Animancer.GetState(otherAnimation);
    if (otherState != null && otherState.IsPlaying)
    {
        playState.NormalizedTime = otherState.NormalizedTime;
        otherState.Speed = speed;
    }
}

Turning

So far the state can Walk and Run in one direction but cannot 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()
{
    UpdateAnimation();
    UpdateTurning();
}

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

private void UpdateTurning()
{
    // TODO.
}

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

private void UpdateTurning()
{
    var movement = Creature.Brain.MovementDirection;
    if (movement == Vector3.zero)
        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 = Creature.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 = Creature.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 creature'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 creature go faster by simply setting a longer vector (going slower is fine though):

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;
}

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 : CreatureState
{
    [SerializeField] private AnimationClip _Walk;
    [SerializeField] private AnimationClip _Run;

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

    private void UpdateAnimation()
    {
        AnimationClip playAnimation, otherAnimation;
        
        if (Creature.Brain.IsRunning)
        {
            playAnimation = _Run;
            otherAnimation = _Walk;
        }
        else
        {
            playAnimation = _Walk;
            otherAnimation = _Run;
        }
        
        var playState = Animancer.Play(playAnimation, 0.25f);
        
        var speed = Mathf.Min(Creature.Brain.MovementDirection.magnitude, 1);
        playState.Speed = speed;
        
        var otherState = Animancer.GetState(otherAnimation);
        if (otherState != null && otherState.IsPlaying)
        {
            playState.NormalizedTime = otherState.NormalizedTime;
            otherState.Speed = speed;
        }
    }

    private void UpdateTurning()
    {
        if (Creature.Brain.MovementDirection == Vector3.zero)
            return;

        var targetAngle = Mathf.Atan2(Creature.Brain.Movement.x, Creature.Brain.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;
    }

    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;
    }
}

Keyboard Brain

We now have the components for a simple creature 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 Creature 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 : CreatureBrain
{
    [SerializeField] private CreatureState _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 != Vector2.zero)
    {
        _Locomotion.TryEnterState();
    }
    else
    {
        Creature.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 = Vector3.zero;.

The full KeyboardBrain script looks like this

using Animancer.FSM;
using UnityEngine;

public sealed class KeyboardBrain : CreatureBrain
{
    [SerializeField] private CreatureState _Locomotion;

    private void Update()
    {
        var input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
        if (input != Vector2.zero)
        {
            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 = Vector3.zero;

            Creature.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 Creature we are about to rebuild.

  1. Right Click in the Hierarchy and create an empty game object.
  2. Rename it to "Creature".
  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 Creature 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 creature thanks to the Reset method we put in the base state class.

  1. Drag the LowPolyMan model (from Assets/Plugins/Animancer/Examples/Art/Low Poly Man) into the Hierarchy as a child of the Creature 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 Creature and rename it to "Brain".
  4. Add a KeyboardBrain and LocomotionState and assign their references.
  5. Go back to the root Creature 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: