Implementing a design pattern
Future students of the course “Unity Game Developer. Professional“ we invite you to watch an open lesson on the topic “Advanced artificial intelligence of enemies in shooters.”
And now we are sharing the traditional translation of useful material.
In this tutorial, we will master the Command design pattern and implement it in Unity as part of a game object movement system.
Introducing the Command pattern
Requests, orders and commands: we are all familiar with them in real life; one person sends a request (or order, or command) to another person to perform (or not to perform) some of the tasks assigned to him. In software design and development, this works in a similar way: a request from one component is passed to another to perform specific tasks within the Team pattern.
Definition: The Command pattern is a behavioral design pattern in which a request is transformed into an object that encapsulates (contains) all the information you need to performing an action or trigger event at a later time. This conversion to objects allows you to parameterize methods with different requests, delay execution of a request, and / or enqueue it.
A good and reliable software product should be based on the principle of separation of duties. It can usually be implemented by breaking the application down into multiple layers (or software components). A common example in practice is dividing an application into two levels: a graphical user interface (GUI), which is responsible only for the graphical part, and a logic handler, which implements business logic.
The GUI layer is responsible for rendering a beautiful picture on the screen and capturing any input data, while the actual computation for a specific task occurs at the logic processor level. Thus, the GUI layer delegates work to the underlying business logic layer.
Pattern structure Command
The structure of the Command pattern is shown below in the form of a UML class diagram. The classes included in the diagram are detailed below.
Class diagram for a design pattern Command
To implement the Command pattern, we need an abstract class Command
, specific commands (ConcreteCommandN
) and classes Invoker
, Client
and Receiver
…
Command
Command is usually an interface with one or two methods of execution (Execute) and cancellation (Undo) of the command operation. All concrete command classes must derive from this interface and must implement the actual Execute and, optionally, the Undo implementation.
public interface ICommand
{
void Execute();
void ExecuteUndo();
}
Invoker
Class Invoker
(also known as Sender
) is responsible for initiating requests. This is the class that runs the required command. This class must have a variable that stores a reference to the command object or its container. The invoker, instead of directly sending the request to the recipient, runs a command. Please note that the invoker is not responsible for the creation of the command object. He usually receives a pre-built command from the client via the constructor.
Client
The Client creates and configures specific command objects. The client must pass all request parameters, including the Receiver instance, to the command constructor. After that, the resulting command can be associated with one or more invokers. Any class that creates various command objects can serve as a client.
Receiver (optional class)
The Receiver class is the class that accepts the command and contains basically all the business logic. Almost any object can act as a receiver. Most commands only handle the details of how the request is passed on to the recipient, while the recipient itself does the actual work.
Specific commands
Concrete commands inherit from the Command interface and implement different types of requests. The particular command itself should not do the work, but rather should pass the call to one of the business logic objects or to the receiver (as described above). However, to simplify your code, these classes can be combined.
The parameters required to execute the method on the receiving object can be declared as fields in a specific command. You can make command objects immutable by only allowing those fields to be initialized through the constructor.
Implementing the Team pattern in Unity
As mentioned above, we are going to implement the Command pattern in Unity to solve the problem of moving a game object by applying different types of movement. Each of these types of movement will be implemented as a command. We will also implement the Undo function so that we can undo operations in reverse.
So, let’s begin!
Creating a new 3D Unity project
We’ll start by creating a 3D Unity project. Let’s call it CommandDesignPattern
…
Surface creation
For this tutorial, we will create a simple Plane object that will shape our surface to move. Right-click the Hierarchy window and create a new Plane game object. Rename it “Ground” and resize it to 20 units in the x-axis and 20 units in the z-axis. You can apply color or texture to the surface to your liking to make it look more attractive.
Player creation
Now we will create a game object Player
… In this tutorial, we will use the object to represent the player. Capsule
… Right click on the window Hierarchy and create a new game object Capsule
… Rename it to Player
…
Creating the GameManager.cs script
Please select game object Ground
and add a new scripting component. Name the script GameManager.cs
…
Now we will implement moving the object Player
…
To do this, we add public GameObject
variable named player
…
public GameObject mPlayer;
Now drag the game object Player
of Hierarchy
in field Player
in the inspector window.
Realization of player movements
We will use the arrow keys (Up, Down, Left and Right) to move the player.
First, let’s implement the movement in the simplest way. We will implement it in the method Update
… For simplicity, we implement a discrete movement of 1 unit for each keystroke in the corresponding directions.
void Update()
{
Vector3 dir = Vector3.zero;
if (Input.GetKeyDown(KeyCode.UpArrow))
dir.z = 1.0f;
else if (Input.GetKeyDown(KeyCode.DownArrow))
dir.z = -1.0f;
else if (Input.GetKeyDown(KeyCode.LeftArrow))
dir.x = -1.0f;
else if (Input.GetKeyDown(KeyCode.RightArrow))
dir.x = 1.0f;
if (dir != Vector3.zero)
{
_player.transform.position += dir;
}
}
Click the button Play
and see what happened. Press the arrow keys (Up, Down, Left and Right) to see the player’s movement.
Implementing click motion
Now we will implement right-click movement – Player
will have to move to a location on Ground
which was clicked. How do we do this?
First of all, we need the position of the point on Ground
that was clicked with the right mouse button.
public Vector3? GetClickPosition()
{
if(Input.GetMouseButtonDown(1))
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if(Physics.Raycast(ray, out hitInfo))
{
//Debug.Log("Tag = " + hitInfo.collider.gameObject.tag);
return hitInfo.point;
}
}
return null;
}
What is this Vector3 return type?
Operator use ?
for return types in C # like
public int? myProperty { get; set; }
means that the type of the question mark value is nullable type
Nullable
types are instances of the structureSystem.Nullable
… A type that accepts a valueNULL
, may represent the correct range of values for its base value type, plus an optional valueNULL
… For instance,Nullable<Int32>
which is pronounced«Nullable of Int32»
, can be assigned any value from -2147483648 to 2147483647, and it can also be assignednull
…Nullable<bool>
can be assigned a valuetrue
,false
ornull
… Ability to appointnull
numeric and boolean types are especially useful when you are dealing with databases and other types that contain elements that may not be assigned a value. For example, a boolean field in a database can store valuestrue
orfalse
, or it may not yet be defined.
Now that we have the click position, we need to implement the function MoveTo
… Our function MoveTo
should move the player smoothly. We will implement this as a coroutine with linear interpolation of the displacement vector.
public IEnumerator MoveToInSeconds(GameObject objectToMove, Vector3 end, float seconds)
{
float elapsedTime = 0;
Vector3 startingPos = objectToMove.transform.position;
end.y = startingPos.y;
while (elapsedTime < seconds)
{
objectToMove.transform.position = Vector3.Lerp(startingPos, end, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
yield return null;
}
objectToMove.transform.position = end;
}
Now all we have to do is call the coroutine whenever a right click occurs.
Let’s change the method Update
by adding the following lines of code.
****
var clickPoint = GetClickPosition();
if (clickPoint != null)
{
IEnumerator moveto = MoveToInSeconds(_player, clickPoint.Value, 0.5f);
StartCoroutine(moveto);
}
****
Click the button Play
and see what happened. Press the arrow keys (Up, Down, Left and Right) and right-click on Ground
to see the movement of the object Player
…
Implementing the undo operation
How to implement the undo operation (Undo
)? Where is motion cancellation needed? Try to guess for yourself.
Implementing the Team pattern in Unity
We are going to implement the method Undo
for each move operation that we can perform with both keystrokes and right-clicking.
The easiest way to implement an operation Undo
– use the Team design pattern by implementing it in Unity.
Within this pattern, we transform all types of movement into commands. Let’s start by creating an interface Command
…
Command interface
public interface ICommand
{
void Execute();
void ExecuteUndo();
}
Our interface Command
has two methods. The first is the usual method Execute
and the second is the method ExecuteUndo
performing the undo operation. For each specific command, we will need to implement these two methods (in addition to other methods if needed).
Now let’s convert our basic movement to a specific command.
CommandMove
public class CommandMove : ICommand
{
public CommandMove(GameObject obj, Vector3 direction)
{
mGameObject = obj;
mDirection = direction;
}
public void Execute()
{
mGameObject.transform.position += mDirection;
}
public void ExecuteUndo()
{
mGameObject.transform.position -= mDirection;
}
GameObject mGameObject;
Vector3 mDirection;
}
CommandMoveTo
public class CommandMoveTo : ICommand
{
public CommandMoveTo(GameManager manager, Vector3 startPos, Vector3 destPos)
{
mGameManager = manager;
mDestination = destPos;
mStartPosition = startPos;
}
public void Execute()
{
mGameManager.MoveTo(mDestination);
}
public void ExecuteUndo()
{
mGameManager.MoveTo(mStartPosition);
}
GameManager mGameManager;
Vector3 mDestination;
Vector3 mStartPosition;
}
Notice how the method is implemented ExecuteUndo
… It just does the opposite of what the method does Execute
…
Invoker class
Now we need to implement the class Invoker
… remember, that Invoker
Is a class that contains all commands. Also remember that for work Undo
we will need to implement a data structure like Last In First Out (LIFO)
…
What is LIFO? How can we implement LIFO? I present to you the data structure Stack
…
C # provides a special type of collection in which items are stored in LIFO (Last In First Out) style. This collection includes a shared and non-shared stack. It provides a method Push()
to add a value to the top (as the last), the method Pop()
to remove the top (or last) value and the method Peek()
to get the top value.
Now we will implement the class Invoker
which will contain the command stack.
public class Invoker
{
public Invoker()
{
mCommands = new Stack<ICommand>();
}
public void Execute(ICommand command)
{
if (command != null)
{
mCommands.Push(command);
mCommands.Peek().Execute();
}
}
public void Undo()
{
if(mCommands.Count > 0)
{
mCommands.Peek().ExecuteUndo();
mCommands.Pop();
}
}
Stack<ICommand> mCommands;
}
Note how the methods Execute
and Undo
implemented by an invoker. When calling the method Execute
the invoker pushes the command onto the stack by calling the method Push
and then executes the method Execute
teams. The command on top of the stack is obtained using the method Peek
… Likewise, when calling
Invoker’s Undo calls the method ExecuteUndo
commands, getting the top command from the stack (using the method Peek
). Thereafter Invoker
removes the top command using the method Pop
…
We are now ready to use Invoker
and teams. To do this, we will first add a new variable for the object Invoker
to our class GameManager
…
private Invoker mInvoker;
Next, we need to initialize the object mInvoker
in method Start
our script GameManager.
mInvoker = new Invoker();
Undo
We’ll call cancellation by pressing the key U
… Let’s add the following code to the method Update
…
// Undo
if (Input.GetKeyDown(KeyCode.U))
{
mInvoker.Undo();
}
Using Commands
Now we will change the method Update
according to the pattern implementation Команда
…
void Update()
{
Vector3 dir = Vector3.zero;
if (Input.GetKeyDown(KeyCode.UpArrow))
dir.z = 1.0f;
else if (Input.GetKeyDown(KeyCode.DownArrow))
dir.z = -1.0f;
else if (Input.GetKeyDown(KeyCode.LeftArrow))
dir.x = -1.0f;
else if (Input.GetKeyDown(KeyCode.RightArrow))
dir.x = 1.0f;
if (dir != Vector3.zero)
{
//Using command pattern implementation.
ICommand move = new CommandMove(mPlayer, dir);
mInvoker.Execute(move);
}
var clickPoint = GetClickPosition();
//Using command pattern right click moveto.
if (clickPoint != null)
{
CommandMoveTo moveto = new CommandMoveTo(
this,
mPlayer.transform.position,
clickPoint.Value);
mInvoker.Execute(moveto);
}
// Undo
if (Input.GetKeyDown(KeyCode.U))
{
mInvoker.Undo();
}
}
Click the button Play
and see what happens. Press the arrow keys (Up, Down, Left and Right) to see the player’s movement, and the “U” for canceling in reverse order.
Conclusion
Design Pattern Team is one of twenty-three well-known GoF design patternsthat describe how to solve recurring design problems to develop flexible and reusable object-oriented software – that is, objects that are easier to implement, modify, test, reuse, and maintain.
Unity script listing
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public interface ICommand
{
void Execute();
void ExecuteUndo();
}
public class CommandMove : ICommand
{
public CommandMove(GameObject obj, Vector3 direction)
{
mGameObject = obj;
mDirection = direction;
}
public void Execute()
{
mGameObject.transform.position += mDirection;
}
public void ExecuteUndo()
{
mGameObject.transform.position -= mDirection;
}
GameObject mGameObject;
Vector3 mDirection;
}
public class Invoker
{
public Invoker()
{
mCommands = new Stack<ICommand>();
}
public void Execute(ICommand command)
{
if (command != null)
{
mCommands.Push(command);
mCommands.Peek().Execute();
}
}
public void Undo()
{
if (mCommands.Count > 0)
{
mCommands.Peek().ExecuteUndo();
mCommands.Pop();
}
}
Stack<ICommand> mCommands;
}
public GameObject mPlayer;
private Invoker mInvoker;
public class CommandMoveTo : ICommand
{
public CommandMoveTo(GameManager manager, Vector3 startPos, Vector3 destPos)
{
mGameManager = manager;
mDestination = destPos;
mStartPosition = startPos;
}
public void Execute()
{
mGameManager.MoveTo(mDestination);
}
public void ExecuteUndo()
{
mGameManager.MoveTo(mStartPosition);
}
GameManager mGameManager;
Vector3 mDestination;
Vector3 mStartPosition;
}
public IEnumerator MoveToInSeconds(GameObject objectToMove, Vector3 end, float seconds)
{
float elapsedTime = 0;
Vector3 startingPos = objectToMove.transform.position;
end.y = startingPos.y;
while (elapsedTime < seconds)
{
objectToMove.transform.position = Vector3.Lerp(startingPos, end, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
yield return null;
}
objectToMove.transform.position = end;
}
public Vector3? GetClickPosition()
{
if (Input.GetMouseButtonDown(1))
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hitInfo))
{
//Debug.Log("Tag = " + hitInfo.collider.gameObject.tag);
return hitInfo.point;
}
}
return null;
}
// Start is called before the first frame update
void Start()
{
mInvoker = new Invoker();
}
// Update is called once per frame
void Update()
{
Vector3 dir = Vector3.zero;
if (Input.GetKeyDown(KeyCode.UpArrow))
dir.z = 1.0f;
else if (Input.GetKeyDown(KeyCode.DownArrow))
dir.z = -1.0f;
else if (Input.GetKeyDown(KeyCode.LeftArrow))
dir.x = -1.0f;
else if (Input.GetKeyDown(KeyCode.RightArrow))
dir.x = 1.0f;
if (dir != Vector3.zero)
{
//----------------------------------------------------//
//Using normal implementation.
//mPlayer.transform.position += dir;
//----------------------------------------------------//
//----------------------------------------------------//
//Using command pattern implementation.
ICommand move = new CommandMove(mPlayer, dir);
mInvoker.Execute(move);
//----------------------------------------------------//
}
var clickPoint = GetClickPosition();
//----------------------------------------------------//
//Using normal implementation for right click moveto.
//if (clickPoint != null)
//{
// IEnumerator moveto = MoveToInSeconds(mPlayer, clickPoint.Value, 0.5f);
// StartCoroutine(moveto);
//}
//----------------------------------------------------//
//----------------------------------------------------//
//Using command pattern right click moveto.
if (clickPoint != null)
{
CommandMoveTo moveto = new CommandMoveTo(this, mPlayer.transform.position, clickPoint.Value);
mInvoker.Execute(moveto);
}
//----------------------------------------------------//
//----------------------------------------------------//
// Undo
if (Input.GetKeyDown(KeyCode.U))
{
mInvoker.Undo();
}
//----------------------------------------------------//
}
public void MoveTo(Vector3 pt)
{
IEnumerator moveto = MoveToInSeconds(mPlayer, pt, 0.5f);
StartCoroutine(moveto);
}
}
Links
Wikipedia Command Design Pattern
Design Patterns in Game Programming
Learn more about the course “Unity Game Developer. Professional “.
View an open lesson on the topic “Advanced artificial intelligence of enemies in shooters.”
