State Pattern for Changing AI and Player Control in Phaser 3

Use this design pattern to write better code when player or AI control can be swapped

by on 10 minute read


Have you made a few games with Phaser in JavaScript?

And now you want to learn about code structure and best practices?

You've probably come across design patterns in researching ways to improve your code.

But which design patterns and for what use cases? 🤔

The standard examples are okay but… who is making a calculator?

We're making games. How do these patterns apply to games?

In this article, we will look at applying the State Pattern to allow switching AI or player control for paddles in a Pong game.

Also, we're glad to see that you want to keep improving. 👏

Starting with the Pong Template

We will be building on top of the Pong Template found here.

This template corresponds to a YouTube series for beginners so it is a great place to see how the code can be improved!

Pong has a left paddle and a right paddle.

In the template, the left paddle is always controlled by the player and, the right paddle is always controlled by the AI.

We will change it so that each paddle can be player controlled or AI-controlled.

The concepts in this article will allow us to make a local 2 player game, a networked 2 player game, or a game played entirely between AI's!

What is the State Pattern?

The State Pattern is a way of structuring code so that a behavior can be dynamically chosen at runtime.

The Pong Template hard codes a behavior for moving the left paddle (player control) and the right paddle (AI control).

There is no easy way to let players pick which paddle they would prefer to use.

To paraphrase Henry Ford, “you can pick any paddle you want as long as it is the left one”. 😅

Using the State Pattern is not the only way to allow the player or AI to control a paddle.

Another way is to have both implementations in a Paddle class and a boolean flag to switch between each.

But then what happens if your AI is more like Pac-Man and has several different states depending on the situation? 🤔

Your paddle class will quickly get large, messy, and confusing!

With the State Pattern, you can take the player input logic or the AI logic and move it outside of the Paddle class.

Then the Paddle won't need to know the specifics of how it should move. Instead, it will be given a behavior to handle moving logic.

A closely related sibling of the State Pattern is the Strategy Pattern. You'd be hardpressed to find a good explanation of the differences between the two because they are very similar in practice.

Note that this is different than a Finite State Machine (FSM). But it is a building block to an FSM.

Creating a Paddle Class

The template currently leaves the control logic of the paddles to the Game Scene. Let's change that.

Start by creating a Paddle class so that we can more easily reason about changing the state of a paddle.

Create a Paddle.js file in the same directory as the Game.js Scene class.

 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
import Phaser from 'phaser'

export default class Paddle extends Phaser.GameObjects.Rectangle
{
	/**
	 * @param {Phaser.Scene} scene 
	 * @param {number} x 
	 * @param {number} y 
	 * @param {number} width 
	 * @param {number} height 
	 * @param {number} fillColor 
	 * @param {number} fillAlpha 
	 */
	constructor(scene, x, y, width, height, fillColor, fillAlpha)
	{
		super(scene, x, y, width, height, fillColor, fillAlpha)

		scene.physics.add.existing(this, true)

		this.controlState = undefined
	}

	setControlState(controlState)
	{
		this.controlState = controlState
	}

	update()
	{
		if (!this.controlState)
		{
			return
		}

		this.controlState.update(this)
	}
}

Notice that we are subclassing Phaser.GameObjects.Rectangle to continue using rectangles for rendering the paddle.

We add a physics body to the paddle on line 18 in the constructor instead of in the Scene.

Then we add a controlState property to hold a reference to the state that will control how the paddle is moved.

A setter is created on lines 23 - 26 so that it can be set from outside the class at runtime.

It is then used in the update() method on line 35.

With this structure, we can pass in different state implementations to change how the paddle should be controlled.

Wrapping Player Input State

The player input logic is currently being handled in the processPlayerInput() method on the Game Scene.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
processPlayerInput()
{
	/** @type {Phaser.Physics.Arcade.StaticBody} */
	const body = this.paddleLeft.body

	if (this.cursors.up.isDown)
	{
		this.paddleLeft.y -= 10
		body.updateFromGameObject()
	}
	else if (this.cursors.down.isDown)
	{
		this.paddleLeft.y += 10
		body.updateFromGameObject()
	}
}

This method is then called by the update() method.

We will want to take this logic and wrap it into a self-contained class that can be passed to our Paddle class.

Create a PlayerInputState.js file in a folder named states in the same directory that you created Paddle.js.

 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
import Phaser from 'phaser'

export default class PlayerInputState
{
	/**
	 * @param {Phaser.Types.Input.Keyboard.CursorKeys} cursors
	 */
	constructor(cursors)
	{
		this.cursors = cursors
	}

	update(paddle)
	{
		/** @type {Phaser.Physics.Arcade.StaticBody} */
		const body = paddle.body

		if (this.cursors.up.isDown)
		{
			paddle.y -= 10
			body.updateFromGameObject()
		}
		else if (this.cursors.down.isDown)
		{
			paddle.y += 10
			body.updateFromGameObject()
		}
	}
}

We know that we'll need access to the CursorKeys so we make that a constructor argument on line 8.

Then we have an update(paddle) method on line 13 that takes a Paddle instance. This corresponds to the this.controlState.update(this) call in the Paddle class above.

Notice that the code in update(paddle) is almost identical to what was in processPlayerInput().

Using PlayerInputState in Game Scene

Code that is not relevant to using states will be omitted but we will include enough context for you to compare against the template code.

 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
