03 Interrupt Management

Difficulty: Intermediate - Recommended after Creatures

Location: Assets/Plugins/Animancer/Examples/06 State Machines/03 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
    => StateChange<CreatureState>.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).

This example reuses the Creature script from the Creatures example:

This example reuses the Creature script from the Creatures example and uses a new CreatureState class which inherits from the class with the same name from the Creatures example to use the same base functionality with the new Priority enum and CanExitState property:

using Animancer.FSM;
using UnityEngine;

// This class is in the Animancer.Examples.StateMachines.InterruptManagement namespace.
// It inherits from the Animancer.Examples.StateMachines.Creatures.CreatureState class.
// THey can both have the same name because they are in different namespaces.
public sealed class CreatureState : Creatures.CreatureState
{
    public enum Priority
    {
        Low,// Could specify "Low = 0," if we want to be explicit.
        Medium,// Medium = 1,
        High,// High = 2,
    }

    [SerializeField] private Priority _Priority;

    public override bool CanExitState
    {
        get
        {
            var nextState = (CreatureState)StateChange<Creatures.CreatureState>.NextState;
            return nextState._Priority >= _Priority;
        }
    }
}

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

  • 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 => Priority.Low;

// In the Attack class.
// All attacks are medium priority.
public override Priority Priority => Priority.Medium;

// In the Flinch class.
// Flinching when you get hit is high priority to interrupt everything.
public override Priority Priority => 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 property to allow and restrict specific categories as necessary. For example, for an AttackState it might look like this:

public override bool CanExitState
{
    get
    {
        switch (StateChange<CreatureState>.NextState.ActionType)
        {
            case ActionType.Flinch:
            case ActionType.Die:
                return true;
        
            default:
                return false;
        }
    }
}