2 분 소요

Intro

Command(명령) 패턴이란 “요청자체를 캡슐화한 것” 이다.

처음엔 잘 와닿지 않았지만, 예제를 보면서 이해가 되었다.

예제

아래는 키 입력을 받는 코드이다.

public class InputHandler : MonoBehaviour
{
    private void Update()
    {
        GetInput();
    }

    private void GetInput()
    {
        if (Input.GetKeyDown(KeyCode.Space)) Jump();                // 스페이스바 : 점프
        else if (Input.GetKeyDown(KeyCode.A)) Attack();             // A : 공격
        else if (Input.GetKeyDown(KeyCode.LeftControl)) Crouch();   // 좌컨트롤 : 웅크리기
        else if (Input.GetKeyDown(KeyCode.LeftShift)) Run();        // 좌쉬프트 : 달리기
    }

    void Jump()
    {
        Debug.Log("점프");
    }

    void Attack()
    {
        Debug.Log("공격");
    }

    void Crouch()
    {
        Debug.Log("웅크리기");
    }

    void Run()
    {
        Debug.Log("달리기");
    }
}

위 코드는 키를 입력받는 부분과 행동을 실행하는 부분이 결합되어있다.

게임에 따라 다르겠지만, 대부분의 게임은 키 변경 옵션을 지원하기때문에

이렇게 코드를 작성하게되면, 키 변경 기능을 만들기 어렵다.

따라서 키 입력을 받았을 때 수행하는 행동을 인스턴스화 해야한다.

명령패턴의 기본 형태


Command 인터페이스는 모든 행동들의 상위 클래스가 되는 인터페이스이다.

public interface Command
{
    void Execute() {}
}

이제 각 행동마다 Command를 상속받은 하위 클래스를 만들어주면 된다.

public class Jump : Command
{
    void Execute()
    {
        Debug.Log("점프");
    }
}

public class Attack : Command
{
    void Execute()
    {
        Debug.Log("공격");
    }
}

public class Crouch : Command
{
    void Execute()
    {
        Debug.Log("웅크리기");
    }
}

public class Run : Command
{
    void Execute()
    {
        Debug.Log("달리기");
    }
}

이렇게 하면 InputHandler 에서는

  1. 입력과 행동 바인딩

  2. 입력을 받아 해당 입력에 매핑된 Command를 실행

두가지만 하면 된다.

public class InputHandler : MonoBehaviour
{
    private Dictionary<KeyCode, Command> buttons = new();

    private void Start()
    {
        buttons.Add(KeyCode.Space, new Jump());
        buttons.Add(KeyCode.A, new Attack());
        buttons.Add(KeyCode.LeftControl, new Crouch());
        buttons.Add(KeyCode.LeftShift, new Run());
    }

    private void Update()
    {
        GetInput();
    }

    private void GetInput()
    {
        if (Input.GetKeyDown(KeyCode.Space)) buttons[KeyCode.Space].Execute();
        else if (Input.GetKeyDown(KeyCode.A)) buttons[KeyCode.A].Execute();
        else if (Input.GetKeyDown(KeyCode.LeftControl)) buttons[KeyCode.LeftControl].Execute();
        else if (Input.GetKeyDown(KeyCode.LeftShift)) buttons[KeyCode.LeftShift].Execute();
    }
}

딕셔너리를 사용해, 입력된 키를 검색하여 그에 매핑된 행동을 실행해주는 방식이다.

하지만 아직도 문제가 있다.

위 방식은 어떤 객체의 Command를 실행할 것인지가 정해져있다.


따라서, 가한 입력을 어느 오브젝트에서 실행할 것인지에 대한 정보를 Execute에서 받아줄 수 있도록 한다.

actor를 인자로 넘겨주는 형태


public interface Command
{
    void Execute(in GameObject actor) {}
}

public class Jump : Command
{
    void execute(in GameObject actor)
    {
        Debug.Log($"{actor.name} 점프");
    }
}

public class Attack : Command
{
    void execute(in GameObject actor)
    {
        Debug.Log($"{actor.name} 공격");
    }
}

public class Crouch : Command
{
    void execute(in GameObject actor)
    {
        Debug.Log($"{actor.name} 웅크리기");
    }
}

public class Run : Command
{
    void execute(in GameObject actor)
    {
        Debug.Log($"{actor.name} 달리기");
    }
}
public class InputHandler : Monobehaviour
{
    // ...
    private GameObject actor;

    private void GetInput()
    {
        if (Input.GetKeyDown(KeyCode.Space)) buttons[KeyCode.Space].Execute(actor);
        else if (Input.GetKeyDown(KeyCode.A)) buttons[KeyCode.A].Execute(actor);
        else if (Input.GetKeyDown(KeyCode.LeftControl)) buttons[KeyCode.LeftControl].Execute(actor);
        else if (Input.GetKeyDown(KeyCode.LeftShift)) buttons[KeyCode.LeftShift].Execute(actor);
    }
}

이렇게 하면

actor를 바꿔주는것으로, 조종할 오브젝트를 변경할 수 있다.

Undo


명령 패턴으로 가능한것이 실행취소(Undo) 기능이다.

메서드를 실체화 시켜놨기 때문에 이를 순서대로 저장할수도 있게된 것이다.

public interface Command
{
    public void Execute() {}
    public void Undo() {}
}

public class MoveUnit : Command
{
    private Transform unit;
    private Vector3 delta;

    public MoveUnit(Transform unit, Vector3 delta)
    {
        this.unit = unit;
        this.delta = delta;
    }

    public void Execute()
    {
        unit.Traslate(delta);
    }

    public void Undo()
    {
        unit.Translate(-1 * delta);
    }
}

InputHandler 에서는 이동 명령을 호출할 때마다 새로운 MoveUnit를 생성하고,

생성된 MoveUnit를 저장함으로써 Undo를 할 수 있게된다.

public class InputHandler : MonoBehaviour
{
    private GameObject actor;

    private void Awake()
    {
        actor = gameObject;
    }

    private void Update()
    {
        GetInput();
    }

    public Command GetInput()
    {
        if(Input.GetKeyDown(KeyCode.UpArrow))
        {
            Command UpArrow = new MoveUnit(actor.transform, Vector3.up * Time.deltaTime);
            return UpArrow;
        }

        return null;
    }
}

방향키를 입력할 때마다 MoveUnit를 생성하고,

이를 스택에 저장하여 Undo를 할 때 가장 최근의 Command를 Pop 하는 방식으로 작업 취소를 한다.

댓글남기기