Command Pattern to Undo Player Actions

Add undo functionality and improve existing code by using the command pattern

by on 15 minute read


Are you looking for a clean and reusable way to implement undo for player actions?

Perhaps you are making a turn-based strategy game or a puzzle game where the player can test what an action might look like before confirming it?

Then the Command Pattern is what you are looking for. 👀

You may have heard of this pattern because it is commonly used for undo/redo and in this article, we will look at using it in an existing push-box or Sokoban puzzle game.

We are using an existing game to better demonstrate what a realistic use of this pattern would look like. Many patterns are easy to implement at the beginning of a project but it is more likely that you are in the middle of a project.

Sometimes, you only realize you need a pattern or learn of it after you've already coded a bunch of gameplay. Redoing everything from scratch is a non-starter.

It is also important to learn the art of refactoring when doing work in the real world. 😎

Sokoban Template

We will be using the Sokoban Template that you can find here.

The template uses Phaser 3 and is written in TypeScript.

This is a more advanced article so we recommend going through the Sokoban video tutorial series if you are not familiar with TypeScript.

We'll be using language features like interfaces and abstract classes in this article to implement the Command Pattern.

TypeScript and Phaser 3 are a great combination for making games on the web. You can get our free ebook to learn more if you are just getting started 👇

Learn to make an Infinite Runner in Phaser 3 with TypeScript!

Drop your email into the box below to get this free 90+ page book and join our newsletter.

Learn more about the book here.

Commanding Moves

The Command Pattern is a method for encapsulating a request or action into something that can be passed around, reused, or undone. Each request or action is a command.

Our Sokoban game has a player that can move left, right, up, and down while pushing any boxes that are in the way.

We can look at these move actions as commands.

The template does not include undo functionality that is common to Sokoban games but it can be easily accomplished using the Command Pattern.

To start, let's look at the basic move logic in the update() method found in the Game Scene:

 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
update()
{
	// ...

	const justLeft = Phaser.Input.Keyboard.JustDown(this.cursors.left!)
	const justRight = Phaser.Input.Keyboard.JustDown(this.cursors.right!)
	const justDown = Phaser.Input.Keyboard.JustDown(this.cursors.down!)
	const justUp = Phaser.Input.Keyboard.JustDown(this.cursors.up!)

	if (justLeft)
	{
		this.tweenMove(Direction.Left, () => {
			this.player?.anims.play('left', true)
		})
	}
	else if (justRight)
	{
		this.tweenMove(Direction.Right, () => {
			this.player?.anims.play('right', true)
		})
	}
	else if (justUp)
	{
		this.tweenMove(Direction.Up, () => {
			this.player?.anims.play('up', true)
		})
	}
	else if (justDown)
	{
		this.tweenMove(Direction.Down, () => {
			this.player?.anims.play('down', true)
		})
	}
}

The code is fairly straight-forward: pressing the arrow keys will move the player in the desired direction.

Let's start applying the Command Pattern by taking the move left logic and putting it into a separate class called MoveLeftCommand. This means we'll have to take the tweenMove() method and anything it depends on.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 👇 varous imports needed by tweenMove()
import { offsetForDirection } from '../../utils/TileUtils'
import { baseTweenForDirection } from '../../utils/TweenUtils'

import { Direction } from '../../consts/Direction'

import {
	boxColorToTargeColor
} from '../../utils/ColorUtils'

export default class MoveLeftCommand
{
	// copied from Game Scene
	private tweenMove(direction: Direction, onStart: () => void)
	{
		// 🚨 many errors in this method
	}
}

We omitted the code in tweenMove() because we'll need to update it. You should notice that there are many errors as the code is referencing methods from the Game Scene.

First, the easy things to fix are the references to this.player, this.tweens, and this.sound. We just need to take a reference to the player Sprite and the Scene in the constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export default class MoveLeftCommand
{
	private scene: Phaser.Scene
	private player: Phaser.GameObjects.Sprite

	constructor(scene: Phaser.Scene, player: Phaser.GameObjects.Sprite)
	{
		this.scene = scene
		this.player = player
	}

	// ...
}

