커맨드 패턴은 메서드 호출을 실체화 한 것이다.
조금 더 풀어서 설명하자면 객체의 행동을 클래스로 감싸는 것이다.
커맨드 패턴으로 구현할 수 있는 대표적인 예는 '입력키의 변경'과 '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가 된 상태에서 새로운 명령을 행하면 그 이후의 명령목록은 삭제한다.
'🛡️ 디자인 패턴' 카테고리의 다른 글
타입 객체 패턴 (Type Object Pattern) (1) | 2021.09.22 |
---|---|
이벤트 큐 패턴 (Event Queue Pattern) (0) | 2021.08.22 |
컴포넌트 패턴 (Componenet Pattern) (1) | 2021.07.04 |