*아래 내용은 로버트 나이스트롬의 '게임 프로그래밍 패턴'을 토대로 정리한 글입니다.
명령 패턴
디자인 패턴 중 '명령패턴' 이라는 것이 있다. 책에서는 이를
명령 패턴은 메서드 호출을 실체화한 것이다.
라고 간결하게 정리했다.
'실체화'라는 것은 무엇인가를 일급(First-class)로 만든다는 뜻이다. 그런데 '일급'은 또 무엇일까?
일급 객체(first-class object)라고 부르는 것은 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.
보통 함수에 인자로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다. - 위키백과-
파이썬이나 자바스크립트와 같은 언어에서 나타나는 특징으로, 일급 함수를 예로 들 수 있다.
함수를 변수에 저장하거나, 함수를 매개변수로 전달하거나, 함수의 리턴 값으로 함수를 줄 수 있다.
def add(a, b):
return a + b
def add2(add, a, b, c):
return add(a, b) + c
func = add
print(add(1, 2))
print(func(1, 2))
print(add)
print(func)
print(add2(add, 1, 2, 3))
일급 객체에 대한 자세한 내용은 링크를 참조하면 좋을 것 같다.
다시 돌아와서,
명령 패턴은 메서드 호출을 실체화한 것이다 -> 명령 패턴은 메서드 호출을 일급 객체로 만드는 것이다 -> 메서드 호출을 객체로 감싼다
예시1. 키변경
예를 들어, 플레어어가 A키를 누르면 공격하는 코드를 작성했다고 하자.
void InputHandler::handleInput(){
if(isPressed(button_A)) attack();
}
위 예시에서는 A키를 누르면 attack()이라는 함수를 호출한다.
그런데 공격을 A키가 아닌 다른 키로 바꾸고 싶으면 어떻게 해야 할까?
보통 대부분의 게임에서는 플레이어가 키매핑을 바꿀 수 있도록 되어있다.
하지만 위 코드에서 A키 대신 다른 키로 수정하지 않는 이상, 불가능할 것이다.
이러한 코드는 유연하지 않다. 따라서 우리는 attack()이라는 함수를 객체로 바꿀 것이다.
구현
아래와 같이 Command라는 이름의 상위 클래스를 정의한다.
class Command{
public:
virtual ~Command() {}
virtual void execute() = 0;
};
이 클래스로부터 하위 클래스를 만들어 여러 명령을 나타내도록 한다.
class AttackCommand : public Command{
public:
virtual void execute() { attack(); }
};
class JumpCommand : public Command{
public:
virtual void execute() { jump(); }
};
// 등등..
이렇게 만든 클래스를 객체로 만들어서 관리한다.
class InputHandler{
public:
void handleInput();
private:
Command* button_a;
Command* button_b;
// 등등..
AttackCommand* command_attack;
};
//...
command_attack = new AttackCommand();
button_a = &command_attack;
우리는 이제, attack()이라는 명령을 직접 호출하지 않고 객체에게 명령할 것이다.
void InputHandler::handleInput(){
if(isPressed(BUTTON_A)) button_a->execute();
}
만약에 키를 A에서 B로 바꾸고 싶으면? 우리는 button_b라는 객체에 공격 커맨드를 할당하면 된다.
button_a = nullptr;
button_b = &commnad_attack;
예시2. 디커플링
위 예시에서 키변경은 되었으나, 한 가지 문제점은 Command 클래스의 유용성이다.
AttackCommand의 attack() 함수는 플레이어 캐릭터 객체를 찾아서 공격하게 한다.
그러나 게임에는 보통 적이 존재해 마찬가지로 플레이어를 공격할 수 있다.
적이랑 플레이어에게 모두 Command 클래스를 사용하면 코드가 줄어들고 일반적인 좋은 코드가 될 것이다.
즉, 명령을 하는 존재와 명령을 받는 존재가 연결(커플링)되어 있으므로, 이를 해제(디커플링)해 구분지어 놓아야 한다.
구현
먼저 Command 클래스의 execute 함수의 매개변수로 행동을 할 액터를 넘겨받는다.
이후 우리는 InputHandler에서 입력을 받아 알맞는 명령 객체를 연결해야 한다.
이때 InputHandler에서는 액터를 넘겨받지 않는다.
우리는 위에서 일급객체에 대해 간단하게 알아보았다. 그리고 그 조건 중 하나로,
"...변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다."
우리가 만든 InputHandler는 void, 아무것도 반환하지 않는다. 우리는 이것을 Command 객체를 반환하도록 바꿀 것이다.
InputHandler에서는 입력받은 키에 따라 알맞는 명령 객체를 반환하고, 이후 명령을 실행한다.
Command& InputHandler::handleInput(){
if(isPressed(BUTTON_A)) return button_a;
if(isPressed(BUTTON_B)) return butotn_b;
}
//...
Command* command = inputHandler.handleInput();
if(command){
command->execute();
}
이렇게 함으로써, 명령을 하는 부분(InputHandler) - 명령을 받는 부분(액터)로 나누게 되었다.
우리는 추가로 이 명령들을 큐나 스트림으로 만들어 전달하는 것을 생각할 수 있다.
InputHandler에서 명령 객체를 만들어 스트림에 넣으면, 액터에서는 스트림에서 명령 객체를 받아서 호출한다.
예를 들어, 멀티 게임에서 한 플레이어가 입력을 했을 때 명령 객체를 스트림에 넣어 서버로 전달하고, 서버에서 다시 각 플레이어에게 전달해 호출하는 과정을 떠올릴 수 있다.
예시 3. 실행취소, 재실행
명령 객체가 어떤 행동을 실행할 수 있으므로, 반대로 실행취소할 수도 있다.
예시로, 어떤 유닛을 옮기는 명령을 생각해보자.
class MoveUnitCommand : public Command{
public:
MoveUnitCommand(Unit* unit, int x, int y) :
unit_(unit),
x_(x),
y_(y) {
}
virtual void execute(){
unit_->moveTo(x_, y_);
}
private:
Unit* unit_;
int x_, y_;
};
위 코드에서는 Unit 클래스 안에 멤버변수로 바인드되어 있다.
이는 MoveUnitCommand 객체가 무언가를 움직이는 '보편적인' 명령이 아닌 실제 이동을 하는 '구체적인' 행동을 담고 있기 때문이다.
또한 이전과 달리, MoveUnitCommand는 유저가 이동을 선택할 때마다 생성해야 하는 클래스다.
왜냐하면 특정 시점에 어떤 '유닛'을 어느 '위치'로 옮겼는지 그 때마다 가지고 있어야 하기 때문이다.
구현
따라서 handleInput에서는 다음과 같은 코드를 작성한다.
Command* handleInput(){
Unit* unit = getSelectedUnit();
if(isPressed(BUTTON_UP)){
int destY = unit->y() - 1;
return new MoveUnitCommand(unit, unit->x(), destY);
}
if(isPressed(BUTTON_DOWN)){
int destY = unit->y() + 1;
return new MoveUnitCommand(unit, unit->x(), destY);
}
// 등등..
}
실행 취소 undo() 순수 가상 함수를 정의하고 만들고, 이를 위해 MoveUnitCommand의 이전 x,y 좌표를 가지는 멤버변수를 추가한다.
class Command{
public:
virtual void ~Command();
virtual void execute() = 0;
virtual void undo() = 0;
}
class MoveUnitCommand : public Command{
public:
MoveUnitCommand(Unit* unit, int x, int y) :
unit_(unit),
x_(x),
y_(y),
xBefore(0),
yBefore(0) {
}
virtual void execute(){
xBefore = unit->x();
yBefore = unit->y();
unit_->moveTo(x_, y_);
}
virtual void undo(){
unit_->moveTo(xBefore, yBefore);
}
private:
Unit* unit_;
int x_, y_;
int xBefore, yBefore;
};
이렇게 하면은 아래 그림처럼 여러 개의 명령 목록이 있을 때
- 실행 취소는 해당 명령 객체의 undo() 호출
- 실행은 해당 명령 객채의 execute() 호출
- 재실행은 해당 명령 객체의 redo() 호출
이렇게 각 명령을 포인터로 가리키면서 움직이면 실행취소, 재실행을 쉽게 만들 수 있다.
위 예시들은 모두 C++을 기준으로 작성되었는데, 일급 함수를 지원하는 자바스크립트 등으로 작성하면 클래스 대신 함수형으로 만들 수도 있다.