Now, we can change calls to this.tweens and this.sound to this.scene.tweens and this.scene.sound.

Next item to tackle is the calls to Game Scene methods like hasWallAt(), getBoxDataAt(), and others.

The simplest thing to do is to simply say that the passed in scene from the constructor is of type Game and then make the referenced methods public instead of private.

That would work fine but we are going to go in a different direction.

The ILevelState Interface

When working with large or complex codebases, programming against interfaces will lead to easier to maintain code than programming against implementations.

What this means in our example is that we'll create an ILevelState interface with the various methods from the Game Scene that we want to call such as hasWallAt(), getBoxDataAt(), etc.

Then we'll take an instance of ILevelState in the constructor and use it to fix the remaining errors in tweenMove().

Here's what ILeveState looks like 👇

1
2
3
4
5
6
7
8
// in a file named `ILevelState.ts`
export default interface ILevelState
{
	getBoxDataAt(x: number, y: number): { box: Phaser.GameObjects.Sprite, color: number } | undefined
	hasWallAt(x: number, y: number): boolean
	changeTargetCoveredCountForColor(color: number, change: number): void
	hasTargetAt(x: number, y: number, tileIndex: number): boolean
}

Next, update MoveLeftCommand like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export default class MoveLeftCommand
{
	private scene: Phaser.Scene
	private player: Phaser.GameObjects.Sprite
	private levelState: ILevelState

	constructor(scene: Phaser.Scene, player: Phaser.GameObjects.Sprite, levelState: ILevelState)
	{
		this.scene = scene
		this.player = player
		this.levelState = levelState
	}

	// ...
}

Then the tweenMove() method can replace calls to this.hasWallAt() and similar with this.levelState.hasWallAt(). This should fix all but 1 error:

 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
private tweenMove(direction: Direction, onStart: () => void)
{
	if (!this.player || this.scene.tweens.isTweening(this.player!))
	{
		return
	}

	const x = this.player.x
	const y = this.player.y

	const offset = offsetForDirection(direction)
	const ox = x + offset.x
	const oy = y + offset.y

	const hasWall = this.levelState.hasWallAt(ox, oy)

	if (hasWall)
	{
		this.scene.sound.play('error')
		return
	}

	const baseTween = baseTweenForDirection(direction)

	const boxData = this.levelState.getBoxDataAt(ox, oy)
	if (boxData)
	{
		const nextOffset = offsetForDirection(direction, 2)
		const nx = x + nextOffset.x
		const ny = y + nextOffset.y
		const nextBoxData = this.levelState.getBoxDataAt(nx, ny)
		if (nextBoxData)
		{
			this.scene.sound.play('error')
			return
		}

		if (this.levelState.hasWallAt(nx, ny))
		{
			this.scene.sound.play('error')
			return
		}

		const box = boxData.box
		const boxColor = boxData.color
		const targetColor = boxColorToTargeColor(boxColor)

		const coveredTarget = this.levelState.hasTargetAt(box.x, box.y, targetColor)
		if (coveredTarget)
		{
			this.levelState.changeTargetCoveredCountForColor(targetColor, -1)
		}

		this.scene.sound.play('move')

		this.scene.tweens.add(Object.assign(
			baseTween, 
			{
				targets: box,
				onComplete: () => {
					const coveredTarget = this.levelState.hasTargetAt(box.x, box.y, targetColor)
					if (coveredTarget)
					{
						this.levelState.changeTargetCoveredCountForColor(targetColor, 1)
					}
				}
			}
		))
	}
	
	this.tweens.add(Object.assign(
		baseTween,
		{
			targets: this.player,
			onComplete: this.handlePlayerStopped,	// 🚨
			onCompleteScope: this,
			onStart
		}
	))
}

