Finite State Machines

A Finite State Machine (often abbreviated as "State Machine" or "FSM") is a system which manages the current state of an object and the other states it can be in. For example, a treasure chest can either be open or closed; that is two states, though you might not necessarily set up a full FSM for something that simple. A character on the other hand often has many states such as idle, walk, run, attack, and so on, therefore a FSM will often be a good choice for managing that behaviour.

Animancer is not tied to a specific FSM implementation, meaning you can use any system you like. Instead of trying to squeeze a restrictive FSM into the animation system like Mecanim, you manage the states yourself and Animancer simply does what it is told. That said, Animancer does include a general purpose FSM system which is entirely separate from the animation system and is flexible enough for most needs. The State Machines examples demonstrate how to use it and the 3D Game Kit example demonstrates how to convert an Animator Controller based character to use this system instead.

The system is located in the Animancer.FSM namespace with its source scripts in the Assets/Plugins/Animancer/Utilities/State Machine folder (the full source code of the system is included in Animancer Lite). It was designed with the following goals in mind:

  • Flexible: states can be MonoBehaviour components, ScriptableObjects, or regular C# classes.
  • Generic: you define your own base state type to suit your needs. For example, it does not automatically give every state an Update function, but if you put one in your base state type you will be able to call stateMachine.CurrentState.Update() as necessary. Or if you use State Behaviours you can use the regular MonoBehaviour Messages like Update or FixedUpdate.
  • Simple: much like Animancer itself, you do not need to configure the state machine structure upfront. The base StateMachine<TState> class is just a single CurrentState property with some rules about when it is allowed to be changed. If you do want to configure states upfront, the StateMachine<TKey, TState> class adds a Dictionary to allow states to be registered and accessed using a chosen type of key, such as an enum.

There are plenty of other FSM systems available online (including on the Unity Asset Store), or if you’re interested in implementing your own system Unity has a tutorial series about building a Pluggable AI With Scriptable Objects which could easily be adapted for other purposes.

While Animancer itself cannot be used as a fully fledged FSM system, it is possible to give it extra code to execute whenever it updates by implementing the IUpdatable interface.

Base State Types

The base type for all states is the IState<TState> Interface which defines two pairs of methods:

  • CanEnterState and CanExitState allow the state to determine if it is allowed to be entered/exited at the moment.
  • OnEnterState and OnExitState notify the state when it is actually entered/exited.

When you call StateMachine.TrySetState, it checks oldState.CanExitState and newState.CanEnterState, then if both returned true it calls oldState.OnExitState and newState.OnEnterState. You can also use StateMachine.ForceSetState to skip the Can... checks and force the change.

Some examples:

  • You cannot Jump if you are not on the ground so Jump.CanEnterState checks whether the GroundDetector says the character is on the ground.
  • You cannot perform other actions during an Attack so Attack.CanExitState returns false.
  • Attacks can be interrupted if you get hit or even killed though, so instead of always returning false, Attack.CanExitState returns true if the nextState (provided as a parameter) is Flinch or Die. This is often implemented by using an enum to indicate the type of each state so you might have multiple different Attack scripts (perhaps for melee vs. ranged or to implement attack combos for some creatures but not others) yet they all use the same CreatureAction.Attack value to indicate their type.
  • Once the Attack animation finishes, it needs to be able to return to Idle. This could be done by having Attack.CanExitState check the current animation time as well, but since the Attack state already knows it is ending and Idle is unlikely to impose any constraints on when it can be entered we can simply use ForceSetState to skip the check.

There are also several other more specialised base types that Inherit from IState<TState> which are listed below:

Owned States

By default, states do not need a reference to the StateMachine they are used in, however this makes them slightly less convenient to use so if your state type implements IOwnedState<TState> (which inherits from IState<TState>) then you can use several Extension Methods in the StateExtensions class with your states. For example:

public sealed class Creature : MonoBehaviour
{
    [SerializeField]
    private CreatureState _Idle;
    public CreatureState Idle { get { return _Idle; } }

    [SerializeField]
    private CreatureState _Walk;
    public CreatureState Walk { get { return _Walk; } }

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

    private void Awake()
    {
        // Initialise the FSM to start in the Idle state.
        StateMachine = new StateMachine<CreatureState>(_Idle);
    }
}

public sealed class CreatureBrain : MonoBehaviour
{
    [SerializeField]
    private Creature _Creature;

    private void Update()
    {
        if (Input.GetKey(KeyCode.W))
        {
            // If CreatureState implements IState<CreatureState>:
            _Creature.StateMachine.TrySetState(_Creature.Walk);

            // If CreatureState implements IOwnedState<CreatureState> we can use the above or:
            _Creature.Walk.TryEnterState();
        }
    }

    private bool IsWalking()
    {
        // If CreatureState implements IState<CreatureState>:
        return _Creature.StateMachine.CurrentState == _Creature.Walk;

        // If CreatureState implements IOwnedState<CreatureState> we can use the above or:
        return _Creature.Walk.IsCurrentState();
    }
}

State Behaviours

The StateBehaviour<TState> class Inherits from MonoBehaviour and implements IState<TState> to act as the base class for states that are attached as components on GameObjects. It implements the OnEnterState and OnExitState methods to enable and disable itself respectively, allowing you to use the regular MonoBehaviour Messages like OnEnable and OnDisable to implement your state transition logic, as well as other messages like Update or FixedUpdate depending on the needs of each individual state.

Most of the State Machines examples use State Behaviours.

Delegate States

The DelegateState<TState> class also implements IState<TState>, but rather than defining its own logic it simply has a Delegate for each of the methods in the interface so you can assign them when creating the state.

Input Buffers

Rather than simply attempting a state change and giving up if it isn't allowed, it is often desirable to keep trying for a short time. For example, if the player is attacking and they press the attack button again, you probably want the current animation to finish first and then start the next attack in a combo as long as the input didn't occur too long ago. Note that "input" in this case does not only mean "button presses from the player", it can be used for any sort of events that might try to control a state machine (such as AI).

The StateMachine<TState>.InputBuffer class (and the corresponding class in StateMachine<TKey, TState>) allows you to easily implement this sort of buffering:

  1. Construct a new StateMachine<TState>.InputBuffer by passing in the target state machine.
  2. Keep a reference to the buffer and whenever you would normally call TrySetState on the state machine, call it on the buffer instead so that you can specify the desired timeOut duration.
  3. Call Update on the buffer every frame to have it try to enter the buffered state again (if there is one).

The Weapons example demonstrates how to use it.

This system only buffers a single state at a time. If the player presses the attack button while they are already attacking then presses the block button before the first attack ends, it will only execute the block rather than performing the entire second attack before blocking. This assumes the latest command is the only one that matters, which is often good for things like player input, but might not be best for all situations. If you want to ensure that all commands are executed in order, you could make a similar class (perhaps called InputSequenceBuffer) which manages a list of states instead of only a single BufferedState.