커맨드 패턴 (Command Pattern)

커맨드 패턴은 메서드 호출을 실체화 한 것이다.

 

조금 더 풀어서 설명하자면 객체의 행동을 클래스로 감싸는 것이다.

커맨드 패턴으로 구현할 수 있는 대표적인 예는 '입력키의 변경'과 'Undo 기능'이 있다.

 

입력키를 구현해보자.

- 유니티에서 A 버튼에 공격, B 버튼에 점프하도록 구현해보자. 

public class Player : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            Attack();
        }
        else if (Input.GetKeyDown(KeyCode.B))
        {
            Jump();
        }
    }
    
    private void Attack(){}
    private void Jump(){}
}

간단하게 위와 같이 구현할 수 있다. 이 정도도 상용코드로 문제가 없고 무리가 없다.

유저가 키를 변경하는 기능이 없다면 말이다.

 

 

키를 변경 가능하게 하자

- 여기서 커맨드 패턴이 사용되면 다음과 같은 코드가 된다.

public class Player : MonoBehaviour
{
    private Command btnACommand;
    private Command btnBCommand;

    private void Awake()
    {
        btnACommand = new CommandAttack();
        btnBCommand = new CommandJump();
    }

    private void Update()
    {
        Command command = GetCommand();
        if (command != null)
            command.Execute(this);
    }

    private Command GetCommand()
    {
        if (Input.GetKeyDown(KeyCode.A)) return btnACommand;
        else if (Input.GetKeyDown(KeyCode.B)) return btnBCommand;
        else return null;
    }

    public void Attack() { }
    public void Jump() { }
}

// 명령을 구현하는 상위 추상 클래스
public abstract class Command
{
    public abstract void Execute(Player p);
}

public class CommandAttack : Command
{
    public override void Execute(Player p)
    {
        p.Attack();
    }
}

public class CommandJump : Command
{
    public override void Execute(Player p)
    {
        p.Jump();
    }
}

본래의 코드보다 상당히 길어진 것을 확인할 수 있다.

 

Command라는 명령을 구현하는 상위 클래스를 만들고, CommandAttack과 CommandJump라는 객체에 상속시킨 뒤,

두 하위 클래스는 Execute()에 키를 눌렀을 때 발생되는 행동을 구현한다.

 

코드에서는 직접적으로 키를 바꾸는 부분은 구현해 놓지않았지만,

Awake()에 있는 Command를 할당하는 부분을 외부에서 다시 할당 할 수 있게해주면

키를 쉽게 바꿀 수 있다는 사실은 분명하다.

 

아 이제 완벽해! 라고 생각하는 순간, 기획팀에서 또 요청이 들어온다.

"전투를 혼자하니까 재미없네요. 3명의 캐릭터를 등장시킨 뒤, 유저가 원할 때 전환되게 해주세요!"

 

 

플레이어블 캐릭터를 변경 가능하게 리팩토링하자

 지금은 Player의 update문에 직접 키를 입력하게 했다.

주인공이 여러명이 되는 경우, Player에서 update문을 받게되면 모든 캐릭터가 동시에 움직이게 될 것이다.

때문에 PlayerController라는 클래스를 만들어서 플레이어 객체에서 인풋을 분리하자.

public class PlayerController : MonoBehaviour
{
    public Player Player { get; set; }

    private Command btnACommand;
    private Command btnBCommand;

    private void Awake()
    {
        btnACommand = new CommandAttack();
        btnBCommand = new CommandJump();
    }
    private Command GetCommand()
    {
        if (Input.GetKeyDown(KeyCode.A)) return btnACommand;
        else if (Input.GetKeyDown(KeyCode.B)) return btnBCommand;
        else return null;
    }

    private void Update()
    {
        Command command = GetCommand();
        if (command != null)
            command.Execute(player);
    }
}

public class Player : MonoBehaviour
{
    public void Attack() { }
    public void Jump() { }
}

플레이어 컨트롤러가 현재 레퍼런스로 가지고있는 단 한개의 player 객체에 명령을 준다.

(역시 전환하는 함수는 직접 구현하진 않았지만, player 변수에 할당하면 전환하는 방식으로 구현하면 된다.)

 

Command 스크립트는 Excute()로 행동을 명령 할 때, 행동하려는 객체의 레퍼런스 p를 같이 넘겨준다.

public abstract void Execute(Player p);

때문에 플레이어객체가 바뀌어도 문제가 없도록 짜여있으니 그대로 사용하여도 된다.

 

 

명령 취소

다음으로 알아볼 커맨드 패턴의 예시는 명령 취소(undo) 이다. 전 단계에서 명령을 Command 객체로 추상화했기 때문에 단계별 취소를 구현하기에 용이하다.

public class MoveCommand : Command
{
    private Player _player;
    private int _x;
    private int _y;
    private int _xBefore;
    private int _yBefore;

    public void MoveCommand(Player p, int x, int y)
    {
        _player = p;
        _x = x;
        _y = y;
    }
    
    public void Execute()
    {
        _xBefore = _player.x;
        _yBefore = _player.y;
        _player.moveTo(_x, _y);
    }
    
    public void Undo()
    {
        _player.moveTo(_xBefore, _yBefore);
    }
}

원래의 상태를 기억하기 위해서 _xBefore, _yBefore 멤버 변수가 추가되었다.

 

 

명령 목록 관리하기

- 매니저 클래스에서는 실행된 명령의 목록을 관리하며, 현재의 명령 인덱스를 기억해야한다.

  (Undo를 하더라도 목록을 유지하여 Redo를 통해 다시 돌아갈 수 있어야한다)

- Undo가 된 상태에서 새로운 명령을 행하면 그 이후의 명령목록은 삭제한다.

 

 

 

댓글

Designed by JB FACTORY