Once you have Initialized your StateMachine
, you can start telling it to change its state. There are two Try...
methods which respect the CanEnterState
and CanExitState
rules defined by the states and a Force...
method which ignores them.
Method | Description |
---|---|
TrySetState |
Tries to enter a new state. If the target state is already theCurrentState , this method returns true without doing anything. Otherwise it calls TryResetState . |
TryResetState |
Tries to enter a new state or re-enter the current state.
|
ForceSetState |
Enters a state without checking
|
Details
While a StateMachine
is changing its state (throughout all of the Can...
and On...
methods in the IState
interface), references to the PreviousState
and NextState
can be accessed in several ways.
The following examples are based on the Priority Enum demonstrated in the Interruptions example.
TState
refers to the base State Type you are using.- Accessing the previous or next state will throw an exception if no state change is currently occurring (which can be checked using
StateChange<TState>.IsActive
).
State Change
StateChange<TState>.PreviousState
StateChange<TState>.NextState
The static StateChange
properties can be accessed from anywhere:
public override bool CanExitState
=> StateChange<TState>.NextState.Priority > Priority;
State Machine
StateMachine.PreviousState
StateMachine.NextState
If you have a reference to the StateMachine
you can use its properties to avoid needing to specify the TState
generic argument:
public override bool CanExitState
=> StateMachine.NextState.Priority > Priority;
Extension Methods
this.GetPreviousState<TState>()
this.GetNextState<TState>()
Inside a class that implements IState
(or if you have a reference to one) you can use those Extension Methods which are defined in the StateExtensions
class:
public override bool CanExitState
=> this.GetNextState<TState>().Priority > Priority;
Using those Extension Methods without specifying the generic TState
parameter relies on the compiler to infer the parameter which can sometimes lead to it accessing the wrong type and therefore not working properly.
- For example, if you have a
StateMachine<CharacterState>
then you need to checkStateChange<CharacterState>
, but if anIdleState
callsthis.GetPreviousState()
the compiler will infer the generic type asIdleState
and try to accessStateChange<IdleState>
which would be empty and cause it to throw an exception:InvalidOperationException: Attempted to access StateChange<IdleState> but no StateMachine of that type is currently changing its State
.
- This can be avoided by specifying the generic parameter yourself:
this.GetPreviousState<CharacterState>()
.
Examples
- You cannot
Jump
if you are not on the ground soJump.CanEnterState
checks whether the character is on the ground (or asks whatever system is responsible for checking that). - You cannot perform other actions during an
Attack
soAttack.CanExitState
returns false. Attack
s can be interrupted if you get hit or killed though, so instead of always returning false,Attack.CanExitState
returns true if theStateChange<TState>.NextState
isFlinch
orDie
. This is often implemented by using anenum
to indicate the priority of each state or type of each state so you might have multiple differentAttack
scripts (perhaps for melee vs. ranged or to implement attack combos for some characters but not others) yet they all use the sameCharacterAction.Attack
as their type. The Interruptions example goes into more detail about this.- Once the
Attack
animation finishes, it needs to be able to return toIdle
. This could be done by havingAttack.CanExitState
check the current animation time as well, but since theAttack
state already knows it is ending andIdle
is unlikely to impose any constraints on when it can be entered you could simply useForceSetState
to skip the check.
The State Machines examples go into more detail and demonstrate these ideas in action.
Design Rationale
The state change system might seem a bit complicated compared to simply passing the change details into the IState
methods as parameters, but there are several main advantages to this design in terms of minimising Boilerplate Code and maximising the flexibility of the system:
- It allows the
IState
interface to avoid needing the state type as a generic parameter, which simplifies its usage throughout the system. - It avoids the need to declare the parameters in every state because most of the time state changes are dependent on other factors so the parameters would be unused. In particular, while
CanEnterState
will sometimes base its decision on the previous state, it's much less likely to be relevant inOnEnterState
but the parameter would still need to be included in every implementation of that method just in case it's needed. - It allows states in Keyed State Machines to access the key change as well as the state change without requiring them to be based on an entirely different
IState
interface (which would need to pass both the key and state into every method, meaning even more useless code when they aren't used).
Here's a comparison of what various implementations would look like with either approach:
The Current Implementation | If It Used Parameters |
---|---|
|
|
|
|
|
|
|
|