04 More Brains

Difficulty: Intermediate

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

Namespace: Animancer.Examples.StateMachines.Brains

This example expands upon the Brains example to demonstrate how you can control each creature differently by using different brains even though they share the same scripts for actually performing actions. Specifically, it adds a new brain class that uses mouse input instead of keyboard input.

This concept can be applied to anything that might need to control a creature such as artificial intelligence for non-player characters, network messages in a multiplayer game, or pre-set events in a cutscene system.

Mouse Brain

We are going to create a single new script to control the character with the mouse like you might in a top down game. Instead of moving in response to constant input like the KeyboardBrain, this time we are going to implement a system where you click somewhere to have your character walk there on their own. In a real game you might use a pathfinding algorithm to have the character avoid obstacles in their way, but for this example we will just walk straight towards the target.

This script starts off the same as the KeyboardBrain with a reference to the locomotion state:

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

Picking a Target

The first thing we need is a field to store the target point:

private Vector3 _Destination;

Then in Update we can check for mouse clicks:

private void Update()
{
    if (Input.GetMouseButton(0))
    {
        _Destination = ???
    }
}

Note that we used GetMouseButton rather than GetMouseButtonDown so that it will continue recalculating the target point every frame while the button is held.

In order to find the world position the mouse cursor is actually pointing at we need to use a Raycast:

var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

RaycastHit raycastHit;
if (Physics.Raycast(ray, out raycastHit))
{
    _Destination = raycastHit.point;
}

We want that ray to hit terrain but not the player (or any other creatures). Normally you might have creatures on their own layer and make a LayerMask as a Serialized Field to pass into the Raycast so you can use the Inspector to choose which layers it can hit. But since we do not want to mess with your project's layer settings just for this example, we can just go to the scene and set the Creature to the Ignore Raycast layer using the dropdown menu in the top right of the Inspector.

That raycast will tell us where the mouse cursor is pointing on the terrain (specifically, on any objects with Collider components on layers that are included in whatever LayerMask we use) but it cannot help us if the mouse is pointing out the edge of the scene so the ray doesn't hit any collider, which we can visualise using some Debug Lines:

if (Physics.Raycast(ray, out raycastHit))
{
    _Destination = raycastHit.point;
    Debug.DrawLine(ray.origin, raycastHit.point, Color.green);
}
else
{
    Debug.DrawRay(ray.origin, ray.direction * 10000, Color.red);
}

To fix that, we can use a bit of simple maths to just find out where the ray intersects the horizontal XZ plane at a specific Y height:

public static Vector3 CalculateRayTargetXZ(Ray ray, float y = 0)
{
    return ray.origin - ray.direction * ((ray.origin.y - y) / ray.direction.y);
}

Then we can use that method for when the raycast misses, using the creature's current Y position as the intersect height:

else
{
    _Destination = CalculateRayTargetXZ(ray, Creature.Rigidbody.position.y);
}

The full Update method now looks like this:

private void Update()
{
    if (Input.GetMouseButton(0))
    {
        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        RaycastHit raycastHit;
        if (Physics.Raycast(ray, out raycastHit))
        {
            _Destination = raycastHit.point;
            Debug.DrawLine(ray.origin, raycastHit.point, Color.green);
        }
        else
        {
            _Destination = CalculateRayTargetXZ(ray, Creature.Rigidbody.position.y);
            Debug.DrawLine(ray.origin, _Destination.Value, Color.red);
        }
    }
}

Moving Towards the Target

Now that we know where the player wants to go, we just need to tell the creature which way that is. We also want to do this in Update, so first we should split the method we just wrote out to keep the script organised:

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

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

private void UpdateMovement()
{
    // TODO.
}

So far we have a destination point, but the MovementDirection needs to actually be the direction from the creature to the destination. At a basic level, that only requires a simple Vector Subtraction:

MovementDirection = _Destination - Creature.Rigidbody.position;

The last thing we need to do is tell the creature to enter the locomotion state:

_Locomotion.TryEnterState();

This would give us a functional script which we could test in the scene, but it has an obvious problem: we are always telling it to move so it will immediately move towards the default (0, 0, 0) on startup and even after we pick a destination it will constantly try to move towards that point without returning to Idle once it arrives.