39
40
41
42
43
44
45
// imports and other code...

import Paddle from './Paddle'
import PlayerInputState from './states/PlayerInputState'

class Game extends Phaser.Scene
{
	init()
	{
		// other code...

		// moved from bottom of create()
		this.cursors = this.input.keyboard.createCursorKeys()
	}

	create()
	{
		// create ball and other code...

		const playerInputState = new PlayerInputState(this.cursors)

		// replace these 2 lines
		// this.paddleLeft = this.add.rectangle(50, 250, 30, 100, Colors.White, 1)
		// this.physics.add.existing(this.paddleLeft, true)

		// with these lines
		this.paddleLeft = new Paddle(this, 50, 250, 30, 100, Colors.White, 1)
		this.paddleLeft.setControlState(playerInputState)
		this.add.existing(this.paddleLeft)
	}

	update()
	{
		// other code...

		// replace this line
		// this.processPlayerInput()

		// with this
		this.paddleLeft.update()

		this.updateAI()
		this.checkScore()
	}
}

We moved the setting of this.cursors to init() because we will need early in the create() method.

Then we create a new PlayerInputState on line 20 that is given to the newly created Paddle on lines 27 - 29.

The update() method is changed to replace this.processPlayerInput() with this.paddleLeft.update().

Give this a try and you'll see that you can still control the left paddle with the arrow keys even though the old processPlayerInput() code is not being used.

The State Pattern structure is working! 🎉

Next, we need to update the updateAI() logic.

Creating a Basic AI State

The AI logic currently looks lke 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
updateAI()
{
	const diff = this.ball.y - this.paddleRight.y
	if (Math.abs(diff) < 10)
	{
		return
	}

	const aiSpeed = 3
	if (diff < 0)
	{
		// ball is above the paddle
		this.paddleRightVelocity.y = -aiSpeed
		if (this.paddleRightVelocity.y < -10)
		{
			this.paddleRightVelocity.y = -10
		}
	}
	else if (diff > 0)
	{
		// ball is below the paddle
		this.paddleRightVelocity.y = aiSpeed
		if (this.paddleRightVelocity.y > 10)
		{
			this.paddleRightVelocity.y = 10
		}
	}

	this.paddleRight.y += this.paddleRightVelocity.y
	this.paddleRight.body.updateFromGameObject()
}

Just like with PlayerInputState we can wrap this in a separate class.

Create a BasicAIState.js file in the same folder as PlayerInputState.js 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
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
export default class BasicAIState
{
	constructor(ball)
	{
		this.ball = ball

		this.paddleRightVelocity = new Phaser.Math.Vector2(0, 0)
	}

	update(paddle)
	{
		const diff = this.ball.y - paddle.y
		if (Math.abs(diff) < 10)
		{
			return
		}

		const aiSpeed = 3
		if (diff < 0)
		{
			// ball is above the paddle
			this.paddleRightVelocity.y = -aiSpeed
			if (this.paddleRightVelocity.y < -10)
			{
				this.paddleRightVelocity.y = -10
			}
		}
		else if (diff > 0)
		{
			// ball is below the paddle
			this.paddleRightVelocity.y = aiSpeed
			if (this.paddleRightVelocity.y > 10)
			{
				this.paddleRightVelocity.y = 10
			}
		}

		paddle.y += this.paddleRightVelocity.y
		paddle.body.updateFromGameObject()
	}
}

The AI logic uses the ball to determine how to move so we pass that into the constructor as we did with cursors for the PlayerInputState.

We also create a class property for paddleRightVelocity as we had in the Game Scene. Since it was only used for AI movement we no longer need it in the Game Scene.

Then update(paddle) has the same logic as the updateAI() method.

This looks much cleaner, doesn't it? 🧐

Using BasicAIState in the Game Scene

Using the BasicAIState will be similar to what we did for PlayerInputState earlier.

 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
// import at the top of Game.js
import BasicAIState from './states/BasicAIState'

// then in the create() and update() methods...
create()
{
	// other code..

	const playerInputState = new PlayerInputState(this.cursors)
	const basicAI = new BasicAIState(this.ball)

	// left paddle code...

	// replace this right paddle code
	// this.paddleRight = this.add.rectangle(750, 250, 30, 100, Colors.White, 1)
	// this.physics.add.existing(this.paddleRight, true)

	// with this that uses the basicAI state
	this.paddleRight = new Paddle(this, 750, 250, 30, 100, Colors.White, 1)
	this.paddleRight.setControlState(basicAI)
	this.add.existing(this.paddleRight)
}

update()
{
	// other code...

	// we added this earlier
	this.paddleLeft.update()

	// now, replace this line
	// this.updateAI()

	// with this line for the right paddle
	this.paddleRight.update()
}

This should be pretty self-explanatory based on what we've learned from adding PlayerInputState earlier.

Give this a try and the right paddle should move as it did before!

Next Steps

And that's the State Pattern! You can try using the BasicAIState for both paddles to let the game play itself.

To let the player pick which paddle to use, you can have a selection screen that passes the information to the Game Scene as we did here and then set the appropriate states for each paddle.

From here you can use the same concepts for creating a multiplayer game with local or networked players. 😎

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

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

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

State Pattern for Character Movement in Phaser 3

by on

Writing clean and well-organized code is something all game developers aspire to. We want code to be reusable and easy …

7 minute read

Didn't find what you were looking for?


comments powered by Disqus