State Pattern for Character Movement in Phaser 3

Encapsulate state logic to create complex character actions with clean code

by on 7 minute read


Writing clean and well-organized code is something all game developers aspire to.

We want code to be reusable and easy to maintain. That way we can spend the limited time we have on exciting gameplay features instead of drowning in bugs created by fragile code.

Design patterns help us achieve this goal by standardizing how a game is structured so that we can more easily find our way around projects and avoid common pitfalls that lead to unstable games.

In this article, we will look at using the State Pattern to create a reusable and scalable system for controlling a character in Phaser 3.

What about a State Machine?

A Finite State Machine, often simply called a State Machine, is one advanced application of the State Pattern.

We are not going to create a full-blown State Machine in this article. Instead, we'll show how the State Pattern can be used to create a generic State Machine.

This example will be building on top of the Memory Match tutorial series.

Memory Match is a Mario Party-inspired memory game where you control a character to move around and pick boxes to find all matches.

Get the source code from the tutorial here to more easily follow along.

Creating a State

The character in Memory Match can move up, down, left, and right. Each of these can be considered a movement state.

Here's what the player movement in Memory Match looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
if (this.cursors.left.isDown)
{
	this.player.setVelocity(-speed, 0)
	this.player.play('left-walk', true)
}
else if (this.cursors.right.isDown)
{
	this.player.setVelocity(speed, 0)
	this.player.play('right-walk', true)
}
else if (this.cursors.up.isDown)
{
	this.player.setVelocity(0, -speed)
	this.player.play('up-walk', true)
}
else if (this.cursors.down.isDown)
{
	this.player.setVelocity(0, speed)
	this.player.play('down-walk', true)
}

We set the player's velocity and animation depending on which state of movement the player is in.

A separate State class can be created from each direction of movement so that when we are moving left we can be in the MoveLeftState and when we are moving right we can be in the MoveRightState.

Create a MoveLeftState.js file in a states folder with the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export default class MoveLeftState
{
	/** @type {Phaser.Physics.Arcade.Sprite} */
	player

	/**
	 * @param {Phaser.Physics.Arcade.Sprite} player 
	 */
	constructor(player)
	{
		this.player = player
	}

	enter()
	{
		this.player.play('left-walk')

		const speed = 200
		this.player.setVelocity(-speed, 0)
	}
}

This State class takes in an instance of the player in the constructor. Then it has an enter() method that will be called each time the State is first entered into.

It is important that enter() is only called once each time the State starts. This lets us omit the second argument to this.player.play(). Previously, We passed in true to tell Phaser to ignore if the animation is already playing.

The other 3 movement states will look similar to MoveLeftState. Here's an example of the MoveDownState:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export default class MoveDownState
{
	/** @type {Phaser.Physics.Arcade.Sprite} */
	player

	/**
	 * @param {Phaser.Physics.Arcade.Sprite} player 
	 */
	constructor(player)
	{
		this.player = player
	}

	enter()
	{
		this.player.play('down-walk')

		const speed = 200
		this.player.setVelocity(0, speed)
	}
}

We think you can figure out what MoveRightState and MoveUpState should look like. 😎

Idle State

Along with the 4 movement states, we also have an idle state where the player is just standing still.

It should look similar to the movement states but we'll include it here just in case:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default class IdleState
{
	/** @type {Phaser.Physics.Arcade.Sprite} */
	player

	/**
	 * @param {Phaser.Physics.Arcade.Sprite} player 
	 */
	constructor(player)
	{
		this.player = player
	}

	enter()
	{
		this.player.setVelocity(0, 0)
		const key = this.player.anims.currentAnim.key
		const parts = key.split('-')
		const direction = parts[0]
		this.player.play(`${direction}-idle`)
	}
}

Take a minute to compare these State classes with the Memory Match code we had in the updatePlayer() method.

You'll see that we've basically encapsulated each movement logic into a separate class.

Now, we need a class that controls the switching of states.

Controlling Player States

Let's make a class called PlayerController with this set up:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export default class PlayerController
{
	/** @type {{ [key: string]: { enter: () => void } }} */
	states

	/** @type {{ enter: () => void }} */
	currentState

	/**
	 * @param {Phaser.Physics.Arcade.Sprite} player 
	 */
	constructor(player)
	{
		this.states = {
			// TODO: add states
		}
	}

	/**
	 * 
	 * @param {string} name 
	 */
	setState(name)
	{
		// TODO: switch from the previous state
	}
}

