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 Groundwhich was clicked. How do we do this?

First of all, we need the position of the point on Groundthat 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 structure System.Nullable… A type that accepts a value NULL, may represent the correct range of values ​​for its base value type, plus an optional value NULL… For instance, Nullable<Int32>which is pronounced «Nullable of Int32», can be assigned any value from -2147483648 to 2147483647, and it can also be assigned nullNullable<bool> can be assigned a value true, false or null… Ability to appoint null 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 values true or false, 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 Updateby 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 Groundto 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 Executeand the second is the method ExecuteUndoperforming 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 Invokerwhich 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

Wikidepia Design Patterns

Wikipedia Command Design Pattern

Refactoring Guru

Game Programming Patterns

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.”


GET A DISCOUNT

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *