06-01 Characters

Location: Samples/06 State Machines/01 Character

Recommended After: Library Character

Learning Outcomes: in this sample you will learn:

How to use Animancer's Finite State Machine system.

A useful pattern for dividing character logic into multiple scripts and why doing that is useful.

Summary

This sample demonstrates how the logic from the Library Character sample can be divided into multiple scripts using Animancer's Finite State Machine system.

  • This sample doesn't teach any new animation concepts.
  • It explains why you should split complex logic into multiple scripts and demonstrates how you can do so.
  • It uses Animancer's Finite State Machine system, but the general concepts can be utilised in practically any system.

Controls

  • W = Move
  • Left Click = Shoot

Introduction

This sample uses six scripts to achieve the same thing that LibraryCharacterAnimations did in one script. Keep reading if you're wondering why that might be a good idea. Otherwise, feel free to skip to the Overview.

The LibraryCharacterAnimations script has a lot of responsibilities squeezed into it:

  • Reference all the character's animations.
  • Play and manage those animations:
    • Set the Action animation to go to the Idle animation when it ends.
    • Start with the Idle animation.
    • Keep track of the character's current animation.
  • Manage when each action can be performed:
    • When the character is not performing an action, allow them to Idle, Move, or perform an Action.
    • When the character is performing an action, only allow them to restart the Action.
  • Read user input:
    • Detect when the user Left Clicks and perform an Action if allowed.
    • Detect when the user is holding the W key and play the Move animation if allowed.

Doing so many things in one script violates the Single Responsibility Principle. It's not too bad for a very simple situation like these samples, but game characters are often far more complex than that and can benefit greatly from having a properly designed code structure:

  • It makes it easier to accomodate changes throughout the development of a project as you add new features and try different things without breaking old code and needing to waste time fixing it.
  • It can potentially allow you to reuse scripts in multiple different situations throughout a project and in future projects.
  • It allows bugs to be found and fixed more easily, without breaking other code.

Sharing Logic

A mistake commonly made by inexperienced developers is to write classes with names like Player and Enemy which end up having a significant amount of identical code. This isn't always the case, but in a lot of games the enemies follow the same general structure and rules as the player:

Player Enemy
Has a model or sprite that can be animated. Has a model or sprite that can be animated.
Has a model or sprite that can be animated. Has a model or sprite that can be animated.
Has a health pool and can take damage. Has a health pool and can take damage.
Has a Rigidbody or CharacterController for movement. Has a Rigidbody or CharacterController for movement.
Has a system for managing the actions it can perform. Has a system for managing the actions it can perform.
Is controlled by user input from a controller/keyboard/mouse/etc. Is controlled by some form of AI.

Since there are so many similarities between them, it makes sense to structure their scripts in a way that allows them to share much of the same logic. Sharing logic is generally done by splitting it into multiple scripts and using Inheritance and/or Composition to connect them. This also helps adhere to the Single Responsibility Principle which aims to make code easier to reuse in different situations, easier to change throughout the development of a project, and easier to fix when bugs are found.

Overview

The general code structure in this sample has been used effectively in a wide range of different situations (both with and without Animancer) but like all design patterns it can't be perfect for everything so you should always feel free to add your own variations or use a different approach entirely depending on the needs of your situation.

Here's an overview of the main parts which make up a character in this system:

Script Description
Character A centralised group of references to the common parts of a character and their state machine.
State Machine Determines what the character is currently doing and enforces the entry/exit rules defined by individual States.
States Determine what the character could do and what actually happens when they do it.
Brain Decides what the character wants to do (as a result of player input, AI, etc.).

The code structure is much more complicated than the Library Character sample, but by the end of this sample you should understand how it all fits together.

Also note that this sample isn't using a Transition Library but it could if you wanted it to. Simply assign the same library from the Library Character sample and its presence will automatically modify the transitions at runtime.

Why a Character?

The Character script is responsible for holding references to other systems so they can all be easily referenced in one place. It's the core of any character, whether it be a player, enemy, NPC, person, animal, monster, or robot.

A different name may be more suitable depending on the type of game you are making - for example, a strategy game might call it a Unit while a space game might call it a Ship - but the idea is always the same: all of the game's characters share the same standard script which holds references to each of its other components (and typically very little logic of its own).

