06-03 Brains

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

Recommended After: Characters and Linear Blending

Learning Outcomes: in this example you will learn:

How to separate inputs from actions and why doing so is useful.

How to effectively organize other parts of a character's logic beyond their state machine.

How to make a character turn in the direction they want to move.

This example expands upon the Characters example to allow the character to walk and run around the scene. It goes into further detail about why it's useful to manage input using a "brain" script and introduces several possible ways of communicating between the brain and the character's other scripts.

Summary

  • Dividing what a character wants to do into a separate script from the logic that controls what they actually can do and the execution of those actions can greatly improve the flexibility and reusability of your code.
  • Using a central class to hold parameters such as the direction the character wants to move is an effective way of communicating between the brain and the character's other scripts.

Controls

  • WASD = Move
  • Left Shift = Run
  • Left Click = Shoot

Overview

The general code structure of the Characters example can still be seen here, but there are some notable changes and additions:

Change Description
CapsuleCollider and Rigidbody Allows the character to move around the scene without going through walls.
CharacterParameters Used for communicating between the brain and other components. The Character component always had this field, but it wasn't used in earlier examples.
RootMotionRedirect Takes the root motion from animations which would normally be applied to the character model and applies it to the Rigidbody instead.
MoveState Replaces the second IdleState from earlier examples. It contains logic for walking, running, and turning based on the CharacterParameters.
MovingCharacterBrain Replaces the SimpleCharacterBrain from earlier examples as the script that controls the character's actions based on user input.

Click here to show the code structure from the Characters example:

The additions from the Interruptions example aren't included in this example because they aren't relevant to the topics discussed here and would only add unnecessary complexity to the scene. But those components would still work if you wanted to add them (a HealthPool and FlinchState for the character, and ClickToInteract somewhere else in the scene).

Why Brains?

Consider the steps that are involved in getting a character to perform an action:

  1. Detect that the Left Mouse Button was pressed.
  2. Check if the character was already doing something else that would prevent them from performing the action.
  3. Play the action animation.

Only step 1 of that process is relevant to determining what the character wants to do (or in this case, what the player wants them to do) so that's the only part that goes in the brain. When it detects the button press it tells the character to attempt the action and leaves the remaining steps up to the other systems.

This Separation of Concerns means that each type of state only needs to be implemented once and will then be usable by any kind of character, whether it be the player, or an NPC. This also makes it easy to change the way a character is controlled without affecting the way any of their actions actually work, either by modifying the brain or by assigning a different brain.

The term "Brain" was chosen because it accurately describes its purpose, which is to control the behaviour of a Character. But feel free to use other terminology that suits you such as "Character Input".

Parameters

Earlier examples had the brain SimpleCharacterBrain telling the StateMachine when to enter a particular state, but in this example the MoveState also needs to know which direction the brain wants to move and whether it wants to run or not. You could put those parameters in the brain or in the Character, but for the sake of effective Separation of Concerns this example has a dedicated CharacterParameters class.

This class doesn't really need to be serializable since it's only set at runtime, but making it serializable means it can easily show the current parameter values in the Inspector which can be very helpful for debugging.

[Serializable]
public sealed class CharacterParameters
{

The MovementDirection is a Vector3 indicating which way the character wants to move. If the brain wants to move slowly we can allow a small vector to be assigned, but we don't want to allow the brain to make the character move faster by just setting a larger vector so we use Vector3.ClampMagnitude to limit the magnitude of the assigned value to 1:

    [SerializeField]
    private Vector3 _MovementDirection;
    public Vector3 MovementDirection
    {
        get => _MovementDirection;
        set => _MovementDirection = Vector3.ClampMagnitude(value, 1);
    }

WantsToRun doesn't need any special validation but we still want it to have a field with the [SerializeField] attribute so we use a ref property which allows the value to be get and set without needing to write the actual get and set methods:

    [SerializeField]
    private bool _WantsToRun;
    public ref bool WantsToRun => ref _WantsToRun;
}

Parameters Field

The CharacterParameters is used as a serialized field in the Character class:

