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.
TStaterefers 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 anIdleStatecallsthis.GetPreviousState()the compiler will infer the generic type asIdleStateand 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
Jumpif you are not on the ground soJump.CanEnterStatechecks whether the character is on the ground (or asks whatever system is responsible for checking that). - You cannot perform other actions during an
AttacksoAttack.CanExitStatereturns false. Attacks can be interrupted if you get hit or killed though, so instead of always returning false,Attack.CanExitStatereturns true if theStateChange<TState>.NextStateisFlinchorDie. This is often implemented by using anenumto indicate the priority of each state or type of each state so you might have multiple differentAttackscripts (perhaps for melee vs. ranged or to implement attack combos for some characters but not others) yet they all use the sameCharacterAction.Attackas their type. The Interruptions example goes into more detail about this.- Once the
Attackanimation finishes, it needs to be able to return toIdle. This could be done by havingAttack.CanExitStatecheck the current animation time as well, but since theAttackstate already knows it is ending andIdleis unlikely to impose any constraints on when it can be entered you could simply useForceSetStateto 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
IStateinterface 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
CanEnterStatewill sometimes base its decision on the previous state, it's much less likely to be relevant inOnEnterStatebut 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
IStateinterface (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 |
|---|---|
|
|
|
|
|
|
|
|