While programming your in-game entities, there are cases where they need to behave differently under different conditions, which suggests an idea of a state.
But if you choose to employ a brute-force method, the code will quickly become an untangled mess with a lot of if-else conditions, nested and otherwise.
To solve this problem elegantly, you can utilize the State design pattern and that’s what this tutorial is all about!
In this tutorial, you’ll learn:
- The fundamentals of the State Pattern in Unity.
- What a finite state machine is and how to use it.
- How to implement these ideas to control a character’s movement.
Getting Started
Download the project materials by clicking the Download Materials button at the top or bottom of this tutorial. Extract the zip file and open the starter project in Unity.
The project includes several folders to help you get started. Within Assets/RW you’ll find: Animations, Materials, Models, Prefabs, Resources, Scenes, Scripts, and Sounds, each named according to the kind of assets they contain.
You’ll only work with Scenes and Scripts to complete this tutorial.
Go to RW/Scenes and open Main. In the Game view, you’ll see a hooded character in a medieval castle grounds.
Click Play and notice the Camera moves to bring the animated Character in frame. Right now, there are no interactions inside this little game. But that’s exactly what you’ll work on in this tutorial.
Exploring the Character
In the Hierarchy, select Character. Next, take a look at the Inspector. You’ll see a similarly named Component that contains the logic which controls the Character.
Open Character.cs from RW/Scripts.
There’s a lot going on there but you don’t need to worry about most of it. For now, take note of the following methods.
Move
: It moves the character by accepting the float valuesspeed
as the translation speed androtationSpeed
as the angular speed.ResetMoveParams
: This method resets the parameters used for movement animation and the angular velocity of the character. This is simply used for clean-up.SetAnimationBool
: This sets a Bool type animation parameterparam
tovalue
.CheckCollisionOverlap
: It accepts apoint
of typeVector3
and returns abool
indicating if there are colliders around thepoint
within a defined radius.TriggerAnimation
: It triggers the input animation parameterparam
.ApplyImpulse
: This applies an impulse force on the Character equal to theVector3
input parameterforce
.
You’ll use these methods in the following sections. For the purposes of this tutorial, you don’t need to worry about their inner workings.
Understanding State Machines
State machines are a concept in which a container stores the status of something at any given time. Then, given an input, it can provide an output based on the current state, transitioning to a new state in the process. State machines can be represented by a State Diagram. Preparing a state diagram will help you think about all the possible states of your system, and how you transition between them.
Finite State Machines
Finite state machines or FSMs are one of the four major families of automaton. Automatons are abstract models of simple machines. They’re studied under Automata Theory, a theoretical branch of computer science.
Simply put:
- An FSM consists of a finite number of states. At a given time only one such state is active.
- Each state defines which state it’ll transition to as an output, based on the sequence of inputs it receives.
- The output state becomes the new active state. In other words, a state transition occurs.
To understand this better, consider a character in a platformer game who’s standing on the ground. The character is in the Standing state. This would be its active state until you press a button to make the character jump.
The Standing state identifies the button press as a valid input and transitions to the Jumping state as an output.
Assume there are a fixed number of such movement states and the character can only be in one such state at a time. This is an example of an FSM.
Hierarchical State Machines
Consider a platformer game using FSM, in which several states share the same physics logic. For example, you can move and jump in both the Crouching and Standing states. In this case, several inputs result in the same behavior and output for two different states.
In a situation like this, it makes sense to have some kind of delegation to some other state for the shared or common behavior. Luckily, you can achieve this via hierarchical state machines.
In a hierarchical FSM, there are sub-states which delegate the unhandled inputs to their super-state. This in-turn elegantly reduces the size and complexity of the FSM while maintaining the logic.
The State Pattern
In their book, Design Patterns: Elements of Reusable Object-Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides — the Gang of Four, define the intent for the State pattern as follows:
“Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.”
To understand this better, consider the following example:
- A script that accepts input for movement logic is attached to an in-game entity.
- This class maintains a current state variable which simply references an instance of a state class.
- The input delegates to this current state which handles it and produces the behavior defined within itself. It also handles the required state transitions.
Thus, due to the different states referenced by the current state variable over time, the same script class will seem to behave differently. This is the essence of the State pattern.
In this project, the aforementioned Character class is the one that will behave differently due to the various states. But, you want good behavior only! :]
In general, there are three key points for every state class which help define the overall behavior of the state:
- Entry: This is where you enter the state and do things you only need to do once when you first enter the state.
- Exit: Similar to the Entry, this is where you do any clean-ups you only need to do once before the state changes.
- Update Loop: This is where the core update logic, which executes in every frame, exists. You can divide this into multiple parts such as a loop for physics updates or a loop for handling input.
Defining State and State Machine
Navigate to RW/Scripts and open StateMachine.cs.
The State Machine will provide an abstraction for, you guessed it, a state machine. Notice the CurrentState
property inside this class. This will store the reference to the current active state of the state machine.
Now to define the idea of a state, go to RW/Scripts and open State.cs in your IDE.
State is an abstract class you’ll use as a blueprint to derive from, for all the state classes in this project. Some code has already been provided for you.
DisplayOnUI
only displays the name of the current state on the screen UI. You don’t need to worry about its inner workings but you do need to understand it takes an enumerator of type UIManager.Alignment
as an input parameter which can either be Left
or Right
. This results in the name of the state displaying on either the bottom left or bottom right of the screen respectively.
Additionally, there are two protected variables, character
and stateMachine
. The character
references an instance of the Character class while stateMachine
references an instance of the State Machine that associates with the state.
The constructor links the character
and stateMachine
when a state instance is created.
Multiple instances of Character
in the scene can each have their own set of states and state machines.
Now, add the following methods inside of State.cs and save the file:
public virtual void Enter()
{
DisplayOnUI(UIManager.Alignment.Left);
}
public virtual void HandleInput()
{
}
public virtual void LogicUpdate()
{
}
public virtual void PhysicsUpdate()
{
}
public virtual void Exit()
{
}
These virtual methods define the key points of a state discussed earlier. When the State Machine transitions between states, you call Exit
on the previous state and Enter
for the new active state.
The HandleInput
, LogicUpdate
and PhysicsUpdate
together define the update loop. HandleInput
handles the input. LogicUpdate
handles the core logic and the PhyiscsUpdate
handles the physics logic and calculations.
Now open StateMachine.cs once again, add the following methods and save the file:
public void Initialize(State startingState)
{
CurrentState = startingState;
startingState.Enter();
}
public void ChangeState(State newState)
{
CurrentState.Exit();
CurrentState = newState;
newState.Enter();
}
Initialize
configures the state machine by setting CurrentState
to the startingState
variable and calling Enter
on it. This initializes the state machine by setting the active state for the first time.
ChangeState
handles transitions between States. It calls Exit
on the old CurrentState
before updating its reference to newState
. Finally it calls Enter
on the newState
.
With that, you’ve defined a State and a State Machine.
Creating the Movement States
Take a look at the following state diagram which shows the various movement states of an in-game player entity. In this section, you’ll implement the state pattern for the shown movement FSM:
Take note of the various movement states, namely Standing, Ducking, and Jumping, and how the input can cause a state transition. This is a hierarchical FSM where Grounded is the super-state for the Ducking and Standing sub-states.
Go back to Unity and navigate to RW/Scripts/States. You’ll find several C# files with names ending in State.
Each of these files defines one single class, all of which inherit from State
. As such, these classes define the states you’ll use in this project.
Now open the Character.cs from RW/Scripts.
Navigate to the top of the #region Variables
of the file and add the following code:
public StateMachine movementSM;
public StandingState standing;
public DuckingState ducking;
public JumpingState jumping;
This movementSM
references the state machine that handles movement logic for the Character
instance. You also added references for three states you’ll implement for each type of movement.
Navigate to #region MonoBehaviour Callbacks
in the same file. Add the following MonoBehaviour methods and then save:
private void Start()
{
movementSM = new StateMachine();
standing = new StandingState(this, movementSM);
ducking = new DuckingState(this, movementSM);
jumping = new JumpingState(this, movementSM);
movementSM.Initialize(standing);
}
private void Update()
{
movementSM.CurrentState.HandleInput();
movementSM.CurrentState.LogicUpdate();
}
private void FixedUpdate()
{
movementSM.CurrentState.PhysicsUpdate();
}
- In
Start
, the code creates an instance of State Machine and assigns it tomovementSM
as well as creating the instances of the various movement states. As you create each of the movement states, you pass references to theCharacter
instance using thethis
keyword as well asmovementSM
instance. Finally, you callInitialize
onmovementSM
and pass inStanding
as the starting state. - In the
Update
method, you callHandleInput
andLogicUpdate
on theCurrentState
ofmovementSM
. Similarly, inFixedUpdate
, you callPhysicsUpdate
on theCurrentState
ofmovementSM
. This essentially delegates the tasks to the active state, which is what the State pattern is all about.
Now you need to define the behavior inside each of the movement states. Brace yourself for lots of code!
Two Feet Firmly on the Ground
Go back to RW/Scripts/States in the Project window.
Open Grounded.cs and note that this class has a constructor that matches that of the State
. This makes sense since this class is inhering from it. You’ll see the same in all the other state classes as well.
Add the following code:
public override void Enter()
{
base.Enter();
horizontalInput = verticalInput = 0.0f;
}
public override void Exit()
{
base.Exit();
character.ResetMoveParams();
}
public override void HandleInput()
{
base.HandleInput();
verticalInput = Input.GetAxis("Vertical");
horizontalInput = Input.GetAxis("Horizontal");
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
character.Move(verticalInput * speed, horizontalInput * rotationSpeed);
}
Here’s what is happening:
- You override some of the virtual methods defined in the parent class. In order to keep any functionality that could exist in the parent you call the
base
method of the same name from within each overridden method. This is an important pattern you’ll continue to use. - In the next line of
Enter
, thehorizontalInput
andverticalInput
variables are set to their default values. - Inside
Exit
, you callResetMoveParams
method ofcharacter
for clean-up while transitioning to another state, as described before. - In
HandleInput
method,horizontalInput
andverticalInput
cache the horizontal and vertical input axis values. As such, you can use the W, A, S and D keyboard keys to move the character. - In the
PhysicsUpdate
, you make a call toMove
by passing in thehorizontalInput
andverticalInput
variables multiplied by the respective speeds. Thespeed
variable stores the translation speed whereasrotationSpeed
stores the angular speed.
Now open Standing.cs and note that it inherits from Grounded
. This is because Standing is a sub-state for Grounded, as discussed previously. There are various ways of implementing this relationship but in this tutorial you’ll use inheritance.
Add the following override
methods and save the script:
public override void Enter()
{
base.Enter();
speed = character.MovementSpeed;
rotationSpeed = character.RotationSpeed;
crouch = false;
jump = false;
}
public override void HandleInput()
{
base.HandleInput();
crouch = Input.GetButtonDown("Fire3");
jump = Input.GetButtonDown("Jump");
}
public override void LogicUpdate()
{
base.LogicUpdate();
if (crouch)
{
stateMachine.ChangeState(character.ducking);
}
else if (jump)
{
stateMachine.ChangeState(character.jumping);
}
}
- In
Enter
you configure some variables inherited fromGrounded
. You apply the Character’sMovementSpeed
andRotationSpeed
to thespeed
androtationSpeed
. They apply to the normal translation and angular speeds intended for the character entity respectively.Also, the variables for storing input,crouch
andjump
, are reset to false. - Inside
HandleInput
,crouch
andjump
store the user input for crouching and jumping respectively. In the Main scene, if the user presses the Shift key, crouch is set to true. Similarly, the user can use the Space key forjump
. - In
LogicUpdate
you check thebool
variablescrouch
andjump
. Ifcrouch
is true,movementSM.CurrentState
changes tocharacter.ducking
. Otherwise, ifjump
is true, it changes tocharacter.jumping
.
Save and build the project and click Play. You should be able to move around using the W, A, S and D keys. If you try pressing the Shift or Space keys right now, unexpected behavior will occur since the corresponding states aren’t implemented yet.
Try moving under the bench shaped platforms. You’ll notice that isn’t possible due to the Character’s collider height. To let the Character do this, you’ll add the ducking behavior.
Sneaking Under the Table
Open Ducking.cs. Notice Ducking
also inherits from the Grounded
class for the same reasons Standing
does. Add the following override
methods and save the script:
public override void Enter()
{
base.Enter();
character.SetAnimationBool(character.crouchParam, true);
speed = character.CrouchSpeed;
rotationSpeed = character.CrouchRotationSpeed;
character.ColliderSize = character.CrouchColliderHeight;
belowCeiling = false;
}
public override void Exit()
{
base.Exit();
character.SetAnimationBool(character.crouchParam, false);
character.ColliderSize = character.NormalColliderHeight;
}
public override void HandleInput()
{
base.HandleInput();
crouchHeld = Input.GetButton("Fire3");
}
public override void LogicUpdate()
{
base.LogicUpdate();
if (!(crouchHeld || belowCeiling))
{
stateMachine.ChangeState(character.standing);
}
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
belowCeiling = character.CheckCollisionOverlap(character.transform.position +
Vector3.up * character.NormalColliderHeight);
}
- Inside
Enter
, the parameter to trigger the crouch animation is set to true which enables the crouch animation. Thecharacter.CrouchSpeed
andcharacter.CrouchRotationSpeed
properties set thespeed
androtation
which return the intended translation and angular speed of the character while crouching.
Next,character.CrouchColliderHeight
sets the collider size of the character, which returns the intended collider height while crouching. Finally,belowCeiling
is reset to false. - Inside
Exit
, the parameter for crouch animation is set to false. This would disable the crouch animation. Afterwards the collider height is set to normal intended height returned bycharacter.NormalColliderHeight
. - Inside
HandleInput
,crouchHeld
is set to accept the user input. In the Main scene, holding down Shift will setcrouchHeld
to true. - Inside
PhysicsUpdate
, thebelowCeiling
variable is set by passing in theVector3
point near the Character GameObject’s head toCheckCollisionOverlap
by. If there is a collision around that point, it means the Character is below a ceiling of some sort. - Inside
LogicUpdate
, you check to see if eithercrouchHeld
orbelowCeiling
is true. If neither of them is true,movementSM.CurrentState
changes tocharacter.standing
.
Build and click Play. Now you should be able to move around. If you press Shift, the Character will crouch and you can move around while crouching.
You should also be able to go under the platforms. If you release Shift while underneath the platforms, the Character will continue crouching until you leave its cover.
Up, Up and Away!
Open Jumping.cs. You’ll see a method named Jump
. Don’t worry about its specifics but understand it’s used to make the Character jump with physics, animation and magic! :]
Now add the usual override
methods and save the script
public override void Enter()
{
base.Enter();
SoundManager.Instance.PlaySound(SoundManager.Instance.jumpSounds);
grounded = false;
Jump();
}
public override void LogicUpdate()
{
base.LogicUpdate();
if (grounded)
{
character.TriggerAnimation(landParam);
SoundManager.Instance.PlaySound(SoundManager.Instance.landing);
stateMachine.ChangeState(character.standing);
}
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
grounded = character.CheckCollisionOverlap(character.transform.position);
}
- Inside
Enter
, theSoundManager
singleton plays the jump sound. Afterwards,grounded
is reset to its default value. FinallyJump
gets called. - Inside
PhysicsUpdate
, theVector3
point near the Character’s feet is sent toCheckCollisionOverlap
, which meansgrounded
will be set to true if the Character is on ground. - In
LogicUpdate
whengrounded
is true, you callTriggerAnimation
for the landing animation, a landing audio plays andmovementSM.CurrentState
changes tocharacter.standing
.
Finally, with this, you’ve fully implemented the movement FSM using the State Pattern. Build and play. Use Space to make the character jump.
Now you can say, “my code is in a state!” with a smile on your face! :]
Where to Go From Here?
You can use the Download Materials button at the top and bottom of this tutorial to download both the starter and final projects.
While state machines are useful they come with their own limitations. Concurrent State Machines and Pushdown Automata tackle some of those limitations. You can read more about them in the book Game Programming Patterns by R.Nystrom.
You can also delve deeper by exploring behavioral trees for making advanced in-game entities.
I hope you found this tutorial useful and enjoyed learning the topic. Please feel free to join the forum below for any questions or comments.
More info: https://www.raywenderlich.com/6034380-state-pattern-using-unity
Leave a Reply