class Character
{
    ...
    
    [SerializeField]
    private CharacterParameters _Parameters;
    public CharacterParameters Parameters => _Parameters;
}

Moving Character Brain

With the parameters defined, we're now ready to make a brain to control them. This new brain class is identical to SimpleCharacterBrain from the Characters example, except for the UpdateMovement method:

public sealed class MovingCharacterBrain : MonoBehaviour
{
    [SerializeField] private Character _Character;
    [SerializeField] private CharacterState _Move;
    [SerializeField] private CharacterState _Action;

    private void Update()
    {
        UpdateMovement();
        UpdateAction();
    }

    private void UpdateMovement()
    {
        // This method is different and will be explained below.
    }

    private void UpdateAction()
    {
        if (ExampleInput.LeftMouseUp)
            _Character.StateMachine.TryResetState(_Action);
    }
}

SimpleCharacterBrain and MovingCharacterBrain were implemented as completely separate scripts to avoid confusion, but with a few changes they could utilise Inheritance to avoid needing to repeat so much of the same code:
  1. Remove the sealed keyword from SimpleCharacterBrain so it can be inherited.
  2. Change the UpdateMovement method in SimpleCharacterBrain from private to protected virtual so it can be overridden.
  3. Change the _Move method in SimpleCharacterBrain from private to protected so it can be accessed in an inheriting class.
  4. Change the UpdateMovement method in MovingCharacterBrain from private to protected override so it replaces the method in the base class (SimpleCharacterBrain).
  5. Remove everything else from MovingCharacterBrain because it's all inherited from the base class now.

Where the SimpleCharacterBrain.UpdateMovement method only had to try to enter the Move or Idle state, this one also needs to set the MovementDirection and WantsToRun parameters.

First, it checks if the input is not zero:

private void UpdateMovement()
{
    var input = ExampleInput.WASD;
    if (input != default)
    {

It needs to take the user's screen-space WASD input and convert that into a world-space vector to set the MovementDirection. It's important to do this conversion here in the brain because this is where code that's specific to player input is supposed to go. It wouldn't make sense to do it in CharacterParameters or MoveState because those classes are supposed to be usable by any character and this conversion is only relevant to the player.

First, it gets 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 it can build the movement vector by multiplying the input values by those axes:

        _Character.Parameters.MovementDirection =
            right * input.x +
            forward * input.y;

And it still needs to try to enter the movement state:

        _Character.StateMachine.TrySetState(_Move);
    }

If the player isn't trying to move, clear the movement vector and return to idle:

    else
    {
        _Character.Parameters.MovementDirection = default;
        _Character.StateMachine.TrySetDefaultState();
    }

And finally, indicate whether the character wants to run or not:

    _Character.Parameters.WantsToRun = ExampleInput.LeftShiftHold;
}

Why is the _Move field a base CharacterState instead of the more specific MoveState?

You might be wondering why the brain uses:

[SerializeField] private CharacterState _Move;

Instead of:

[SerializeField] private MoveState _Move;

It's because nothing in the brain actually cares what specific type of state it is, as long as it inherits from CharacterState to be usable in the Character's StateMachine.

In the Weapons example, the brain needs to set the NextWeapon before entering the EquipState so that brain has an actual EquipState field.

But this example isn't doing that so the field might as well be as unrestrictive as possible which means you could potentially use the same control logic on a different type of state without needing to modify this brain.

Move State

Where the earlier examples simply used an IdleState with a movement animation assigned, this time we have a proper MoveState which is responsible for Walking and Running as well as Turning:
public sealed class MoveState : CharacterState
{
    [SerializeField, DegreesPerSecond] private float _TurnSpeed = 360;
    [SerializeField] private float _ParameterFadeSpeed = 2;
    [SerializeField] private LinearMixerTransition _Animation;

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

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

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