The handlePlayerStopped() method on the Game Scene sets the player's animation to idle, updates the moves count, and checks if the level is completed.

Our command should only set the player's animation to idle as that is part of moving and then provide a callback to let the Game Scene handle the rest.

To do that we can add a stopPlayerAnimation() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private stopPlayerAnimation()
{
	if (!this.player)
	{
		return
	}

	const key = this.player?.anims.currentAnim?.key
	if (!key.startsWith('idle-'))
	{
		this.player.anims.play(`idle-${key}`, true)
	}
}

Then, update the tweenMove() method to take an onComplete callback and update the tween to use it like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private tweenMove(direction: Direction, onStart: () => void, onComplete?: () => void)
{
	// other code...

	this.scene.tweens.add(Object.assign(
		baseTween,
		{
			targets: this.player,
			onComplete: () => {
				this.stopPlayerAnimation()
				if (onComplete)
				{
					onComplete()
				}
			},
			onStart
		}
	))
}

The MoveLeftCommand class should now be error-free. 🎉

Execute and Undo

Our MoveLeftCommand needs 2 more methods for executing an action and then undoing it.

Add these two execute() and undo() methods to the class like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export default class MoveLeftCommand
{
	// ...

	execute(onComplete?: () => void)
	{
		this.tweenMove(Direction.Left, () => {
			this.player.anims.play('left', true)
		}, onComplete)
	}

	undo(onComplete?: () => void)
	{
		this.tweenMove(Direction.Right, () => {
			this.player.anims.play('right', true)
		}, onComplete)
	}
}

Notice that execute() simply calls tweenMove() with Direction.Left and then undo() does the same thing except it uses the opposite direction.

There's also an optional onComplete handler for any logic that needs to happen after the move is finished.

Using MoveLeftCommand

There's a fair amount of code in MoveLeftCommand so let's make sure it does what we expect.

First, update the Game Scene to implement the ILevelState interface like this:

1
2
3
4
5
6
7
8
// other imports...

import ILevelState from './ILevelState'

export default class Game extends Phaser.Scene implements ILevelState
{
	// ...
}

Then change getBoxDataAt(), hasWallAt(), changeTargetCoveredCountForColor(), and hasTargetAt() from private to public by simply removing the private keyword.

 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
export default class Game extends Phaser.Scene implements ILevelState
{
	// ...

	// 👇 notice the private keyword is removed
	changeTargetCoveredCountForColor(color: number, change: number)
	{
		// ...
	}

	getBoxDataAt(x: number, y: number)
	{
		// ...
	}

	hasWallAt(x: number, y: number)
	{
		// ...
	}

	hasTargetAt(x: number, y: number, tileIndex: number)
	{
		// ...
	}
}

Next, replace the move left code from the update() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 1️⃣ import this 👇
import MoveLeftCommand from './moves/MoveLeftCommand'

// 2️⃣ then in the update() method
update()
{
	// ...

	const justLeft = Phaser.Input.Keyboard.JustDown(this.cursors.left!)
	// ...

	if (justLeft)
	{
		const command = new MoveLeftCommand(this, this.player!, this)
		command.execute(() => this.handlePlayerStopped())
	}
	// ...
}

Play the game and everything should still work as expected. Pressing the left arrow key should move the player and any boxes in the way.

Generalizing Move Commands

Think about what a MoveRightCommand would look like and you should see that it will share a lot of code with MoveLeftCommand.

The only thing that will be different is the direction given to tweenMove() in the execute() and undo() methods.

This means we can extract the common logic into an abstract base class called BaseMoveCommand that LeftMoveCommand, RightMoveCommand, and others can extend from.

An abstract class is a class that cannot be used to create new instances. Instead, it must be subclassed. It is similar to the idea of an interface except it contains concrete implementations.

This is what BaseMoveCommand would look like:

  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
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import IMoveCommand from './IMoveCommand'
import ILevelState from '../ILevelState'

