Location: Samples/06 State Machines/02 Interruptions
Recommended After: Characters and Doors
Learning Outcomes: in this sample 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.
Summary
This sample expands upon the Characters sample to demonstrate how you can control which states are allowed interrupt each other. 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.
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.
This sample 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 sample 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 sample 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 sample just reuses the interaction system from the Doors sample:
- 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 sample 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.
Note how nothing directly references the FlinchState
. It Inherits a Character
reference from the base CharacterState
and registers itself to the HealthPool.OnHitReceived
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 sample, but will be used here:
public class Character : MonoBehaviour
{
...
[SerializeField]
private HealthPool _Health;
public HealthPool Health => _Health;
The HealthPool
class is very simple:
public class HealthPool : MonoBehaviour, IInteractable
{
public event Action OnHitReceived;
public void Interact()
{
OnHitReceived?.Invoke();
}
}
- 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 sample 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.
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 with a Transition Asset that it plays in OnEnable
and an End Event to return to the default state (Idle) when the animation finishes.
public class FlinchState : CharacterState
{
[SerializeField] private TransitionAsset _Animation;
protected virtual void OnEnable()
{
AnimancerState state = Character.Animancer.Play(_Animation);
state.Events(this).OnEnd ??= Character.StateMachine.ForceSetDefaultState;
}
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 its Awake
method which registers a Lambda Expression 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.
protected virtual 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 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
:
protected virtual void OnEnable()
{
AnimancerState state = Character.Animancer.Play(_Animation);
state.Events(this).OnEnd ??= Character.StateMachine.ForceSetDefaultState;
}
Unfortunately, that approach is too simple for this sample 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:
//CharacterState nextState = StateChange<CharacterState>.NextState;
//CharacterState nextState = _Character.StateMachine.NextState;
CharacterState nextState = this.GetNextState();
return nextState.Priority >= Priority;
}
}
}
public class ActionState : CharacterState
{
public override int Priority => 1;
}
public 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 class ActionState : CharacterState
{
public override CharacterStatePriority Priority => CharacterStatePriority.Medium;
}
public 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 sample, 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 sample) 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
:
CharacterState 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 sample scripts:
public abstract class CharacterState : StateBehaviour
{
public virtual CharacterStatePriority Priority => CharacterStatePriority.Low;
public virtual bool CanInterruptSelf => false;
}
public class ActionState : CharacterState
{
public override CharacterStatePriority Priority => CharacterStatePriority.Medium;
public override bool CanInterruptSelf => true;
}
public class FlinchState : CharacterState
{
public override CharacterStatePriority Priority => CharacterStatePriority.High;
public override bool CanInterruptSelf => true;
}
public class AttackState : CharacterState// From the Weapons sample.
{
public override CharacterStatePriority Priority => CharacterStatePriority.Medium;
// CanInterruptSelf defaults to false from the base class.
}
public class EquipState : CharacterState// From the Weapons sample.
{
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 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 sample, 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 sample 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?
Sample | 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 sample, 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. |