    private void UpdateTurning()
    {
        var movement = Character.Parameters.MovementDirection;
        if (movement == default)
            return;

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

        var turnDelta = _TurnSpeed * Time.deltaTime;

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

Walking and Running

MoveState is responsible for both walking and running. Two animation states managed by one logical state because they both behave the same way. This could be implemented by simply giving it two ClipTransition fields, but then whenever it changes between them it would need to synchronize their times to avoid going from one part of the walk animation to a completely different part of the run animation. That problem is explained further in the Mixer Synchronization section and that's also the solution, because mixers already have inbuilt synchronization so this state can just use a linear mixer:

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

That mixer is set up with two animations:

  • Humanoid-Walk at threshold 0.
  • Humanoid-Run at threshold 1.

When this state is entered, it plays the mixer and sets the mixer parameter based on the character' parameters:

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

The above assignment uses a Conditional Expression: if WantsToRun is true, it will set the parameter to 1, otherwise it will set it to 0.

While this state is active, every frame it will need to update the mixer based on the input parameter and also turn the character towards the direction they're moving:

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

Mixer parameters immediately jump to the value they are given rather than moving gradually, which would cause the character to snap between walking and running poses without any blending. That might be what you want, but otherwise the Mixer Smoothing section explains how you can interpolate the value yourself. In this case, the target value could change at any time so we use Mathf.MoveTowards to continually move a set distance towards it every frame:

    [SerializeField] private float _ParameterFadeSpeed = 2;

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

Note that this example moves the character around using Root Motion so this state doesn't do any actual moving. If you don't want to use Root Motion, you could give the Character a reference to their Rigidbody (or whatever character controller system you are using) and control it in this state as well.

Turning

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.

So the MoveState script has a field to determine how fast it can turn (measured in degrees per second):

    [SerializeField, DegreesPerSecond] private float _TurnSpeed = 360;

Firstly, it needs to make sure it actually has a MovementDirection, because it wouldn't know which way to turn if it somehow ended up in this state without the brain setting the direction:

    private void UpdateTurning()
    {
        var movement = Character.Parameters.MovementDirection;
        if (movement == default)
            return;

Then it determines the angle it wants to turn towards:

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

Without going into the maths behind it, Mathf.Atan2 gives the angle of a vector in radians. So the state just feeds in the x and z values to get the angle around the y axis, then converts the result to degrees because Transform.eulerAngles uses degrees.

Then it calculate how far to rotate this frame:

        var turnDelta = _TurnSpeed * Time.deltaTime;

Then it gets the Transform.eulerAngles, moves the y value towards the target angle, and applies 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;
    }
}

It rotates the DefaultHumanoid model independently from the root Character object because the Character has a CapsuleCollider and Rigidbody on it. The capsule is only a vague approximation of the character's shape anyway, so there's no need for the rotation to have any interaction with the physics system.

Root Motion

This example uses root motion to move the character. Since the model (with the Animator and AnimancerComponent) is a child of the character's root (with the CapsuleCollider, Rigidbody, and Character) the root motion would normally be applied only to the model so it would move independently of the character's physics. That's obviously undesirable so the RootMotionRedirect script uses the Redirecting Root Motion technique introduced in the Root Motion example to apply it to the parent Rigidbody instead.

public sealed class RootMotionRedirect : MonoBehaviour
{
    [SerializeField] private Rigidbody _Rigidbody;
    [SerializeField] private Animator _Animator;

    private void OnAnimatorMove()
    {
        if (_Animator.applyRootMotion)
        {
            _Rigidbody.MovePosition(_Rigidbody.position + _Animator.deltaPosition);
            _Rigidbody.MoveRotation(_Rigidbody.rotation * _Animator.deltaRotation);
        }
    }
}

Conclusion

Now we have a character who can walk, run, turn, and shoot:

What Next?

Example Topic
Weapons Defining animations separately from the character (i.e. on weapons) and swapping between them dynamically.
Platformer Game Kit A separate (free) package which demonstrates a much more complete character implementation for a 2D platformer game. In addition to its Player Input Brain it also has a Behaviour Tree Brain which controls the enemies using a simple node based system.
3D Game Kit A more complex character framework based on Unity's 3D Game Kit Lite.