명령은 구체화된 메서드 호출입니다.
GOF(Gang of Four) - 명령은 콜백을 대체하는 객체 지향입니다.
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(); }
해당 함수는 게임 루프(Game Loop)에 의해 프레임당 한 번 호출된다.
해당 코드는 사용자 입력을 게임 작업에 hard-wire형식으로 작동하지만 많은 게임에서는 사용자가 버튼 매핑 방법을 구성 할 수 있다.
이를 지원하려면 jump() 및 fireGun()에 대한 직접 호출을 교체할 수 있는 것으로 전환해야 한다.
Swapping out은 변수를 할당하는 것과 비슷하기에 게임 동작을 나타내는데 사용할 수 있는 객체가 필요하다. Enter : Command Pattern
- 클래스 정의
class Command { public: virtual ~Command() {} virtual void execute() = 0; };
- 각기 다른 작업(사용자 조작 명령)에 대한 하위 클래스를 만든다.
// 점프 명령 class JumpCommand : public Command { public: virtual void execute() { jump(); } }; // 발사 명령 class FireCommand : public Command { public: virtual void execute() { fireGun(); } }; // You get the idea...
- 입력 처리기에서 각 버튼에 대한 명령을 포인터로 저장한다.
class InputHandler { public: void handleInput(); // Methods to bind commands... 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(); }
- 이를 통해 각 입력이 직접 호출시 간접 참조 계층을 통해 실행된다.
class Command { public: virtual ~Command() {} virtual void execute(GameActor& actor) = 0; };
- GameActor라는 게임 세계의 캐릭터를 나타내는 "게임 객체"클래스가 존재한다.
- excoute()라는 명령이 다음과 같이 선택한 캐릭터에 대한 메서드를 호출할 수 있도록 전달한다.
class JumpCommand : public Command { public: virtual void execute(GameActor& actor) { actor.jump(); } };
- 이제 이 클래스 하나를 사용하여 게임의 어떤 캐릭터를 뛰어다니게 할 수 있다.
- 입력 처리기와 명령을 받아 올바른 개체에서 호출하는 부분이 누락되어 있다.
- 먼저 handleInput()에 명령을 반환할 수 있게 변경한다.
Command* InputHandler::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_; // Nothing pressed, so do nothing. return NULL; }
- 어떤 액터를 전달할지 모르기 때문에 명령을 즉시 실행할 수가 없다. 여기에서 명령이 구체화된 호출이라는 사실을 활용한다 -> 호출이 실행될 때 지연시킬 수 있다.
- 그런 다음 해당 명령을 받아 플레이어를 나타내는 액터에서 실행하는 코드가 아래와 같이 필요하다.
Command* command = inputHandler.handleInput(); if (command) { command->execute(actor); }
- 액터가 플레이어의 캐릭터에 대한 참조라고 가정하면 사용자의 입력에 따라 플레이어를 올바르게 구동하므로 첫 번째 예에서와 동일한 동작으로 돌아간다.
- 그러나 명령과 명령을 수행하는 캐릭터 사이에 간접적인 레이어를 추가하면 약간의 능력이 생긴다.
- 이제 명령을 실행하는 캐릭터를 변경하여 플레이어가 게임의 캐릭터를 제어하도록 할 수 있다.
- AI와 Command를 수행하는 actor 코드 사이의 분리는 유연성을 제공한다.
- Command를 선택하는 AI와 Command를 수행하는 actor 코드 사이의 분리는 우리에게 많은 유연성을 준다.
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_; };
- Input handler를 추상화하여 Command를 편리하게 사용하기 위해 플레이어의 움직임(move)를 Command 캡슐화 되어있다.
- 해당 코드는 유닛의 이동.
- 이전의 명령은 수정한 actor에서 Command를 추상화하여 이동 중인 유닛에 바인딩 하려 하였다. 이는 여러 컨텍스트에서 사용할 수 있는 일반적인 이동 작업이 아닌 게임의 턴 순서에서 구체적인 동작이다.
- 이는 Command Pattern이 구현되는 방식의 변화를 강조한다.
- 여기서 Command는 더 구체적으로 특정한 시점에 할 수 있는 것을 나타내며 이것은 다음과같이 플레이어가 동작을 선택할 때마다 Input Handling 코드가 이것의 Instance를 생성한다는 것을 의미한다.
Command* handleInput() { Unit* unit = getSelectedUnit(); if (isPressed(BUTTON_UP)) { // Move the unit up one. int destY = unit->y() - 1; return new MoveUnitCommand(unit, unit->x(), destY); } if (isPressed(BUTTON_DOWN)) { // Move the unit down one. int destY = unit->y() + 1; return new MoveUnitCommand(unit, unit->x(), destY); } // Other moves... return NULL; }
- Command가 한 번만 사용된다는 사실은 곧 이점이 될 것이다.
- 명령을 실행 취소할 수 있도록 하려면 각 Command 클래스에서 구현해야 하는 다른 작업을 정의한다.
class Command { public: virtual ~Command() {} virtual void execute() = 0; virtual void undo() = 0; };
- undo() 메서드는 해당 execute() 메서드에 의해 변경된 게임 상태를 되돌린다.
class MoveUnitCommand : public Command { public: MoveUnitCommand(Unit* unit, int x, int y) : unit_(unit), xBefore_(0), yBefore_(0), x_(x), y_(y) {} virtual void execute() { // Remember the unit's position before the move // so we can restore it. xBefore_ = unit_->x(); yBefore_ = unit_->y(); unit_->moveTo(x_, y_); } virtual void undo() { unit_->moveTo(xBefore_, yBefore_); } private: Unit* unit_; int xBefore_, yBefore_; int x_, y_; };
- 클래스에 더 많은 state를 추가해줌으로써 유닛이 움직일때 이전의 위치(xBefore, yBefore)를 기록하여 해당 동작을 취소하고 싶을때 돌아갈 수 있도록 허용해준다.
- 플레이어가 동작을 취소할 수 있도록 마지막으로 실행한 명령을 유지한다.
- Ctrl-z를 누르면 우리는 그 명령의 undo()메서드를 호출한다. (이미 실행 취소한 경우 다시 실행이 되어 명령을 다시 실행한다.)
- 마지막 Command를 기억하는 대신 Command 목록과 현재 Command에 대한 참조를 유지함으로써 플레이어가 Command를 실행할 때 목록에 추가하고 "현재"를 가리킨다.
- 플레이어가 Undo를 선택하면 현재 Command를 실행 취소(undo)하고 현재 포인터를 뒤로 이동한다.
- redo할 때, 포인터를 전진시킨 다음 그 Command를 실행한다.
- 일부 Command를 실행 취소한 후 새 Command를 선택하면 현재(돌아간 Command)Command 이후의 목록에 있는 모든 항목이 삭제된다.