It references things like:

  • The animation system (an AnimancerComponent if you use Animancer, but otherwise it might just be an Animator).
  • A state machine (this sample uses Animancer's Finite State Machine system, but you can use any system you like).
  • Any other common features that most characters have such as a Rigidbody, character attributes, inventory, health pool, and so on.

Having this central script means that other scripts can simply have one reference to the Character and access all their other components through it. For example, the Action State wants to play an animation and then return the character to their Idle State. So instead of needing to reference the AnimancerComponent, StateMachine, and idle CharacterState, it just references the Character so it can access the Character.Animancer, Character.StateMachine, and Character.Idle.

With that said, don't fall into the trap of thinking everything is a character. Otherwise, you might end up in a situation like Fallout 3 where the easiest way for them to implement a moving train was to make it as a character's hat and hide the character underground.

What's in this Character?

It references the AnimancerComponent:

public class Character : MonoBehaviour
{
    [SerializeField]
    private AnimancerComponent _Animancer;

And has a read-only Property so other scripts can use that reference but not re-assign it:

    public AnimancerComponent Animancer => _Animancer;

The other fields aren't used in this sample but are properly explained in later samples which reuse the scripts from this one:

Code Sample
[SerializeField]
private HealthPool _Health;
public HealthPool Health => _Health;
Interruptions
[SerializeField]
private CharacterParameters _Parameters;
public CharacterParameters Parameters => _Parameters;
Brains
[SerializeField]
private Equipment _Equipment;
public Equipment Equipment => _Equipment;
Weapons

In a real project, this is where you would also put references to things like a Rigidbody or CharacterController, a script that detects whether the character is currently on the ground, character stats, an inventory system, etc.

State Machine

The Character class also holds the StateMachine that will manage their actions. It uses the Serialized Field pattern explained on the Initialization page:

[DefaultExecutionOrder(-10000)]
public class Character : MonoBehaviour
{
    ...
    
    [SerializeField]
    private StateMachine<CharacterState>.WithDefault _StateMachine;
    public StateMachine<CharacterState>.WithDefault StateMachine => _StateMachine;

    protected virtual void Awake()
    {
        _StateMachine.InitializeAfterDeserialize();
    }
}

Specifically, it uses a State Machine with a Default State to easily return to Idle after performing an Action.

States

The base State Type in this sample is CharacterState.

It could Inherit from MonoBehaviour and implement IState, but it's easier to just Inherit from StateBehaviour:

public abstract class CharacterState : StateBehaviour
{

Every state has a reference to the Character it controls:

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

And in order to avoid needing the user to manually assign the Character for every state, it has an OnValidate method which uses the Extension Method Approach to automatically find an appropriate reference nearby:

#if UNITY_EDITOR
    protected override void OnValidate()
    {
        base.OnValidate();

        gameObject.GetComponentInParentOrChildren(ref _Character);
    }
#endif

That method needs to be in a #if UNITY_EDITOR block because the base method it overrides in StateBehaviour is also in a #if UNITY_EDITOR block.

There are also several more properties to determine which states can interrupt each other. They're explained properly in the Interruptions sample:

    public virtual CharacterStatePriority Priority => CharacterStatePriority.Low;

    public virtual bool CanInterruptSelf => false;

    public override bool CanExitState
    {
        get
        {
            // There are several different ways of accessing the state change details:
            // CharacterState nextState = StateChange<CharacterState>.NextState;
            // CharacterState nextState = this.GetNextState();
            CharacterState nextState = _Character.StateMachine.NextState;
            if (nextState == this)
                return CanInterruptSelf;
            else if (Priority == CharacterStatePriority.Low)
                return true;
            else
                return nextState.Priority > Priority;
        }
    }
}

This sample has 3 states which use that base:

Idle State

The Idle state is very simple, just a Transition Asset which it plays when the state is entered:

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

    protected virtual void OnEnable()
    {
        Character.Animancer.Play(_Animation);
    }
}

CharacterState inherits from StateBehaviour which means it will automatically disable itself by default and then enable itself when the state machine enters that state. So the OnEnable method will effectively do the same as if we had used public override void OnEnterState().

Move State

There's no new script for the Move in this sample because IdleState already does exactly what it needs, so the character simply has a second IdleState component with a different animation assigned.

The Brains sample implements a proper MoveState to allow the character to actually move around the scene, including walking and running, as well as turning.

Action State

The Action state starts the same as the others but has a bit more complexity below:

public class ActionState : CharacterState
{
    [SerializeField] private TransitionAsset _Animation;

    protected virtual void OnEnable()
    {
        AnimancerState state = Character.Animancer.Play(_Animation);
        state.Events(this).OnEnd ??= Character.StateMachine.ForceSetDefaultState;
    }

Unlike Idle and Move, this animation isn't looping so we want to return to Idle when it ends so we set its End Event to return to the Default State.

It also overrides the properties that control how it can be interrupted (which will be explained properly in the Interruptions sample):

  • Priority returns Medium so that it can interrupt the IdleState but not be interrupted by it (because IdleState uses the default Low priority from the base CharacterState).
  • CanInterruptSelf returns true so that the character can still rapid fire like they could in the Library Character sample.
    public override CharacterStatePriority Priority => CharacterStatePriority.Medium;

    public override bool CanInterruptSelf => true;
}

Brain

A character's brain is responsible for deciding what they want to do. This section focusses on what this specific script does and the Brains sample explains why the system in general is designed this way.

There are two things this character can choose to do: Move and perform an Action. So the brain has a Character to control and fields for each of those actions:

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

Then it has an Update method which is similar to the Update method in the Basic Character sample, but this one is simpler because the responsibility of determining what the character can do at any given time is already handled by the State Machine:

    protected virtual void Update()
    {
        UpdateMovement();
        UpdateAction();
    }

The UpdateMovement and UpdateAction methods are also very similar to the Methods in the Basic Character sample and it avoids the extra complexity added in the Method Changes section because instead of directly telling Animancer to play animations it tells the character to try to enter states and leaves it up to those states to determine what actually happens:

    private void UpdateMovement()
    {
        float forward = SampleInput.WASD.y;
        if (forward > 0)
        {
            _Character.StateMachine.TrySetState(_Move);
        }
        else
        {
            _Character.StateMachine.TrySetDefaultState();
        }
    }

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

There are several different methods of Changing States used there:

Method Effect Why use it here?
TrySetState Will do nothing if the character was already in the target state. Because we call it every frame while the movement button is held and there's no point in repeatedly entering the Move state.
TryResetState Will re-enter the target state if the character was already in it. Because we want to allow Rapid Fire if the user clicks again while they were already performing the action.
TrySetDefaultState Calls TrySetState with the Default State. Because the brain doesn't have a reference to the character's Idle state (it could be given one, but this way it doesn't need one).

Scene Setup

Since we put all that effort into logically separating the character's scripts, it's worthwhile to logically group their scene objects as well to avoid ending up with a long list of components on a single object in the Inspector:

The Root Object has the Character and IdleState. In a real project, other components referenced by the Character such as a Rigidbody would also go here.

The Model has the Animator and AnimancerComponent. This allows the character's visuals to be easily swapped out for a different model.

The Brain and Actions has the BasicCharacterBrain, MoveState, and ActionState on the same object because the brain is what controls those actions.

There are plenty of other ways the character could be structured. Some approaches are better than others, but in the end it mostly comes down to personal preference.

You could put each state onto its own GameObject, but that's a lot of performance overhead to have an additional GameObject and Transform for every state.

You might also be tempted to put the Brain on a separate object from the Actions, but that can be inconvenient because when you drag and drop the Actions object from the Hierarchy into a CharacterState field on the Brain Unity will simply assign the first CharacterState component instead of letting you pick which one you want to assign.

  1. Right Click on the header tab of any Unity window (such as the Inspector) to open the context menu.
  2. Use the Add Tab menu to open another Inspector window.
  3. Select the Brain object.
  4. Click the Lock icon in the top right of one of the Inspector windows.
  5. Select the Actions object.
  6. Now the locked Inspector will be showing the Brain and the un-locked one will be showing the Actions so you can drag a specific state component into the field you want on the brain component.

With all that set up, now you can enter Play Mode to see the character in action.

What Next?

Sample Topic
Interruptions Ways of controlling which state changes are allowed and which aren't.
Brains A deeper look into the concept of character brains.
Weapons Equipping and attacking with various weapons that each have different animations.
Platformer Game Kit A separate (free) package which demonstrates a much more complete character implementation for a 2D platformer game.
3D Game Kit A more complex character framework based on Unity's 3D Game Kit Lite.