Implementing a design pattern

We invite future students of the " Unity Game Developer. Professional " course to watch the 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.





: β€” , , () , . , / .





. , ( ). β€” : (GUI), , (logic handler), -.





GUI , . , GUI -.





 

UML ​. , , .





Command



, (ConcreteCommandN



) Invoker



, Client



Receiver



.





Command

Command (Execute) (Undo) . Execute , , Undo.





public interface ICommand      
{ 
    void Execute();
    void ExecuteUndo();       
}
      
      



Invoker

Invoker



( Sender



) . , . , . . , . .





Client

(Client) . , (Receiver), . . , .





Receiver ( )

Receiver () β€” , -. . , , .





Command . , - ( ). .





, , . (immutable),   .





Unity

, Unity . . (Undo), .





, !





3D Unity

3D Unity. CommandDesignPattern



.





Plane, . Hierarchy Plane. Β«GroundΒ» 20 X 20 z. , .





Player



. Capsule



. Hierarchy Capsule



. Player



.





GameManager.cs

Ground



. GameManager.cs



.





Player



.





public GameObject



player



.





public GameObject mPlayer;
      
      



Player



Hierarchy



Player



.





(Up, Down, Left Right).





. Update



. 1 .





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;
    }
}
      
      



Play



, . (Up, Down, Left Right), .





 

β€” Player



Ground



, . ?





, Ground



, .





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;
}
      
      



Vector3?





?



C#,





public int? myProperty { get; set; }
      
      



, nullable





Nullable



, System.Nullable



. , NULL



, NULL



. , Nullable<Int32>



, Β«Nullable of Int32Β»



, -2147483648 2147483647, null



. Nullable<bool>



true



, false



null



. null



, , , . , true



false



, .





, , MoveTo



. MoveTo



. .





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;
}
      
      



, , , .





Update



, .





****
    var clickPoint = GetClickPosition();
    if (clickPoint != null)
    {
        IEnumerator moveto = MoveToInSeconds(_player, clickPoint.Value, 0.5f);
        StartCoroutine(moveto);
    }
****
      
      



Play



, . (Up, Down, Left Right) Ground



, Player



.





(Undo



)? ? .





Unity

Undo



, , .





Undo



β€” , Unity.





. Command



.





Command

public interface ICommand
{
    void Execute();
    void ExecuteUndo();
}
      
      



Command



. β€” Execute



, β€” ExecuteUndo



, . ( , ).





.





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;
}
      
      



, ExecuteUndo



. , Execute



.





Invoker

Invoker



. , Invoker



β€” , . , Undo



Last In First Out (LIFO)



.





LIFO? LIFO? Stack



.





C# , LIFO (Last In First Out). . Push()



( ), Pop()



( ) Peek()



.





Invoker



, .





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;
}
      
      



, Execute



Undo



. Execute



, Push



Execute



. Peek



.





Undo ExecuteUndo



, ( Peek



). Invoker



, Pop



.





Invoker



  . Invoker



GameManager



.





private Invoker mInvoker;
      
      



mInvoker



Start



GameManager.







mInvoker = new Invoker();
      
      



Undo

U



. Update



.





// Undo 
    if (Input.GetKeyDown(KeyCode.U))
    {
        mInvoker.Undo();
    }
      
      



Update







.





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();
    }
}
      
      



Play



, . (Up, Down, Left Right), , Β«uΒ»





β€” GoF, , - , , , , , .





Unity

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);
    }
}
      
      



Wikidepia Design Patterns





Wikipedia Command Design Pattern





Refactoring Guru





Game Programming Patterns





Design Patterns in Game Programming






"Unity Game Developer. Professional".



" ".













All Articles