Design Patterns Unity Turn Based Strategy Game
Have you ever wondered how games like Super Meat Boy and others achieve their replay functionality? One of the ways to do it is by executing the inputs exactly as the player issued them, which, in turn, means that the input needs to be stored somehow. The Command pattern can be used to do that and more.
The Command pattern is also useful if you want to create Undo and Redo functionalities in a strategy game.
In this tutorial, you will implement the Command pattern using C# and use it to traverse a bot character through a 3D maze. During this process, you'll learn:
- The fundamentals of the Command pattern.
- How to implement the Command pattern
- Queuing input commands and postponing their execution.
- Undoing and redoing the issued commands before their execution.
Note: This tutorial assumes that you are already familiar with Unity and have intermediate C# knowledge. This tutorial uses Unity 2019.1 and C# 7.
Getting Started
To set things in motion, download the project materials using the link at the top or bottom of this tutorial. Unzip the file and open the Starter project inside Unity.
Go to RW/Scenes and open the Main scene. You will notice that the scene contains a bot inside a maze, and there is a terminal UI that displays instructions. The floor's design is like as a grid, which will be useful visually when you make the bot move across the maze.
If you click Play, the instructions won't seem to be working. That's OK because you will add that functionality in this tutorial.
The most interesting part of this scene is the Bot GameObject. Select it from the Hierarchy by clicking on it.
Take a look in the Inspector, and you will see that it has a Bot component attached to it. You will use this component while issuing input commands.
Understanding the Bot logic
Navigate to RW/Scripts and open the Bot script in your code editor. You don't need to know about what is happening in the Bot script. But take note of the two methods named Move and Shoot. Again, you don't need to worry about what happens inside these methods but you need to understand how to use them.
Notice that the Move method accepts an input parameter of type CardinalDirection. CardinalDirection is an enumeration. An enumerator of type CardinalDirection can be either Up, Down, Right or Left. Based on the chosen CardinalDirection, the bot will move by exactly one square across the grid in the corresponding direction.
The Shoot method makes the bot shoot a projectile that can destroy the yellow walls but is useless against other walls.
Finally, take a look at the ResetToLastCheckpoint method; to understand what it does, take a look at the maze. In the maze, there are points referred to as the checkpoints. To solve the maze, the bot should reach the green checkpoint.
When the bot crosses a new checkpoint, it becomes the last checkpoint for the bot. ResetToLastCheckpoint resets the bot's location to the last checkpoint.
You can't use these methods yet, but you will soon. First, you will learn about the Command design pattern.
Understanding the Command Design Pattern
The Command pattern is one of the 23 design patterns described in the book Design Patterns: Elements of Reusable Object-Oriented Software by the
The authors state that the "Command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different requests, queue or log requests, and support undoable operations."
Woah there! What??
I know, this definition isn't exactly "user friendly," but let's break it down.
Encapsulation refers to the fact that a method call can be encapsulated as an object.
The encapsulated method can act on multiple objects based on the input parameter. This is what parameterizing other objects means.
The resulting "command" can then be stored alongside other commands before their execution. This refers to the queuing of requests.
Command Queue
Finally, "undoable" here doesn't mean something that is impossible to achieve, but rather it refers to the operations that can be reversed by an Undo functionality.
Interesting…
OK, but what does this mean in code?
Simply put, a Command class will have an Execute method which can accept an object (on which the command acts) called the Receiver as an input parameter. So, essentially, the Execute method is encapsulated by the Command class.
The multiple instances of a Command class can be passed around as regular objects, which means that they can be stored in a data structure, such as a queue, stack, etc.
Finally, to execute any command, its Execute method will need to be called. The class that triggers the execution is called the Invoker.
All Clear!
Right now, the project contains an empty class called BotCommand. In the next section you will tackle the requirements to implement the above, enabling the Bot to execute actions using the Command pattern. :]
Moving the Bot
Implementing the Command Pattern
In this section, you will implement the Command pattern. There are multiple ways to implement this pattern. This tutorial will teach you one of such ways.
First, go to RW/Scripts and open the BotCommand script in your editor. The BotCommand class should be empty but not for long.
Paste the following code inside the class:
//1 private readonly string commandName; //2 public BotCommand(ExecuteCallback executeMethod, string name) { Execute = executeMethod; commandName = name; } //3 public delegate void ExecuteCallback(Bot bot); //4 public ExecuteCallback Execute { get; private set; } //5 public override string ToString() { return commandName; } So what is happening, here?
- The
commandNamevariable is simply used to store a human, readable name of a command. It is not essential to this pattern, but you will need it later in the tutorial. - The
BotCommandconstructor accepts a function and a string. This will help you to setup a Command object'sExecutemethod and itsname. - The
ExecuteCallbackdelegate defines the type of the encapsulated method. The encapsulated method will return void and accept an object of typeBot(the Bot component) as an input parameter. - The
Executeproperty will reference the encapsulated method. You will use this to call the encapsulated method. - The
ToStringmethod is overridden to return thecommandNamestring. Useful for convenience and use in the UI for example.
Save your changes and — congratulations! You have successfully implemented the Command pattern.
All that's left is to use it.
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.
Note: You cannot enter more commands until the execution process is over..
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
botCommandslist stores references to theBotCommandinstances. Remember, as far as memory is concerned you only created five command instances, but there can be multiple references to the same command. Moreover, theexecuteCoroutinevariable references theExecuteCommandsRoutinewhich handles the command execution. -
Updatechecks if user has pressed the Return key, in which case it callsExecuteCommands, otherwiseCheckForBotCommandsgets called. -
CheckForBotCommandsuses the static methodHandleInputfromBotInputHandlerto 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. ifexecuteRoutineis not null, it will return without passing anything toAddToCommands. As such the user has to wait until the execution finishes. -
AddToCommandsadds a new reference to the returned command instance, tobotCommands. -
InsertNewTextmethod of theUIManagerclass 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 passcommandNameto it. - The method
ExecuteCommandsstartsExecuteCommandsRoutine. -
ResetScrollToTopfromUIManagerscrolls the terminal UI to the top. This is done just before the execution starts. -
ExecuteCommandsRoutinehas aforloop, which iterates over the commands inside thebotCommandslist and executes them one-by-one by passing thebotobject to the method returned by theExecuteproperty. A pause ofCommandPauseTimeseconds is added after each execution. - The method
RemoveFirstTextLinefromUIManagerremoves 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,
botCommandsgets cleared and the bot gets reset to the last checkpoint it crossed usingResetToLastCheckpoint. Finally,executeRoutineis set tonulland the user can continue issuing more inputs.
Implementing Undo and Redo Functionalities
Run the scene one more time and try to reach the green checkpoint.
You might notice that there is no way for you right now to undo a command that you entered, which means that if you made a mistake you cannot go back unless you execute all the commands. You can fix that by adding an Undo and consequently a Redo functionality.
Go back to SceneManager.cs and add the following variable declaration right after the List declaration for botCommands:
private Stack<BotCommand> undoStack = new Stack<BotCommand>(); The undoStack variable is a Stack (from the Collections family) that will store the references to the commands that have been undone.
Now, you will add two methods UndoCommandEntry and RedoCommandEntry for the purposes of Undo and Redo respectively. In the SceneManager class, paste the following code after ExecuteCommandsRoutine:
private void UndoCommandEntry() { //1 if (executeRoutine != null || botCommands.Count == 0) { return; } undoStack.Push(botCommands[botCommands.Count - 1]); botCommands.RemoveAt(botCommands.Count - 1); //2 uiManager.RemoveLastTextLine(); } private void RedoCommandEntry() { //3 if (undoStack.Count == 0) { return; } var botCommand = undoStack.Pop(); AddToCommands(botCommand); } Going through this code:
- If the commands are being executed or if the
botCommandslist is empty, theUndoCommandEntrymethod won't do anything. Otherwise, it will push the reference to the very last entered command, onto theundoStack. This also removes the command reference from thebotCommandslist. -
RemoveLastTextLinemethod fromUIManagerremoves the last line of text from the terminal UI so that the UI is consistent with the contents ofbotCommandswhen an undo occurs. -
RedoCommandEntrydoes nothing ifundoStackis empty. Otherwise, it pops off the last command on top ofundoStackand adds it back to thebotCommandslist viaAddToCommands.
Now, you will add keyboard inputs to use these methods. Inside the SceneManager class, replace the body of the Update method with the following:
if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else if (Input.GetKeyDown(KeyCode.U)) //1 { UndoCommandEntry(); } else if (Input.GetKeyDown(KeyCode.R)) //2 { RedoCommandEntry(); } else { CheckForBotCommands(); } - Pressing the key U calls the
UndoCommandEntrymethod. - Pressing the key R calls the
RedoCommandEntrymethod.
Handling Edge Cases
Great — you are almost done! But before finishing, you should make sure of two things:
- If you enter a new command, the
undoStackshould get cleared. - Before executing the commands, the
undoStackshould get cleared.
To do this, first you will add a new method to SceneManager. Paste the following method after CheckForBotCommands:
private void AddNewCommand(BotCommand botCommand) { undoStack.Clear(); AddToCommands(botCommand); } This method clears the undoStack and then calls the AddToCommands method.
Now, replace the call to AddToCommands inside CheckForBotCommands with the following:
AddNewCommand(botCommand); Finally, paste the following line after the if statement inside the ExecuteCommands method, to clear the undoStack before execution:
undoStack.Clear(); And you are done! For real this time!
Phew!
Save your work. Build and click Play in the editor. Type commands as before. Press U to undo the commands. Press R to redo the undoed commands.
Try to reach the green checkpoint.
Where to Go From Here?
You can download the project materials using the Download Materials button at the top or bottom of this tutorial.
To know more about the design patterns involved in game programming, I strongly recommend checking out Game Programming Patterns by Robert Nystrom.
To learn more about advanced C# techniques, check out the course C# Collections, Lambdas, and LINQ on our website.
Challenge
As a challenge, see if you can reach the green checkpoint at the end of the maze. I have provided the solution below in case you are stuck. It is just one of the many solutions you can arrive at.
[spoiler title="Maze Solution"]
- moveUp × 2
- moveRight × 3
- moveUp × 2
- moveLeft
- shoot
- moveLeft × 2
- moveUp × 2
- moveLeft × 2
- moveDown × 5
- moveLeft
- shoot
- moveLeft
- moveUp × 3
- shoot × 2
- moveUp × 5
- moveRight × 3
[/spoiler]
That's it! Thanks for reading. I hope you enjoyed the tutorial, and if you have any questions or comments, please join the forum discussion below!
Special thanks to the artists Lee Barkovich, Jesús Lastra and sunburn for some of the assets used in the project.
raywenderlich.com Weekly
The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.
Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!
Design Patterns Unity Turn Based Strategy Game
Source: https://www.raywenderlich.com/3067863-implementing-the-command-pattern-in-unity
Posted by: darnelldayer1948.blogspot.com

0 Response to "Design Patterns Unity Turn Based Strategy Game"
Post a Comment