2. 명령 패턴
명령 패턴(command pattern)은 메서드 호출을 실체화(reify)한 것이다. 실체화 혹은 일급(first-class)은 어떤 개념을 변수에 저장하거나 함수에 전달할 수 있도록 데이터, 즉 객체로 바꿀 수 있다는 것을 의미한다. 여기에서 명령 패턴은 메서드 호출을 객체로 만들었다는 의미이다.
2.1 예제: 입력 키 변경
모든 게임에서는 버튼이나 키보드, 마우스를 누르는 등의 사용자 입력을 읽는 코드가 있다. 이런 코드는 입력을 받아서 게임에서 의미 있는 행동으로 전환된다. 입력 단축키를 변경할 수 없게 하려면 다음과 같이 간단하게 구현하면 된다.
void InputHandler::handleInput() {
if (isPressed(BUTTON_X) jump();
else if (isPressed(BUTTON_Y) fireGun();
else if (isPressed(BUTTON_A) swapWeapon();
else if (isPressed(BUTTON_B) lurchIneffectively();
}
하지만 키 변경을 지원하려면 함수를 직접 호출하지 말고 교체할 수 있는 객체를 할당해야 한다. 게임에서 할 수 있는 행동을 공통 상위 클래스로 정의하고, 각 행동별로 하위 클래스를 만든다. 그리고 입력 핸들러 코드에 각 버튼별로 Command 클래스 포인터를 저장하면 입력 처리는 다음 코드로 위임된다.
// 오브젝트가 행동을 실행합니다.
class Command {
public:
virtual ~Command();
virtual void execute() = 0;
};
// 오브젝트가 뜁니다.
class JumpCommand() : public Command {
public:
virtual void execute() override { jump(); }
};
// 오브젝트가 총을 발사합니다.
class FireCommand : public Command {
public:
virtual void execute() override { fireGun(); }
};
class InputHandler {
public:
void handleInput();
private:
Command* _buttonX;
Command* _buttonY;
Command* _buttonA;
Command* _buttonB;
};
// 함수를 한 층 우회해서 호출합니다.
void InputHandler::handleInput() {
if (isPressed(BUTTON_X) _buttonX->execute();
else if (isPressed(BUTTON_Y) _buttonY->execute();
else if (isPressed(BUTTON_A) _buttonA->execute();
else if (isPressed(BUTTON_B) _buttonB->execute();
}
2.2 액터에게 지시하기
방금 정의한 Command 클래스는 jump나 fireGun 같은 전역 함수가 플레이어 캐릭터 객체를 암시적으로 찾아 움직여야 하는 제한이 걸려있다. Command 클래스의 유용성을 확장하려면 제어하려는 객체를 함수에서 직접 찾게 하지 말고 밖에서 전달해준다. Command를 상속받은 JumpCommand 클래스와 FireGunCommand 클래스는 execute()가 호출될 때 GameActor 객체를 인수로 받기 때문에 원하는 액터의 메서드를 호출할 수 있다.
// 게임 객체 클래스 GameActor입니다.
class GameActor {
public:
void jump() { cout << "jump" << endl; }
void fireGun() { cout << "fire gun" << endl; }
};
class Command {
public:
virtual ~Command() = default;
virtual void execute(GameActor& actor) = 0;
};
class JumpCommand : public Command {
virtual void execute(GameActor& actor) override {
actor.jump();
}
};
class FireGunCommand : public Command {
virtual void execute(GameActor& actor) override {
actor.fireGun();
}
};
입력 핸들러에서는 입력을 받아 적당한 객체의 메서드를 호출하는 명령 객체를 연결하기만 한다. handleInput()에서는 명령이 실체화된 함수 호출이라는 점에서 함수 호출 시점을 조금 지연시킨다. 다음으로 명령 객체를 받아 GameActor 객체에 적용하는 코드가 필요하다. 이렇게 추상 계층을 한 단계 둔 덕분에 명령을 실행할 때 액터만 바꾸면 플레이어가 게임에 있는 어떤 액터라도 제어할 수 있게 되었다.
class InputHandler {
public:
Command* handleInput() {
if (isPressed(BUTTON_X)) return _buttonX;
if (isPressed(BUTTON_Y)) return _buttonY;
if (isPressed(BUTTON_A)) return _buttonA;
if (isPressed(BUTTON_B)) return _buttonB;
// 아무것도 누르지 않았다면 아무것도 하지 않습니다.
return nullptr;
}
private:
Command* _buttonX;
Command* _buttonY;
Command* _buttonA;
Command* _buttonB;
};
int main(void) {
// 플레이어 액터입니다.
GameActor player;
InputHandler inputHandler;
Command* command = inputHandler.handleInput();
if (command) {
command->execute(player);
}
return 0;
}
2.3 실행 취소와 재실행
실행 취소(undo) 기능은 명령 패턴을 이용하면 쉽게 만들 수 있다. 싱글 플레이어 턴제 게임에서 이동 취소 기능을 추가한다고 해보자. MoveUnitCommand 클래스는 이동하려는 유닛과 위치 값을 생성자에서 받아 명령과 명시적으로 바인드했다. 이 명령 클래스는 특정 시점에 발생될 일을 표현한다는 점에서 좀 더 구체적이다. 입력 핸들러 코드는 플레이어가 이동을 선택할 때마다 명령 인스턴스를 생성해야 한다.
Command* handleInput(Unit* unit) {
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);
}
if (isPressed(BUTTON_LEFT)) {
int destX = unit->x() - 1;
return new MoveUnitCommand(unit, unit->y(), destX);
}
if (isPressed(BUTTON_RIGHT)) {
int destX = unit->x() + 1;
return new MoveUnitCommand(unit, unit->y(), destX);
}
}
한편, 명령을 취소할 수 있도록 순수 가상 함수 undo()를 정의하자. undo()에서는 execute()에서 변경하는 위치를 반대로 바꿔주면 된다. MoveUnitCommand 클래스에는 이동을 취소하고 이전 위치로 돌아갈 수 있도록 _xBefore, _yBefore 데이터 멤버에 이전 위치를 따로 저장한다.
class Unit {
public:
Unit(int x, int y) : _x(x), _y(y) { };
void moveTo(int x, int y) {
_x = x;
_y = y;
cout << "(" << x << ", " << y << ")\n";
}
int x() { return _x; }
int y() { return _y; }
private :
int _x, _y;
};
class Command {
public:
virtual ~Command() = default;
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() override {
_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;
};
여러 단계의 실행 취소를 지원하려면 명령 리스트를 만들고 사용자가 명령을 실행하면 새로 생성된 명령을 리스트 가장 뒤에 추가한다. 그리고 현재 명령을 포인터로 참조할 수 있도록 기억하면 된다.
2.4 클래스만 좋고 함수형은 별로인가?
C++는 일급 함수를 제대로 지원하지 않고, 함수 포인터와 펑터(functor)도 쓰기가 까다로워서 지금까지 보여준 예제는 클래스만 사용했다. 다른 언어에서 클로저를 제대로 지원해준다면 명령 패턴에 클래스 대신 함수를 써도 된다.
'Computer Science > 디자인 패턴' 카테고리의 다른 글
게임 프로그래밍 패턴 | Chapter 04 관찰자 패턴 (0) | 2022.08.08 |
---|---|
게임 프로그래밍 패턴 | Chapter 03 경량 패턴 (0) | 2022.08.05 |
게임 프로그래밍 패턴 | Chapter 01 구조, 성능, 그리고 게임 (0) | 2022.07.30 |
댓글