02 Interrupt Management

Difficulty: Intermediate

Location: Assets/Plugins/Animancer/Examples/06 State Machines/02 Interrupt Management

Namespace: Animancer.Examples.StateMachines.InterruptManagement

This example demonstrates how you can determine which states can interrupt each other using a simple Enum. It uses a Priority enum to indicate the importance of each state. Each time an action is attempted, it simply compares the priority of that state to the priority of the current state. If the new priority is equal or higher, then it performs that action.

Idle, Walk, and Run are Low priority, Swing is Medium, and Flinch is High, which means that:

  • We can swap between Idle, Walk, and Run freely.
  • Swing can interrupt any of those, but we cannot return to Idle, Walk, or Run during the Swing animation.
  • Flinch can interrupt any of the others, but nothing else can interrupt it.

Interruptable Boolean

In simple situations, all that you might need to implement an interruption management system is a simple bool to indicate whether an animation can be interrupted. Instead of having your scripts call AnimancerComponent.Play directly, make your own method for them to call which handles the interruption check:

private bool _CanBeInterrupted;

public AnimancerState Play(AnimationClip clip, bool canBeInterrupted)
{
    if (_CanBeInterrupted)
        ForcePlay(clip, canBeInterrupted);
}

// You may want to make this private, but it depends on your use case.
public AnimancerState ForcePlay(AnimationClip clip, bool canBeInterrupted)
{
    _CanBeInterrupted = canBeInterrupted;
    return _Animancer.Play(clip);
}

That way you can allow an Idle or Walk animation to be interrupted by anything while an Attack must play through to the end.

Priority Enum

A simple bool is not enough in a more complex system though. For example, getting hit while performing an Attack might make a character Flinch or even Die, so you would need a way to allow some interruptions while preventing others. This example only uses 3 priority levels, but you could add as many more as you need:

public enum Priority
{
    Low,// Could specify "Low = 0," if we want to be explicit.
    Medium,// Medium = 1,
    High,// High = 2,
}

Since we have defined the values in order from lowest to highest, we can make use of the fact that enums are essentially just ints under the hood to check which priority is higher:

private Priority _Priority;

public AnimancerState Play(AnimationClip clip, Priority priority)
{
    if (_Priority <= priority)
        ForcePlay(clip, priority);
}

public AnimancerState ForcePlay(AnimationClip clip, Priority priority)
{
    _Priority = priority;
    return _Animancer.Play(clip);
}

States

If you are using a Finite State Machine system (as we are in this example), you could simply give each state a Priority:

[SerializeField] private Priority _Priority;

Then compare those values when attempting to exit the state:

public override bool CanExitState(CreatureState nextState)
{
    return nextState._Priority >= _Priority;
}

In the Creatures example, we set up the ForceIdleState Delegate to use ForceSetState instead of TrySetState so that it will ignore the priority. When attempting to perform an action you want to make sure both the current state and target state will allow it, but when an action's animation ends you generally just want to return to Idle which is unlikely to have any additional requirements. Otherwise the state would need to both try to return to idle at the right time and have its CanExitState check the animation time as well to make sure the animation is finished (and ignore the priority if so).

There are two scripts in this example:

This Creature is identical to the one from the Creatures example (but it needs to be a different script because it references the CreatureState class below instead of Creatures.CreatureState):

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

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

    public Action ForceIdleState { get; private set; }

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

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

    public void TrySetState(CreatureState state)
    {
        StateMachine.TrySetState(state);
    }
}

This CreatureState is also practically identical to the one from the Creatures example, but with the new Priority enum and CanExitState method:

using Animancer;
using Animancer.FSM;
using System;
using UnityEngine;

public sealed class CreatureState : StateBehaviour<CreatureState>
{
    public enum Priority
    {
        Low,
        Medium,
        High,
    }

    [SerializeField] private Creature _Creature;
    [SerializeField] private AnimationClip _Animation;
    [SerializeField] private Priority _Priority;

    private void OnEnable()
    {
        var state = _Creature.Animancer.Play(_Animation, 0.25f);
        if (!_Animation.isLooping)
            state.OnEnd = _Creature.ForceIdleState;
    }

    public override bool CanExitState(CreatureState nextState)
    {
        return nextState._Priority >= _Priority;
    }
}

The similarities between those scripts and the ones from the Creatures example are so significant that we could have almost just implemented CreatureState to inherit from the Creatures.CreatureState one without duplicating all the code (and without needing a new Creature script).

Unfortunately since Creatures.CreatureState inherits from StateBehaviour<Creatures.CreatureState>, the CanExitState method would need to take a Creatures.CreatureState parameter instead of an InterruptManagement.CreatureState, meaning it would not be able to cleanly access the _Priority of the other state.

Admittedly this could have been solved using a simple Type-Cast, but we just made them separate scripts in the interest of keeping the example clear about what it is demonstrating, i.e. making simple states with a Priority rather than making a class hierarchy that would not resemble the structure of a real game.

Here is what the script could have looked like if we used inheritance (which would also require Creatures.CreatureState to not be sealed):

using Animancer;
using Animancer.FSM;
using System;
using UnityEngine;

public sealed class CreatureState : Creatures.CreatureState
{
    public enum Priority
    {
        Low,
        Medium,
        High,
    }

    [SerializeField] private Priority _Priority;

    public override bool CanExitState(Creatures.CreatureState nextState)
    {
        return ((CreatureState)nextState)._Priority >= _Priority;
    }
}

Using those scripts with the same setup as the Creatures example allows us to assign a priority to each state:

  • Idle, Walk, and Run are Low priority.
  • Swing is Medium priority.
  • Flinch is High priority.

This means the character can Swing at any time while they are moving around, but if they Flinch because an enemy hits them with an attack they won't be able to start another action until it finishes.

Inheritance Based Priority

Instead of giving each state a serialized Priority field which needs to be set in the Inspector every time you add it to an object, you could give the base state class a virtual or abstract property that can be overridden by the actual state classes that Inherit from it:

// In the base State class.
// Everything is low priority by default.
public virtual Priority Priority { get { return Priority.Low; } }

// In the Attack class.
// All attacks are medium priority.
public override Priority Priority { get { return Priority.Medium; } }

// In the Flinch class.
// Flinching when you get hit is high priority to interrupt everything.
public override Priority Priority { get { return Priority.High; } }

The serialized field approach can be more flexible because some scripts might want a different priority to other instances of the same script, but the inheritance approach is often simpler to use because you do not have to set the priority in the Inspector for every state you create.

Action Types

Using a Priority enum allows you to manage interruptions in a standardised manner without anything caring about the specific states involved, however it can often be more straightforward to use an ActionType enum such as:

public enum CreatureActionType
{
    Idle,
    Walk,
    Run,
    Jump,
    Attack,
    Skill,
    Flinch,
    Die,
}

Then you can implement the CanExitState method to allow and restrict specific categories as necessary. For example, for an AttackState it might look like this:

public override bool CanExitState(CreatureState nextState)
{
    switch (nextState.ActionType)
    {
        case ActionType.Flinch:
        case ActionType.Die:
            return true;

        default:
            return false;
    }
}