Cancelling the Target

We could use a bool _HasDestination field, but this is a good use case for a C# feature called Nullable Types.

By putting a ? after the field type, we can change it from "always a Vector3" to "either a Vector3 or null" (which will default to null):

private Vector3? _Destination;

The UpdateInput method can stay exactly the same, we still always want a destination when we click but when we implement UpdateMovement we will be able to clear it by setting _Destination = null; once the creature arrives at that point.

But first we need a field to specify how close we want to try to get to the target because the character will practically never end up at exactly that point. We could try to adjust the movement speed to be more precise as we did in the Directional Movement example, but in order to keep this example simple we will just stop when the character gets pretty close using a simple threshold field:

[SerializeField] private float _StopDistance = 0.2f;

Start the method by making sure we actually have a destination and calculating the vector from the current position to the destination just like before (except that now we need to use _Destination.Value to access the actual Vector3 value of the nullable):

private void UpdateMovement()
{
    if (_Destination != null)
    {
        var fromCurrentToDestination = _Destination.Value - Creature.Rigidbody.position;

Then we compare the magnitude of that vector (its length) to the _StopDistance. As noted in the Vector Magnitude section, calculating the squared magnitude is faster than calculating the actual magnitude so we can use that because we only need to see which is greater:

        if (fromCurrentToDestination.sqrMagnitude > _StopDistance * _StopDistance)
        {

We could square the _StopDistance on startup to avoid that multiplication every frame, but that is a far smaller performance gain which would probably not be worth the potential confusion about what the value means at any given time (because confusion creates opportunities for bugs).

If the remaining distance is greater than the stop distance, we set the MovementDirection and try to enter the Locomotion state:

            MovementDirection = fromCurrentToDestination;
            _Locomotion.TryEnterState();

We can also use another Debug Line to draw a line from the creature to its destination in case we ever need to see where a creature is going:

            Debug.DrawLine(Creature.Rigidbody.position, _Destination.Value, Color.cyan);

Lastly we just need to return to end the method early if the destination is far enough away that we still want to be in the Locomotion state, but otherwise we want to make sure everything is cleared and try to return to Idle:

            return;
        }
    }

    _Destination = null;
    MovementDirection = Vector3.zero;
    Creature.Idle.TryEnterState();

Better Ray Targeting

This is also a good time to fix a potential issue with the CalculateRayTargetXZ method. If the camera is above the target plane and the ray is pointing further up then it will obviously not intersect that plane by going forward, however the current implementation will calculate the intersection back in the other direction and return a point behind the camera which is very unlikely to be what the player actually intended to happen. The same thing happens if the camera is below the target plane and the ray is pointing downward:

Fortunately, now that we have made the _Destination nullable, we can simply make the CalculateRayTargetXZ method return a nullable value as well so it can return null if the ray will not intersect the target plane by going forward:

public static Vector3? CalculateRayTargetXZ(Ray ray, float y = 0)
{
    y = ray.origin.y - y;

    if (ray.direction.y == 0 || SameSign(y, ray.direction.y))
        return null;

    return ray.origin - ray.direction * (y / ray.direction.y);
}

public static bool SameSign(float x, float y)
{
    return
        (x > 0 && y > 0) ||
        (x < 0 && y < 0) ||
        (x == 0 && y == 0);
}

// Now we also need to null check the destination before drawing its debug line:
private void UpdateInput()
{
...

    _Destination = CalculateRayTargetXZ(ray, creaturePosition.y);
    if (_Destination != null)
        Debug.DrawLine(ray.origin, _Destination.Value, Color.red);
    
    ...
}

The full MouseBrain script looks like this so far:

using Animancer;
using Animancer.FSM;
using UnityEngine;

public sealed class MouseBrain : CreatureBrain
{
    [SerializeField] private CreatureState _Locomotion;
    [SerializeField] private float _StopDistance = 0.2f;

    private Vector3? _Destination;

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

    private void UpdateInput()
    {
        if (Input.GetMouseButton(0))
        {
            var creaturePosition = Creature.Rigidbody.position;

            var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            RaycastHit raycastHit;
            if (Physics.Raycast(ray, out raycastHit))
            {
                _Destination = raycastHit.point;
                Debug.DrawLine(ray.origin, raycastHit.point, Color.green);
            }
            else
            {
                _Destination = CalculateRayTargetXZ(ray, Creature.Rigidbody.position.y);
                if (_Destination != null)
                    Debug.DrawLine(ray.origin, _Destination.Value, Color.red);
            }
        }
    }

    public static Vector3? CalculateRayTargetXZ(Ray ray, float y = 0)
    {
        y = ray.origin.y - y;

        if (ray.direction.y == 0 || SameSign(y, ray.direction.y))
            return null;

        return ray.origin - ray.direction * (y / ray.direction.y);
    }

    public static bool SameSign(float x, float y)
    {
        return
            (x > 0 && y > 0) ||
            (x < 0 && y < 0) ||
            (x == 0 && y == 0);
    }

    private void UpdateMovement()
    {
        if (_Destination != null)
        {
            var fromCurrentToDestination = _Destination.Value - Creature.Rigidbody.position;

            if (fromCurrentToDestination.sqrMagnitude > _StopDistance * _StopDistance)
            {
                MovementDirection = fromCurrentToDestination;
                _Locomotion.TryEnterState();
                Debug.DrawLine(Creature.Rigidbody.position, _Destination.Value, Color.cyan);
                return;
            }
        }

        _Destination = null;
        MovementDirection = Vector3.zero;
        Creature.Idle.TryEnterState();
    }
}

Seeing it in Action

We can now go back to the scene if we want to see it in action:

  1. Select the Brain object and disable the old KeyboardBrain.
  2. Add the new MouseBrain and assign its Locomotion reference.
  3. Select the root Creature object and drag the Brain object into its Brain field to assign the new brain we just added.

This gives us a character that we can steer around as long as we hold the mouse button and after we release they will continue moving to the past spot we were aiming. And we can still rotate the camera with Right Click thanks to the OrbitControls script.

Running

We could bind running to a key like the KeyboardBrain, but instead let's keep everything on the mouse by making the character run while we hold the button then walk once they get close to the destination or once the player releases the button.

First we need a field to determine how close they should get before walking:

[SerializeField] private float _MinRunDistance = 1;

Then we just need to make some simple changes at the end of UpdateInput:

private void UpdateInput()
{
    if (Input.GetMouseButton(0))
    {
        ...

        IsRunning = 
            _Destination != null &&
            Vector3.Distance(creaturePosition, _Destination.Value) >= _MinRunDistance;
    }
    else
    {
        IsRunning = false;
    }
}

Now the character runs while we hold the button:

Live Brain Transplants

Usually each creature will have a single brain for their entire lifetime, however the fact that we have designed them as modular components means that you can easily swap between them if needed. This can be useful in a veriety of circumstances such as:

  • When a player leaves a multiplayer game, their ControllerInputBrain could be replaced by an AIBrain.
  • When a cutscene starts, it might want to take control of certain enemies and/or the player.
  • When a creature is hit by a Fear spell, it could swap their brain for a FearBrain which makes them run away for the duration of the spell before returning control to their regular brain.

In this example we are going to swap between the two brain types we have made so far using simple UI Buttons.

Mouse Brain

To get the MouseBrain ready for the swap, we need to do one last thing: make sure the destination is cleared when it gets re-enabled (you may or may not want to do this in a real game):

private void OnEnable()
{
    _Destination = null;
}

So the final MouseBrain script looks like this:

using Animancer;
using Animancer.FSM;
using UnityEngine;

public sealed class MouseBrain : CreatureBrain
{
    [SerializeField] private CreatureState _Locomotion;
    [SerializeField] private float _StopDistance = 0.2f;
    [SerializeField] private float _MinRunDistance = 1;

    private Vector3? _Destination;

    private void OnEnable()
    {
        _Destination = null;
    }

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

    private void UpdateInput()
    {
        if (Input.GetMouseButton(0))
        {
            var creaturePosition = Creature.Rigidbody.position;

            var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            RaycastHit raycastHit;
            if (Physics.Raycast(ray, out raycastHit))
            {
                _Destination = raycastHit.point;
                Debug.DrawLine(ray.origin, raycastHit.point, Color.green);
            }
            else
            {
                _Destination = CalculateRayTargetXZ(ray, Creature.Rigidbody.position.y);
                if (_Destination != null)
                    Debug.DrawLine(ray.origin, _Destination.Value, Color.red);
            }

            IsRunning =
                _Destination != null &&
                Vector3.Distance(creaturePosition, _Destination.Value) >= _MinRunDistance;
        }
        else
        {
            IsRunning = false;
        }
    }

    public static Vector3? CalculateRayTargetXZ(Ray ray, float y = 0)
    {
        y = ray.origin.y - y;

        if (ray.direction.y == 0 || SameSign(y, ray.direction.y))
            return null;

        return ray.origin - ray.direction * (y / ray.direction.y);
    }

    public static bool SameSign(float x, float y)
    {
        return
            (x > 0 && y > 0) ||
            (x < 0 && y < 0) ||
            (x == 0 && y == 0);
    }

    private void UpdateMovement()
    {
        if (_Destination != null)
        {
            var fromCurrentToDestination = _Destination.Value - Creature.Rigidbody.position;

            if (fromCurrentToDestination.sqrMagnitude > _StopDistance * _StopDistance)
            {
                MovementDirection = fromCurrentToDestination;
                _Locomotion.TryEnterState();
                Debug.DrawLine(Creature.Rigidbody.position, _Destination.Value, Color.cyan);
                return;
            }
        }

        _Destination = null;
        MovementDirection = Vector3.zero;
        Creature.Idle.TryEnterState();
    }
}

Currently the references between the creature and brain (the Creature.Brain and CreatureBrain.Creature properties) are both read-only. We could just give them basic setters, but then anything changing them will need to handle disconnecting the previous references and connecting both of the new references (the new brain needs a reference to the creature and the creature needs a reference to that brain). It would be much better if we implemented the setters to take care of the cross-referencing for us:

So in CreatureBrain we change the Creature property to:

public Creature Creature
{
    get { return _Creature; }
    set
    {
        if (_Creature == value)
            return;

        var oldCreature = _Creature;
        _Creature = value;

        // Make sure the old creature doesn't still reference this brain.
        if (oldCreature != null)
            oldCreature.Brain = null;

        // Give the new creature a reference to this brain.
        // We also only want brains to be enabled when they actually have a creature to control.
        if (value != null)
        {
            value.Brain = this;
            enabled = true;
        }
        else
        {
            enabled = false;
        }
    }
}

And in Creature we change the Brain property in the same way (except that there is no need to disable a Creature if it has no Brain):

public CreatureBrain Brain
{
    get { return _Brain; }
    set
    {
        if (_Brain == value)
            return;

        var oldBrain = _Brain;
        _Brain = value;

        // Make sure the old brain doesn't still reference this creature.
        if (oldBrain != null)
            oldBrain.Creature = null;

        // Give the new brain a reference to this creature.
        if (value != null)
            value.Creature = this;
    }
}

This allows either property to be set and it will disconnect the old creature and brain from each other then connect the new pair while also disabling the old brain to prevent it from updating any more and enabling the new one so it starts updating.

Note that because those properties both access each other, the order in which we do things is very important to prevent an infinite loop. For example:

  1. We set Creature.Brain.
  2. If it already has that brain, return immediately.
  3. Store that brain in the _Brain field.
  4. If the Creature already had a Brain, tell it to stop controlling that Creature.
  5. Set the Brain.Creature.
  6. If it is already controlling that creature, return immediately.
  7. Store that brain in the _Creature field.
  8. If the Brain already had a Creature, tell it the Brain is no longer controlling it.
  9. Set the Creature.Brain. This would be an infinite loop since it is the same as step 1, but because step 3 already stored the given brain, step 2 will return immediately this time.

Setting Up the Buttons

You would not generally have multiple brains attached to the same object, but for the sake of this example the simplest way to set it up is to simply put both on the Brain object:

One should be enabled while the other is disabled. It doesn't really matter which is which, as long as the enabled one is also assigned to the Creature's Brain field.

Then we can set up the button clicks to simply assign the Creature.Brain (as well as some UI Text to show which brain is active):

Keyboard Button Mouse Button

Now we can swap brains whenever we want: