Location: Assets/Plugins/Animancer/Examples/06 State Machines/02 Interruptions
Recommended After: Characters and Doors
Learning Outcomes: in this example you will learn:
How a modular state machine setup makes it easy to add new states.
How to control which states can interrupt each other.
This example expands upon the Characters example to add a FlinchState
and demonstrates several different ways you can control which states can interrupt each other.
The video to the right shows how the Action (shooting the pistol) can interrupt Idle or Walk and clicking on the character to make them Flinch can interrupt any of those, but clicking elsewhere during the Flinch isn't allowed to start the Action.
Summary
Animancer will always play an animation when you tell it to, so it's up to your scripts to decide when each one can be played. This example demonstrates some of the ways you can manage that logic:
- A
bool
to indicate whether each state can be interrupted is simple, but doesn't work if you need a state to be interruptable by some things and not others. - An
int
to indicate priority is a bit more flexible, but doesn't give any clear meaning to specific values. - An
enum
can be used like anint
and gives each value a specific name (low, medium, high, etc.). - Rather than straight up priority, an
enum
could have values for general types of states such as Idle, Attack, and Die.
Controls
- W = Walk
- Left Click on the character = Get Hit
- Left Click elsewhere = Shoot
Overview
This example explains various different ways of controlling which states can interrupt each other, including the CharacterStatePriority
enum used in the actual scripts. But the states in the Characters example are a bit too simple to really explore that concept so before it can do that it explains how to implement a FlinchState
which interrupts a character's other actions. A state like that would normally be triggered when the character gets hit by an attack, but rather than getting sidetracked implementing a proper attack and damage system, this example just reuses the interaction system from the Doors example:
- On startup,
FlinchState
gets the character'sHealthPool
and registers a callback in itsOnHitReceived
event. That callback will tell the character to enter theFlinchState
. ClickToInteract
looks for a component that implementsIInteractable
attached to whatever object is clicked. If it finds one, it callsInteract
on it.- The character's
HealthPool
component implementsIInteractable
. ItsInteract
method invokes anOnHitReceived
event as if the character had taken damage.
The general code structure is the same as the Characters example with a few additions. The modular design of the character's scripts allows the new state to be added without modifying any of the previous scripts.
Click here to show the code structure from the Characters example:
Note how nothing directly references the FlinchState
. It registers itself to the event that will activate it so it's entirely self-contained and nothing else needs to know whether it's there or not. That means determining whether a character will flinch when they take damage is as simple as adding or removing that one component.
Health Pool
The Character
class has a field to reference a HealthPool
which wasn't used in the Characters example, but will be used here:
public sealed class Character : MonoBehaviour
{
...
[SerializeField]
private HealthPool _Health;
public HealthPool Health => _Health;
The HealthPool
class is very simple:
- It has an Event Delegate called
OnHitReceived
which would normally be triggered when the character is hit by an attack. - But since we don't have a hit/damage system (and implementing one would be very off-topic), it implements
IInteractable
from the Doors example using itsInteract
method to invoke the event. - There is also a
ClickToInteract
component in the scene which will call thatInteract
method when the user clicks on the character.
public sealed class HealthPool : MonoBehaviour, IInteractable
{
public event Action OnHitReceived;
public void Interact()
{
OnHitReceived?.Invoke();
}
}
Normally, that class would also have fields like maximum health and current health to keep track of how much damage the character takes.
Using events and interfaces allows this class to do its job without knowing what's interacting with it or what's reacting to it getting hit. The ClickToInteract
component wasn't written with HealthPool
in mind and FlinchState
doesn't know or care what's causing the character to get hit, they all just fit together while operating independently.
Flinch State
The FlinchState
script is very similar to the Action State:
- Has a Transition field.
- Sets its End Event return to the default state (Idle) when the animation finishes.
- Plays that Transition when the state is entered.
public sealed class FlinchState : CharacterState
{
[SerializeField] private ClipTransition _Animation;
private void Awake()
{
_Animation.Events.OnEnd = Character.StateMachine.ForceSetDefaultState;
...
}
private void OnEnable()
{
Character.Animancer.Play(_Animation);
}
The other properties determine which states it can interrupt and which ones it can be interrupted by, which will be explained soon:
public override CharacterStatePriority Priority => CharacterStatePriority.High;
public override bool CanInterruptSelf => true;
}
And the bit that allows it to operate independently is that it also registers a Lambda Expression delegate to the OnHitReceived
event in the HealthPool
component. When that event is triggered, it will invoke the registered delegate which tries to enter this state:
private void Awake()
{
...
Character.Health.OnHitReceived += () => Character.StateMachine.TryResetState(this);
}
It uses TryResetState
rather than TrySetState
so that it can re-enter itself, allowing the character to restart the Flinch animation if they get hit again while it was already playing (which might be undesirable in some games).
Interrupt Behaviour
Now that the FlinchState
has been implemented, it's time to explain some different ways of controlling which states can interrupt each other. Here's a summary of the logic we want:
State | Can be interrupted by |
---|---|
IdleState |
Any state. |
WalkState |
Any state. |
ActionState |
ActionState , FlinchState . |
FlinchState |
FlinchState . |
Can Exit State
In simple situations, you might be able to implement an interruption management system by overriding CanExitState
to directly indicate whether a state can be interrupted:
public sealed class ActionState : CharacterStae
{
public override bool CanExitState => false;
That means any attempt to TrySetState
or TryResetState
will fail, but ForceSetState
will still succeed (because it ignores that property). So the action can't be interrupted normally, but its End Event would still return to the default IdleState
properly since it uses ForceSetDefaultState
:
private void Awake()
{
_Animation.Events.OnEnd = Character.StateMachine.ForceSetDefaultState;
}
Unfortunately, that approach is too simple for this example because we want ActionState
to allow interruptions by FlinchState
without allowing interruptions by IdleState
.
Priority Int
You could use an int
where higher values allow a state to interrupt ones with lower values:
public abstract class CharacterState : StateBehaviour
{
public virtual int Priority => 0;// All states default to 0 unless they override it.
public override bool CanExitState
{
get
{
// There are several different ways of accessing the state change details:
//var nextState = StateChange<CharacterState>.NextState;
//var nextState = _Character.StateMachine.NextState;
var nextState = this.GetNextState();
return nextState.Priority >= Priority;
}
}
}
public sealed class ActionState : CharacterState
{
public override int Priority => 1;
}
public sealed class FlinchState : CharacterState
{
public override int Priority => 2;
}
The Changing States page explains the different ways of accessing the next state in more detail.
Priority Enum
An int
can hold a wide range of values and a character won't have that many different states so it might be cleaner to use an Enum like this:
public enum CharacterStatePriority
{
// Enums are ints starting at 0 by default.
Low,// Could specify "Low = 0," if we want to be explicit or change the order.
Medium,// Medium = 1,
High,// High = 2,
// This means you can compare them with numerical operators like < and >.
}
The CanExitState
property would be completely identical to the Priority Int approach above, while the Priority
property uses the enum
:
public abstract class CharacterState : StateBehaviour
{
public virtual CharacterStatePriority Priority => CharacterStatePriority.Low;
}
public sealed class ActionState : CharacterState
{
public override CharacterStatePriority Priority => CharacterStatePriority.Medium;
}
public sealed class FlinchState : CharacterState
{
public override CharacterStatePriority Priority => CharacterStatePriority.High;
}
Self Interruption
The Priority Int and Priority Enum approaches above allow a state change if nextState.Priority >= previousState.Priority
which would achieve the desired behaviour for this example, but only because the ActionState
and FlinchState
both want to be able to interrupt themselves. It's far more common for actions to not be able to do that, but if we simply changed the >=
to >
then the IdleState
s (and the MoveState
which will be introduced in the Brains example) wouldn't be able to interrupt each other.
A simple solution is to add a CanInterruptSelf
property and make CanExitState
a bit more complicated:
public virtual CharacterStatePriority Priority => CharacterStatePriority.Low;
public virtual bool CanInterruptSelf => false;
public override bool CanExitState
{
get
{
If the next state is actually this
state, use the value of CanInterruptSelf
:
var nextState = _Character.StateMachine.NextState;
if (nextState == this)
return CanInterruptSelf;
All the Low
priority states should be interruptable by anything:
else if (Priority == CharacterStatePriority.Low)
return true;
Otherwise, compare the Priority
of each state and don't allow interruptions by the same Priority
:
else
return nextState.Priority > Priority;
}
}
This is the solution used in the actual example scripts:
public abstract class CharacterState : StateBehaviour
{
public virtual CharacterStatePriority Priority => CharacterStatePriority.Low;
public virtual bool CanInterruptSelf => false;
}
public sealed class ActionState : CharacterState
{
public override CharacterStatePriority Priority => CharacterStatePriority.Medium;
public override bool CanInterruptSelf => true;
}
public sealed class FlinchState : CharacterState
{
public override CharacterStatePriority Priority => CharacterStatePriority.High;
public override bool CanInterruptSelf => true;
}
public sealed class AttackState : CharacterState// From the Weapons example.
{
public override CharacterStatePriority Priority => CharacterStatePriority.Medium;
// CanInterruptSelf defaults to false from the base class.
}
public sealed class EquipState : CharacterState// From the Weapons example.
{
public override CharacterStatePriority Priority => CharacterStatePriority.Medium;
// CanInterruptSelf defaults to false from the base class.
}
Action Type
A linear priority based system 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 a CharacterStateType
enum such as:
public enum CharacterStateType
{
Idle,
Walk,
Run,
Jump,
Action,
Skill,
Flinch,
Die,
}
Then you can override
the CanExitState
property in any state to allow and restrict specific categories as necessary. For example:
public sealed class ActionState : CharacterState
{
public override bool CanExitState
{
get
{
switch (this.GetNextState<CharacterState>().Type)
{
case CharacterStateType.Flinch:
case CharacterStateType.Die:
return true;
default:// Any other value.
return false;
}
}
}
}
For this example, case CharacterStateType.Action
would also need to return true
to allow rapid firing while most actions usually can't interrupt themselves so the specific logic you need will always depend on the situation.
Serialized Field
Instead of using virtual
properties, the above approaches could have used Serialized Fields:
public abstract class CharacterState : StateBehaviour
{
[SerializeField]
private CharacterStatePriority _Priority;
This would mean that you need to select the desired value in the Inspector every time you set up a state component, but would allow you to potentially reuse a state with a different priority. For example, you might want an ActionState
that operates the same as usual but can't be interrupted even by FlinchState
so you could just give it a higher priority.
Conclusion
The actual example scripts use the Priority Enum approach with the Self Interruption system since it's just complex enough to achieve the desired logic without any unnecessary complexity beyond that. Keeping your code open to future expansions and changes is valuable, but it can also be useful to choose a solution that closely meets the requirements of your situation without wasting time and effort supporting potential features that you don't end up needing.
What Next?
Example | Topic |
---|---|
Brains | A deeper look into the concept of character brains. |
Platformer Game Kit | A separate (free) package which demonstrates a much more complete character implementation for a 2D platformer game. Despite having a much more complex character than this example, it's able to achieve the desired logic by simply overriding Can Exit State without needing any of the more complex options presented here. |
3D Game Kit | A more complex character framework based on Unity's 3D Game Kit Lite. |