We have a states property that will hold instances for each of the 5 states we previously created. Then the currentState property will hold a reference to the currently active state.

It will be used in the setState(name) method to ensure that each state's enter() method is only called once.

Let's start by creating the states:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// first, import each state 👇
import IdleState from './states/IdleState'
import MoveLeftState from './states/MoveLeftState'
import MoveRightState from './states/MoveRightState'
import MoveDownState from './states/MoveDownState'
import MoveUpState from './states/MoveUpState'

// then in the constructor:
constructor(player)
{
	this.states = {
		idle: new IdleState(player),
		moveLeft: new MoveLeftState(player),
		moveRight: new MoveRightState(player),
		moveDown: new MoveDownState(player),
		moveUp: new MoveUpState(player)
	}
}

We'll be able to reference each state by name like 'idle', 'moveLeft', 'moveDown', etc.

Next, let's implement setState(name):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
setState(name)
{
	if (this.currentState === this.states[name])
	{
		return
	}

	this.currentState = this.states[name]
	this.currentState.enter()
}

Notice that we check if the desired state is already this.currentState and early exit if that is true.

Then if the desired state is different than this.currentState, we set this.currentState to the new state and call the enter() method.

Using PlayerController in the Game Scene

Let's see our use of the State Pattern in action by creating an instance of PlayerController in the Game Scene like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// import 👇 below previous imports...
import PlayerController from './PlayerController'

export default class Game extends Phaser.Scene
{
	// other imports...

	/** @type {PlayerController} */
	playerController // 👈 create a class property

	create()
	{
		// create the player...

		// create an instance of PlayerController 👇
		this.playerController = new PlayerController(this.player)

		// set initial state to 'idle' 👇
		this.playerController.setState('idle')

		// other code...
	}
	
	// other code...
}

We've omitted a bunch of code from the Game Scene but the main goal is to create a new instance of PlayerController in the create() method.

Next, we'll update the logic in updatePlayer() like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
updatePlayer()
{
	if (!this.player.active)
	{
		return
	}

	// 👇 this replaces the middle code block
	if (this.cursors.left.isDown)
	{
		this.playerController.setState('moveLeft')
	}
	else if (this.cursors.right.isDown)
	{
		this.playerController.setState('moveRight')
	}
	else if (this.cursors.up.isDown)
	{
		this.playerController.setState('moveUp')
	}
	else if (this.cursors.down.isDown)
	{
		this.playerController.setState('moveDown')
	}
	else
	{
		this.playerController.setState('idle')
	}
	// 👆

	const spaceJustPressed = Phaser.Input.Keyboard.JustUp(this.cursors.space)
	if (spaceJustPressed && this.activeBox)
	{
		this.openBox(this.activeBox)

		this.activeBox.setFrame(10)
		this.activeBox = undefined
	}

You should see that Memory Match functions exactly the same as before. The difference is only in how we structured and organized the code.

With the logic for each movement state moved into separate classes, you can add more complex logic without cluttering the Game Scene.

The PlayerController class has the basic building blocks of a State Machine. It can be said that it is a simplified and non-generic State Machine implementation.

Next Steps

The PlayerController class in this example is only handling movement states but it can be updated to neatly encapsulate all player input as well.

This would remove keyboard input logic and the updatePlayer() method from adding clutter to the Game Scene. We have more details on how to do that in our Memory Match Extras course.

If you are not yet ready for a course then be sure to sign up for our newsletter so you don't miss any future Phaser 3 game development tips and techniques!

Drop your email into the box below.

Don't miss any Phaser 3 game development content

Subscribers get exclusive tips & techniques not found on the blog!

Join our newsletter. It's free. We don't spam. Spamming is for jerks.

Phaser 3 design patterns patterns state pattern memory match

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

How to Load Images Dynamically in Phaser 3

by on

Does your game have a lot of images that not every player sees? Maybe a collectible card game? Loading those images …

6 minute read

Fix Stretched Image Distortions in Phaser 3 with 9-Slice Scaling

by on

Are you having image distortion problems when scaling to make a button or panel graphic bigger? Multiple versions of the …

5 minute read

Command Pattern to Undo Player Actions

by on

Are you looking for a clean and reusable way to implement undo for player actions? Perhaps you are making a turn-based …

15 minute read

Advanced Logging with the Strategy Pattern

by on

Have you ever tried debugging a problem with your game that only seems to happen in production? The Developer Tools …

7 minute read

Didn't find what you were looking for?


comments powered by Disqus