커맨드(Command) 패턴 for Game (C++)

|

Command 패턴

커맨드(Command) 패턴은 메소드 호출을 객체로 감싼 패턴입니다.

커맨드 패턴을 적용하지 않고 일반적으로 구현한 경우를 살펴보겠습니다.

게임에서 조이패드의 버튼이 X, Y, A, B의 4개가 있다고 하고, 각 버튼을 눌렀을 때의 행동을 처리하는 handleInput() 메소드가 있다면 다음과 같이 구현될 수 있습니다.


InputHandler

void InputHandler::handleInput() {
  if (isPressed(BUTTON_X)) jump();
  else if (isPressed(BUTTON_Y)) attack();
  else if (isPressed(BUTTON_A)) changeWeapon();
  else if (isPressed(BUTTON_B)) dash();
}

단순하게 위와 같은 코드로 구현을 해도 되지만, 많은 게임들이 입력 키의 맵핑을 변경할 수 있는 기능을 제공하고 있습니다.


Command

키 변경 지원을 위해서는 각 명령을 버튼에 직접 할당하는 것이 아닌, 특별한 객체를 이용할 필요가 있습니다. 따라서 다음과 같은 Command 추상 클래스를 선언할 수 있습니다.

class Command {
 public:
  virtual ~Command() {}
  virtual void execute() = 0;
};

이제 각 행동별로 하위 클래스를 만들면 됩니다.

class JumpCommand: public Command {
 public:
  virtual void execute() { jump(); }
};


InputHandler 수정

InputHandler 클래스는 다음과 같이 수정될 것입니다.

class InputHandler {
 public:
  void handleInput();

 private:
  Command *pButtonX;
  Command *pButtonY;
  Command *pButtonA;
  Command *pButtonB;
};

또한 구현부의 handleInput() 메소드도 다음과 같이 변경될 수 있습니다.

void InputHandler::handleInput() {
  if (isPressed(BUTTON_X)) pButtonX->execute();
  else if (isPressed(BUTTON_Y)) pButtonY->execute();
  else if (isPressed(BUTTON_A)) pButtonA->execute();
  else if (isPressed(BUTTON_B)) pButtonB->execute();
}

여기까지가 커맨드 패턴의 핵심 부분입니다.


Actor와 Command 분리하기

위의 코드와 같이 커맨드 패턴을 이용하여 각 키들의 맵핑과 행동간의 분리를 할 수 있었습니다. 하지만, Actor와 Command 간에는 여전히 커플링(Coupling)이 존재합니다. jump(), attack() 등의 행동은 플레이어 객체만 할 수 있습니다.

Actor와 Command를 분리하기 위해서 Command 클래스를 다음과 같이 변경합니다.

class Command {
 public:
  virtual ~Command() {}
  virtual void execute(GameActor &actor) = 0;
};

JumpCommand 클래스는 다음과 같이 변경됩니다.

class JumpCommand: public Command {
 public:
  virtual void execute(GameActor &actor) { actor.jump(); }
};

이제 JumpCommand 클래스는 어떤 캐릭터에게도 적용할 수 있게 되었습니다. handleInput() 메소드는 다음과 같이 변경하여 Command 객체를 리턴하도록 합니다.

Command *InputHandler::handleInput() {
  if (isPressed(BUTTON_X)) pButtonX;
  else if (isPressed(BUTTON_Y)) pButtonY;
  else if (isPressed(BUTTON_A)) pButtonA;
  else if (isPressed(BUTTON_B)) pButtonB;

  return nullptr;
}

이제 GameActor 클래스에는 다음 코드가 필요합니다.

  Command* command = inputHandler.handleInput();
  if(command) {
    command->execute(actor);
  }

이제 명령을 실행할 때 Actor만 변경하면 어떤 Actor라도 제어가 가능하게 되었습니다.


Undo & Redo

Undo 기능과 Redo 기능은 Command 패턴으로 쉽게 구현할 수 있는 대표적인 기능들입니다.

Command들을 리스트로 관리하는 CommandStack을 이용해서 구현할 수도 있고, Command 클래스 자체가 이전 상태를 관리하도록 하고 그 안에 undo() 함수를 넣어서 구현할 수도 있습니다.

CommandStack을 이용하는 방법은 여기에 있습니다.

여기서는 Command 클래스 자체에 이전 상태를 저장하고, undo() 함수도 가지도록 하겠습니다.

class Command {
 public:
  virtual ~Command() {}
  virtual void execute() = 0;
  virtual void undo() = 0;
};
class MoveUnitCommand: public Command {
 public:
  MoveUnitCommand(Unit *unit, int x, int y) : pUnit(unit) {
    prevX(0);
    prevY(0);
    curX(x);
    curY(y);
  }

  virtual void execute() {
    prevX = pUnit->x();
    prevY = pUnit->y();
    pUnit->moveTo(curX, curY);
  }

  virtual void undo() {
    pUnit->moveTo(prevX, prevY);
  }

 private:
  Unit *pUnit;
  int curX, curY;
  int prevX, prevY;
};

하지만, 결국 Undo 기능과 Redo 기능을 제대로 구현하려면 Stack이 필요하긴 합니다. 그래야 현재의 Command 포인터를 효율적으로 관리할 수 있습니다.