<UNITY>/DESIGN PATTERN

Command Pattern

CodeGrimie 2021. 2. 25. 23:22

Command Pattern은 게임에서 입력처리에 많이 사용된다.

 

실제로 사용해보면 여러가지 편리한 점이 많은데 가장 멋진 점을 뽑으라면 플레이어가 옵션에서 임의로 입력키를 바꿀 수 있는 기능을 구현할 수 있다!

 

아주 간단한 예제를 통해서 Command Pattern을 정리한다.

마우스 왼쪽 클릭을 입력하면 이동 명령을 Do 하면서 Stack에 쌓는다.

반대로 마우스 오른쪽 클릭을 입력하면 Stack을 거슬러 올라가면서 Un Do한다.

▼ 유니티 예제

▼ 개념

Command Pattern의 개념은 그렇게 어렵지 않다.

단순하게 접근하면 명령어 클래스를 하나 만들고 그걸 변수로서 사용하는 것 뿐이다.

 

구현 로직은 아래와 같다.

1. ICommand라는 인터페이스를 만든다.

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

2. 실제 작동할 Command를 추가한다.

public class CommandMoveTo : ICommand
{
    public CommandMoveTo(GameManager _gameManager, Vector3 _startPos, Vector3 _destPos)
    {
        m_gameManager   = _gameManager;
        m_destination   = _destPos;
        m_startPosition = _startPos;
    }

    public void ExecuteDo()
    {
        m_gameManager.MoveTo(m_destination);
    }

    public void ExecuteUndo()
    {
        m_gameManager.MoveTo(m_startPosition);
    }

    GameManager m_gameManager;
    Vector3     m_destination;
    Vector3     m_startPosition;
}

3. 특정 조건 마다 Command를 변경 혹은 저장한다.

var leftClickPoint = GetLeftClickPosition();
if (leftClickPoint != null)
{
        CommandMoveTo moveto = new CommandMoveTo(this, m_player.transform.position, leftClickPoint.Value);
        m_invoker.ExecuteDo(moveto);
}

▼ 전체 코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    /* COMMAND INTERFACE */
    public interface ICommand
    {
        void ExecuteDo();
        void ExecuteUndo();
    }

    /* INVOKER : COMMAND STACK */
    public class Invoker
    {
        public Invoker()
        {
            m_commands = new Stack<ICommand>();
        }

        public void ExecuteDo(ICommand command)
        {
            if (command != null)
            {
                m_commands.Push(command);
                m_commands.Peek().ExecuteDo();
            }
        }

        public void ExecuteUndo()
        {
            if (m_commands.Count > 0)
            {
                m_commands.Peek().ExecuteUndo();
                m_commands.Pop();
            }
        }

        Stack<ICommand> m_commands;
    }

    public  GameObject  m_player;
    private Invoker     m_invoker;

    /* MOVE_TO : A COMMAND */
    public class CommandMoveTo : ICommand
    {
        public CommandMoveTo(GameManager _gameManager, Vector3 _startPos, Vector3 _destPos)
        {
            m_gameManager   = _gameManager;
            m_destination   = _destPos;
            m_startPosition = _startPos;
        }

        public void ExecuteDo()
        {
            m_gameManager.MoveTo(m_destination);
        }

        public void ExecuteUndo()
        {
            m_gameManager.MoveTo(m_startPosition);
        }

        GameManager m_gameManager;
        Vector3     m_destination;
        Vector3     m_startPosition;
    }

    /* SMOOTH MOVEMENT WITH DELTA_TIME WITH COROUTINE */ 
    public IEnumerator MoveToInSeconds(GameObject _gameObject, Vector3 _endPos, float _second)
    {
        float elapsedTime = 0;
        Vector3 startingPos = _gameObject.transform.position;
        _endPos.y = startingPos.y;
        while (elapsedTime < _second)
        {
            _gameObject.transform.position = Vector3.Lerp(startingPos, _endPos, (elapsedTime / _second));
            elapsedTime += Time.deltaTime;
            yield return null;
        }
        _gameObject.transform.position = _endPos;
    }

    public Vector3? GetLeftClickPosition()
    {
        if (Input.GetMouseButtonDown(0))
        {
            RaycastHit hitInfo;
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray, out hitInfo))
            {
                return hitInfo.point;
            }
        }
        return null;
    }

    private void Start()
    {
        m_invoker = new Invoker();
    }

    private void Update()
    {
        /* LEFT CLICK */
        var leftClickPoint = GetLeftClickPosition();
        if (leftClickPoint != null)
        {
            CommandMoveTo moveto = new CommandMoveTo(this, m_player.transform.position, leftClickPoint.Value);
            m_invoker.ExecuteDo(moveto);
        }

        /* RIGHT CLICK */
        if (Input.GetMouseButtonDown(1))
        {
            m_invoker.ExecuteUndo();
        }
    }

    public void MoveTo(Vector3 pt)
    {
        IEnumerator moveto = MoveToInSeconds(m_player, pt, 0.5f);
        StartCoroutine(moveto);
    }
}

여러 파일로 나누면 블로그에 정리하기가 귀찮아져서 그냥 GameManager.cs 한 파일로 작성했다.

실제로 게임을 개발할 때는 당연히 나눠서 작성해야 할 것이다.

GameManager는 입력처리 말고도 충분히 바쁜 친구니까.

?(Null-Conditional Operator)

C++에는 없는 생소한 연산자인 Null 상태 연산자를 한번 써봤다.

간단히 말하면 C#에서 제공하는 변수형에 null 체크를 할 수 있는 추가적인 기능을 제공한다.

 

위의 코드에서 사용했던걸 보면 이해하기 편하다.

public Vector3? GetLeftClickPosition()
{
    if (Input.GetMouseButtonDown(0))
    {
        RaycastHit hitInfo;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out hitInfo))
         {
            return hitInfo.point;
        }
    }
    return null;
}

마우스 왼쪽 클릭이 들어오는 순간의 위치값을 Vector3로 전달하는 의도를 가진 함수의 코드다.

 

프로그래머라면 언제라도 예외상황이 있을 수 있다고 가정하고 예외처리를 해주는 것이 좋은데 여기서 예상할 수 있는 예외상황은 Vector3가 아닌 null값이 반환되는 경우다.

 

C++이었다면 Vector3로 나올 수 없는 값을 반환하거나 따로 bool값을 사용하거나 해야한다.

상대적으로 늦게 탄생한 것 덕분인지 C#에서는 ?연산자를 통해서 null을 반환할 수 있는 편의 기능이 있다.