Command Pattern
2022-11-28
Introduction
Command Pattern is one of the many useful patterns used in software development. Knowing it can greatly help one solve a variety of problems while developing games.
In this tutorial, we'll understand about Command Pattern step-by-step -
- How It Works
- Example
- Excercise
For the exercise, I'll be using Godot to demonstrate command pattern in use, but the example project can be built in any game engine of your choice, as the underlying implementation for the pattern is going to be the same.
How It Works
The idea behind command pattern is to -
Turn a action into an object that contains information about that action in the form of members and methods.
Which in turn is stored in an intermediate buffer like a list, stack, queue, or any data structure(s).
This allows us to keep track of actions via the intermediate buffer and manipulate them as per our needs with the help of each action's members and methods.
It may not be clear at first, so to better understand it let's go through a simple example.
Example
Here we have a game where each of the player's actions, such as movement, interaction, attacking, etc. are all recorded in a Command List (can be any data structure, as per use case). After which they're one by one executed. Here the execution order is based upon the order of player input.
Let's understand this with a diagram -
We can see from this diagram that storing these player actions in an intermediate buffer allows us to do all sorts of things like keeping track of player input history, undo/redo actions, and all sorts of manipulations. Here the key points are -
- Actions
- Command List
The action object contains data and methods about that particular action. Whereas, the command list (or any other data structure that acts as a buffer) contains list of previously executed actions. With the power of these we can solve all sorts of problems that involves tracking and manipulation of actions. As an example, we can implement undo/redo into our game, play all actions in reverse, implement attack combos for n number of consecutive attacks, and much more.
Exercise
Overview
Let's understand Command Pattern better by putting our newly learnt knowledge into use. First and foremost, I encourage you to check the final solution so that it becomes easy to understand what we're trying to achieve.
We can observe a randomly generated maze with the player represented with a red square inside. We can move it with the arrow keys on our keyboard or the UI buttons down below. Try moving the player a couple of moves. You can also see the undo and redo buttons right next to arrow keys. Try pressing the undo button through UI or your keyboard (Ctrl + Z). You'll see the player moves back as expected. Similarly press redo (Ctrl + Y) and see what happens.
And this is an example of the widely used implementation of undo/redo using the Command Pattern!
Project Files
After having seeing the end result it will be easier for us to understand and work on the implementation. So to begin you can download the starter project from over here.
You'll find two projects in the repository, 01 Exercise
and 02 Solution
. To do the implementation open the 01 Exercise
project in Godot. You will find a lot pre-existing code in there. You can ignore it as its working knowledge is not required to understand and implement Undo/Redo. But you're free to go through it and see how it works.
To get ourselves comfortable with the project, let's have a quick walkthrough of it -
When you open the project, the Main.tscn scene should be opened by default. Run the scene and have a play around with it. You'll see that the player is moving, but the undo/redo functionality is not working. And that's what we're gonna implement!
Additionally, the Player moves through getting its input via Navigation.gd and NavigationButton.gd. The actual logic for movement is found in Player.gd.
Finally, the undo/redo gets their input from Edit.gd and CustomButton.gd, while the actual execution happens inside Main.gd.
The above 3 points were just to get a high-level idea of how the existing code works. There's no need to go through their details, as it won't be necessary in understanding the Command Pattern. But feel free to go through its implementation and see how it works.
Steps
Note: I will often be using the terms Action
and Command
interchangeably.
1. Define the Command Class
Create a new scene with the node type set as default Node. Name it BaseCommand
and save the scene at res://Main/CommandPattern/Commands/BaseCommand.tscn
. Then create and add BaseCommand.gd script to the node. Here write the following code snippet -
extends Node
class_name BaseCommand
func execute() -> void:
  pass
func unexecute() -> void:
  pass
Here we are defining the class for our command object that's gonna contain data and methods an action can perform. BaseCommand.gd
is going to act as the base class from which all child classes will be inheriting for their specific purposes. All of them will have two methods execute()
and unexecute()
.
2. Define Action for Player Commands
Next we create a new scene and name it PlayerCommand
. Again save it at following location - res://Main/CommandPattern/Commands/PlayerCommand.tscn
. Then create and attach script PlayerCommand.gd
to it. Here write the following snippet -
extends BaseCommand
class_name PlayerCommand
var player: Player
var direction: Vector2
var magnitude: float
func execute() -> void:
  player.position += direction * magnitude
func unexecute() -> void:
  player.position -= direction * magnitude