import { offsetForDirection } from '../../utils/TileUtils'
import { baseTweenForDirection } from '../../utils/TweenUtils'

import { Direction } from '../../consts/Direction'

import {
	boxColorToTargeColor
} from '../../utils/ColorUtils'

export default abstract class BaseMoveCommand
{
	protected scene: Phaser.Scene
	protected player: Phaser.GameObjects.Sprite
	private levelState: ILevelState

	protected pushedBoxData?: { box: Phaser.GameObjects.Sprite, color: number }

	constructor(scene: Phaser.Scene, player: Phaser.GameObjects.Sprite, levelState: ILevelState)
	{
		this.scene = scene
		this.player = player
		this.levelState = levelState
	}

	abstract execute(onComplete?: () => void)

	abstract undo(onComplete?: () => void)

	protected tweenMove(direction: Direction, onStart: () => void, onComplete?: () => void)
	{
		if (!this.player || this.scene.tweens.isTweening(this.player!))
		{
			return
		}

		const x = this.player.x
		const y = this.player.y

		const offset = offsetForDirection(direction)
		const ox = x + offset.x
		const oy = y + offset.y

		const hasWall = this.levelState.hasWallAt(ox, oy)

		if (hasWall)
		{
			this.scene.sound.play('error')
			return
		}

		const baseTween = baseTweenForDirection(direction)

		const boxData = this.levelState.getBoxDataAt(ox, oy)
		if (boxData)
		{
			const nextOffset = offsetForDirection(direction, 2)
			const nx = x + nextOffset.x
			const ny = y + nextOffset.y
			const nextBoxData = this.levelState.getBoxDataAt(nx, ny)
			if (nextBoxData)
			{
				this.scene.sound.play('error')
				return
			}

			if (this.levelState.hasWallAt(nx, ny))
			{
				this.scene.sound.play('error')
				return
			}

			// store the box data that will be pushed
			this.pushedBoxData = boxData

			const box = boxData.box
			const boxColor = boxData.color
			const targetColor = boxColorToTargeColor(boxColor)

			const coveredTarget = this.levelState.hasTargetAt(box.x, box.y, targetColor)
			if (coveredTarget)
			{
				this.levelState.changeTargetCoveredCountForColor(targetColor, -1)
			}

			this.scene.sound.play('move')

			this.scene.tweens.add(Object.assign(
				baseTween, 
				{
					targets: box,
					onComplete: () => {
						const coveredTarget = this.levelState.hasTargetAt(box.x, box.y, targetColor)
						if (coveredTarget)
						{
							this.levelState.changeTargetCoveredCountForColor(targetColor, 1)
						}
					}
				}
			))
		}
		
		this.scene.tweens.add(Object.assign(
			baseTween,
			{
				targets: this.player,
				onComplete: () => {
					this.stopPlayerAnimation()
					if (onComplete)
					{
						onComplete()
					}
				},
				onStart
			}
		))
	}

	private stopPlayerAnimation()
	{
		if (!this.player)
		{
			return
		}

		const key = this.player?.anims.currentAnim?.key
		if (!key.startsWith('idle-'))
		{
			this.player.anims.play(`idle-${key}`, true)
		}
	}
}

The code here is largely the same as MoveLeftCommand except we've removed the implementation for execute() and undo() and made them abstract. This means each subclass will have to provide an implementation for these 2 methods.

Then we can update MoveLeftCommand to extend from BaseMoveCommand and remove 90% of the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { Direction } from '../../consts/Direction'
import BaseMoveCommand from './BaseMoveCommand'

export default class MoveLeftCommand extends BaseMoveCommand
{
	execute(onComplete: () => void)
	{
		this.tweenMove(Direction.Left, () => {
			this.player.anims.play('left', true)
		}, onComplete)
	}

	undo(onComplete?: () => void)
	{
		this.tweenMove(Direction.Right, () => {
			this.player.anims.play('right', true)
		}, onComplete)
	}
}

