Implementing The Command Pattern In Unity
How to achieve replay functionality, as well as undo and redo in Unity by using the command pattern. Use it to enhance strategy and similar games. By Najmm Shora.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Implementing The Command Pattern In Unity
20 mins
- Getting Started
- Understanding the Bot logic
- Understanding the Command Design Pattern
- Moving the Bot
- Implementing the Command Pattern
- Creating the Commands
- Using the Commands
- Running the Game to Test the Command Pattern
- A Closer Look at the Commands
- Implementing Undo and Redo Functionalities
- Handling Edge Cases
- Where to Go From Here?
- Challenge
Creating the Commands
Open BotInputHandler from RW/Scripts.
Here, you will create five instances of BotCommand
. These instances will respectively encapsulate the methods to move the Bot GameObject up, down, left and right, and also to make the bot shoot.
To do so, paste the following inside this class:
//1
private static readonly BotCommand MoveUp =
new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Up); }, "moveUp");
//2
private static readonly BotCommand MoveDown =
new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Down); }, "moveDown");
//3
private static readonly BotCommand MoveLeft =
new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Left); }, "moveLeft");
//4
private static readonly BotCommand MoveRight =
new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Right); }, "moveRight");
//5
private static readonly BotCommand Shoot =
new BotCommand(delegate (Bot bot) { bot.Shoot(); }, "shoot");
In each of these instances, an anonymous method is passed to the constructor. This anonymous method will be encapsulated inside its corresponding command object. As you can see, the signature of each of the anonymous methods matches the requirements set by the ExecuteCallback
delegate.
In addition, the second parameter to the constructor is a string representing the name given to represent the command. This name will be returned by the ToString
method of the command instance. This will be used later for the UI.
In the first four instances, the anonymous methods call the Move
method on the bot
object. The input parameter varies, however.
For the MoveUp
, MoveDown
, MoveLeft
and MoveRight
commands, the parameter passed to Move
is respectively CardinalDirection.Up
, CardinalDirection.Down
, CardinalDirection.Left
and CardinalDirection.Right
. These correspond to different directions of movement for the Bot GameObject as discussed previously in the Understanding the Command Design Pattern section.
Finally, in the fifth instance, the anonymous method calls the Shoot
method on the bot
object. This will make the bot shoot a projectile on the execution of this command.
Now that you have created the commands, they need to be accessed somehow when the user issues an input.
To do this, paste the following code inside the BotInputHandler
, just below the the command instances:
public static BotCommand HandleInput()
{
if (Input.GetKeyDown(KeyCode.W))
{
return MoveUp;
}
else if (Input.GetKeyDown(KeyCode.S))
{
return MoveDown;
}
else if (Input.GetKeyDown(KeyCode.D))
{
return MoveRight;
}
else if (Input.GetKeyDown(KeyCode.A))
{
return MoveLeft;
}
else if (Input.GetKeyDown(KeyCode.F))
{
return Shoot;
}
return null;
}
The HandleInput
method simply returns a single command instance based on the key pressed by the user. Save your changes before you continue.
Using the Commands
Alright, now it is time to use the commands you created. Go to RW/Scripts again and open SceneManager script in your editor. In this class, you will notice that there is a reference to a uiManager
variable of type UIManager
.
The UIManager
class provides some helpful utility methods for the terminal UI that is being used in the scene. If a method from UIManager
gets used, this tutorial will explain what it does, but for the purposes of this tutorial you don’t need to know its inner workings.
Furthermore, the bot
variable references the bot component attached to the Bot GameObject.
Now, add the following code to the SceneManager
class, replacing the existing code comment //1
:
//1
private List<BotCommand> botCommands = new List<BotCommand>();
private Coroutine executeRoutine;
//2
private void Update()
{
if (Input.GetKeyDown(KeyCode.Return))
{
ExecuteCommands();
}
else
{
CheckForBotCommands();
}
}
//3
private void CheckForBotCommands()
{
var botCommand = BotInputHandler.HandleInput();
if (botCommand != null && executeRoutine == null)
{
AddToCommands(botCommand);
}
}
//4
private void AddToCommands(BotCommand botCommand)
{
botCommands.Add(botCommand);
//5
uiManager.InsertNewText(botCommand.ToString());
}
//6
private void ExecuteCommands()
{
if (executeRoutine != null)
{
return;
}
executeRoutine = StartCoroutine(ExecuteCommandsRoutine());
}
private IEnumerator ExecuteCommandsRoutine()
{
Debug.Log("Executing...");
//7
uiManager.ResetScrollToTop();
//8
for (int i = 0, count = botCommands.Count; i < count; i++)
{
var command = botCommands[i];
command.Execute(bot);
//9
uiManager.RemoveFirstTextLine();
yield return new WaitForSeconds(CommandPauseTime);
}
//10
botCommands.Clear();
bot.ResetToLastCheckpoint();
executeRoutine = null;
}
That's a lot of code! But don't worry; you are finally ready for the first proper run of the project in the Game view.
You will examine this code afterward. Save your changes before you continue.
Running the Game to Test the Command Pattern
Alright, it's time to build everything and press Play in the Unity editor.
You should be able to enter direction commands using the WASD keys. To enter the shoot command, use the F key. Finally, to execute, press the Return key.
Notice how the lines are being added to the terminal UI. The commands are being represented by their names in the UI. This was possible due to the commandName
variable.
Also, notice how the UI scrolls to the top before execution and how the lines are being removed upon execution.
A Closer Look at the Commands
Now, it's time to examine the code you added in the "Using the Commands" section:
- The
botCommands
list stores references to theBotCommand
instances. Remember, as far as memory is concerned you only created five command instances, but there can be multiple references to the same command. Moreover, theexecuteCoroutine
variable references theExecuteCommandsRoutine
which handles the command execution. -
Update
checks if user has pressed the Return key, in which case it callsExecuteCommands
, otherwiseCheckForBotCommands
gets called. -
CheckForBotCommands
uses the static methodHandleInput
fromBotInputHandler
to check if the user has issued an input, in which case a command is returned. The returned command gets passed toAddToCommands
. However, if the commands are being executed i.e. ifexecuteRoutine
is not null, it will return without passing anything toAddToCommands
. As such the user has to wait until the execution finishes. -
AddToCommands
adds a new reference to the returned command instance, tobotCommands
. -
InsertNewText
method of theUIManager
class adds a new line of text to the terminal UI. The line is the string passed to it as the input parameter. In this case, you passcommandName
to it. - The method
ExecuteCommands
startsExecuteCommandsRoutine
. -
ResetScrollToTop
fromUIManager
scrolls the terminal UI to the top. This is done just before the execution starts. -
ExecuteCommandsRoutine
has afor
loop, which iterates over the commands inside thebotCommands
list and executes them one-by-one by passing thebot
object to the method returned by theExecute
property. A pause ofCommandPauseTime
seconds is added after each execution. - The method
RemoveFirstTextLine
fromUIManager
removes the very first line of text in the terminal UI, if it exists. As such, after a command gets executed its name gets removed from the UI. - After the execution of all the commands,
botCommands
gets cleared and the bot gets reset to the last checkpoint it crossed usingResetToLastCheckpoint
. Finally,executeRoutine
is set tonull
and the user can continue issuing more inputs.