So we are inheriting the BaseCommand
class. Then overriding the execute and unexecute methods. Here we have defined variables specific to our usecase of Player movement. In execute we are moving the Player in its usual direction. But in unexecute we are moving Player in opposite direction. Going through it should make you get an idea of what we are doing here. If not, don't worry it's all gonna make sense in the next step.
3. Create the Buffer
After having defined our actions, it is time to setup a system to manage them, and that's where a buffer comes in. We are going to use it as a list in our case, so create a new scene and name it CommandList
and save it at res://Main/CommandPattern/Buffer/CommandList.tscn
. Again create a script of the same name and attach it to the node. Then write the following code in there -
extends Node
var _undo_array: Array = []
var _redo_array: Array = []
func run(command: BaseCommand) -> void:
add_child(command)
command.execute()
_undo_array.append(command)
_empty_redo()
func undo_command() -> BaseCommand:
var command: BaseCommand = _undo_array.pop_back()
if command != null:
_redo_array.append(command)
command.unexecute()
return command
func redo_command() -> BaseCommand:
var command: BaseCommand = _redo_array.pop_back()
if command != null:
_undo_array.append(command)
command.execute()
return command
func _empty_undo() -> void:
for c in _undo_array:
c.queue_free()
_undo_array.clear()
func _empty_redo() -> void:
for c in _redo_array:
c.queue_free()
_redo_array.clear()
func reset_arrays() -> void:
_empty_undo()
_empty_redo()
Whoa! that's quite a lot to process. So let's go through it step by step.
First we define two array variables, _undo_array
and _redo_array
. These are two separate lists (arrays in Godot work like lists) for managing actions (command nodes) back and forth with each other in order to achieve undo/redo functionality.
Then we define the function run() that takes BaseCommand
or any of its child classes as its argument.
Here
add_child(command)
adds the command as its child. This is because in Godot any instance of a class we create is actually instanced as a Node in the tree. So we need to place it somewhere, so we can later remove it. Otherwise it will takeup memory and not get removed, as GDScript doesn't support garbage collection.We execute the command using
command.execute()
. In our case, it will move the Player using PlayerCommand node. Then we add the command to the_undo_array
, and finally empty the_redo_array
. This is because in most applications if we are going through pressing undo command, and then execute a new action. The redo history will be deleted. The same approach I've used in this project.
With undo_command()
, we aim to call this function whenever we want to perform an undo operation. So the code should be easy to understand here.
We first remove the last command from the
_undo_array
. It will give us the last action we performed.Then add it to the
_redo_array
, and run the command'sunexecute()
method. This way we perform the undo operation, and put that action in redo buffer so it can later be used if we perform redo.Finally, return the command node in case we want to do something with it.
In redo_command()
, the same approach applies as in undo_command()
. We remove last element from the _redo_array
, then call command's execute()
method and put the command in _undo_array
.
_empty_undo()
and _empty_redo()
are used to clear undo/redo arrays. Here only _empty_redo()
is used in the implementation of Command Pattern (i.e. when running a command). Other than that _empty_undo()
is used when reseting the game, and is not related to Command Pattern (same with the reset_arrays()
method).
And that's it. Now all we need to do is turn this scene into a Singleton. To do so head over to Project > Project Settings > AutoLoad
and add scene CommandList.tscn
to the list.
4. Update Player.gd
Now time to update Player code to turn movement logic into managable commands. Open Player.gd
script and add a new variable declaration for the PlayerCommand scene.
export var command_scene: PackedScene
Next, we need to update the move(dir: Vector2)
function.
func move(dir: Vector2) -> void:
if _is_collider_ahead(dir):
return
var command = command_scene.instance()
command.player = self
command.direction = dir
command.magnitude = distance
CommandList.run(command)
Here, we instance the PlayerCommand scene and set its parameters. Then finally we run the command by calling CommandList's run()
method that takes care of executing and managing the command.
Finally, save the script and go to Godot's main editor. Set the exported command_scene
value to PlayerCommand scene in the Inspector. And this is it, we have made player movement into manageable actions. We are now just a few steps away from finishing the undo/redo mechanic.
5. Making Undo/Redo Work
Since, the main logic for undo/redo is already implemented in the CommandList.gd
. All we have to do is call the required function. So head over to Main.gd
and update the following two methods -
func undo_command() -> void:
var _command: BaseCommand = CommandList.undo_command()
func redo_command() -> void:
var _command: BaseCommand = CommandList.redo_command()
And I think that's pretty self-explanatory. So without a further a due run the Main.tscn
scene and see the results. Undo/Redo should now be working!
6. Wrapping Up
Almost there, just need to update the New or Reset button you can see with a plus, when the game is running. Head over to Player.gd
again and update the reset()
method with -
func reset() -> void:
position = Vector2(40, 40)
CommandList.reset_arrays()
Here, we call CommandList.reset_arrays()
to clear all arrays when the game is restarted with new maze. Otherwise, undo/redo data from previous game will carry over which we definetely don't want.
Conclusion
We just implemented undo/redo using the Command Pattern. The basic idea behind the pattern is to add data & behaviour to any type of action, and keep a track of it using a buffer so we can perform all sorts of manipulation on it to get our desired result. In our case we implemented undo/redo which is one of the widely used application of Command Pattern.
I hope you found this useful. For any feedbacks or suggestions, do share your thoughts in the comment section down below. Thank You! 😊
Assets
Here's the list of assets shared in the blog -
References
These wonderful resources helped me understand the Command Pattern and how to use it.
- Design Patterns - Command Pattern for a Rewind Mechanic by Pigdev
- Level Up Your Code With Game Programming Patterns - Command Pattern by Unity
- Game Programming Patterns - Command Pattern by Robert Nystrom
License
The following blog is licensed under CC BY 4.0 Arpit Srivastava.