Implementing MoveRightCommand, MoveUpCommand, and MoveDownCommand is something we'll leave to you. It should be pretty simple using MoveLeftCommand above as an example.

Implementing Undo

We've done a lot of work to refactor existing code into something that will allow us to easily add undo.

What we'll need to do is save each executed command to a stack or array and then call undo() on the last command each time we want to undo an action.

First, add a moves property to the Game Scene:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// other imports...

import BaseMoveCommand from './moves/BaseMoveCommand'

export default class Game extends Phaser.Scene implements ILevelState
{
	// ...

	private moves: BaseMoveCommand[] = []
}

Let's make a simple undo button in the Game Scene's create() method like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
create(d: { level: number })
{
	// other code...

	// undo button
	this.add.rectangle(580, 50, 100, 30, 0x0000ff)
		.setInteractive()
		.on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, () => {
			const command = this.moves.pop()
			if (command)
			{
				command.undo(() => {
					--this.movesCount
					this.updateMovesCount()
				})
			}
		})
	this.add.text(580, 50, 'UNDO', { color: '#ffffff'}).setOrigin(0.5)
}

Then, update the code in update() to add the executed command to the moves list like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
update()
{
	// ...

	if (justLeft)
	{
		const command = new MoveLeftCommand(this, this.player!, this)
		command.execute(() => this.handlePlayerStopped())
		this.moves.push(command)
	}
	// ...
}

Try this out and you'll see undo working for the player's movement. One problem you'll notice is that any pushed box does not move back. 😭

We can fix this by keeping track of any pushed boxes in the BaseMoveCommand by adding a pushedBoxData property and setting it to the moved box in tweenMove().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export default abstract class BaseMoveCommand implements IMoveCommand
{
	protected pushedBoxData?: { box: Phaser.GameObjects.Sprite, color: number }

	protected tweenMove(direction: Direction, onStart: () => void, onComplete?: () => void)
	{
		// other code...

		// store the box data that will be pushed
		this.pushedBoxData = boxData
	}
}

Then, we can implement a moveBox() method to move the pushed box in the appropriate direction.

 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
protected moveBox(direction: Direction, boxData?: { box: Phaser.GameObjects.Sprite, color: number })
{
	if (!boxData)
	{
		return
	}

	const baseTween = baseTweenForDirection(direction)

	const { box, color } = boxData

	this.scene.tweens.add(Object.assign(
		baseTween, 
		{
			targets: box,
			onComplete: () => {
				const coveredTarget = this.levelState.hasTargetAt(box.x, box.y, color)
				if (coveredTarget)
				{
					this.levelState.changeTargetCoveredCountForColor(color, 1)
				}
			}
		}
	))
}

Final step is to call this.moveBox() in the undo() method for each move command like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// in MoveRightCommand for example

undo(onComplete?: () => void)
{
	this.tweenMove(Direction.Left, () => {
		this.player.anims.play('left', true)
	}, onComplete)

	this.moveBox(Direction.Left, this.pushedBoxData)
}

Now, undo will move the player and any pushed boxes! 👍

Next Steps

This was quite a long article with many moving pieces so take some time to digest it. 🤓

The Command Pattern is conceptually simple but quite versatile. Along with undoing actions, it can be used to retry actions, delay actions, chain actions together, and more!

For more code refactoring and clean-up, you can try creating a dedicated class that implements ILevelState instead of keeping that logic in the Game Scene and deleting logic that was moved to the BaseMoveCommand.

If you want more strategies for code organization and best practices when making games with Phaser 3 then check out the Memory Match Extras video course.

Not ready for a course? Then be sure to subscribe to our newsletter for more game development tips and techniques! 👇

Don't miss any future 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 patterns command undo sokoban

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

Memory Match in Modern Javascript with Phaser 3 - Part 6

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

8 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 5

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

6 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 4

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

7 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 3

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

10 minute read

Didn't find what you were looking for?


comments powered by Disqus