메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

게임 프로그래밍 패턴

디자인 패턴 다시 보기: 명령 패턴(2/2)

한빛미디어

|

2016-06-03

|

by 로버트 나이스트롬

22,517

4.1. 실행취소와 재실행

이번 마지막 예는 명령 패턴 사용 예 중에서도 가장 잘 알려져 있다. 명령 객체가 어떤 작업을 실행할 수 있다면, 이를 실행취소undo할 수 있게 만드는 것도 어렵지 않다. 실행취소 기능은 원치 않는 행동을 되돌릴 수 있는 전략 게임에서 볼 수 있다. 게임 개발 툴에는 필수다. 레벨  에디터에 실행취소 기능을 제공해주지 않는 것만큼 확실하게 기획자가 당신을 미워하게 만들 방법은 없다. 

그냥 실행취소 기능을 구현하려면 굉장히 어렵지만, 명령 패턴을 이용하면 쉽게 만들 수 있다. 싱글플레이어 턴제 게임에서 유저가 어림짐작보다는 전략에 집중할 수 있도록 이동 취소 기능을 추가한다고 해보자.

이미 명령 객체를 이용해서 입력 처리를 추상화해둔 덕분에, 플레이어 이동도 명령에 캡슐화되어 있다. 어떤 유닛을 옮기는 명령을 생각해보자.

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_;

  int y_;

};

 

MoveUnitCommand 클래스는 이전 예제와 약간 다르다. 이전 예제에서는 명령에서 변경하려는 액터와 명령 사이를 추상화로 격리시켰지만 이번에는 이동하려는 유닛과 위치 값을 생성자에서 받아서 명령과 명시적으로 바인드했다. MoveUnitCommand 명령 인스턴스는 '무엇인가를 움직이는' 보편적인(언제든지 써먹을 수 있는) 작업이 아니라 게임에서의 구체적인 실제 이동을 담고 있다.

이는 명령 패턴 구현을 어떻게 변형할 수 있는지 잘 보여준다. 처음 예제 같은 경우, 어떤 일을 하는지를 정의한 명령 객체 하나가 매번 재사용된다. 입력 핸들러 코드에서는 특정 버튼이 눌릴 때마다 여기에 연결된 명령 객체의 execute()를 호출했었다.

이번에 만든 명령 클래스는 특정 시점에 발생될 일을 표현한다는 점에서 좀 더 구체적이다. 이를테면, 입력 핸들러 코드는 플레이어가 이동을 선택할 때마다 명령 인스턴스를 생성해야 한다. 

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);

  }

  // 다른 이동들...

  return NULL;

}

 

Command 클래스가 일회용이라는 게 장점이라는 걸 곧 알게 될 것이다. 명령을 취소할 수 있도록 순수 가상 함수 undo()를 정의한다. 

class Command {

public:

  virtual ~Command() {}

  virtual void execute() = 0;

  virtual void undo() = 0;

};

 

undo()에서는 execute()에서 변경하는 게임 상태를 반대로 바꿔주면 된다. MoveUnitCommand 클래스에 실행취소 기능을 넣어보자.

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_;

};

 

잘 보면 MoveUnitCommand 클래스에 상태 가 몇 개 추가되었다. 즉 유닛이 이동한 후에는 이전 위치를 알 수 없기 때문에, 이동을 취소할 수 있도록 이전 위치를 xBefore_, yBefore_ 멤버변수에 따로 저장한다.

플레이어가 이동을 취소할 수 있게 하려면 이전에 실행했던 명령을 저장해야 한다. 우리가 Ctrl+Z를 막 누르고 있을 때, 계속 이전 명령의 undo()가 실행되고 있는 것이다(혹은 이미 실행취소했다면 redo()를 호출해 명령을 재실행한다).

여러 단계의 실행취소를 지원하는 것도 그다지 어렵지 않다. 가장 최근 명령만 기억하는 대신, 명령 목록을 유지하고 '현재' 명령이 무엇인지만 알고 있으면 된다. 유저가 명령을 실행하면, 새로 생성된 명령을 목록 맨 뒤에 추가하고, 이를 '현재' 명령으로 기억하면 된다.

 

1.png

▲ 실행취소 스택 따라가기

(OLDER: 오래된 명령, CMD: 명령, UNDO: 실행취소, CURRENT: 현재, REDO: 재실행, NEWER: 최근 명령)

 

유저가 '실행취소'를 선택하면 현재 명령을 실행취소하고 현재 명령을 가리키는 포인터를 뒤로 이동한다. '재실행' 을 선택하면, 포인터를 다음으로 이동시킨 후에 해당 포인터를 실행한다. 유저가 몇 번 '실행취소'한 뒤에 새로운 명령을 실행한다면, 현재 명령 뒤에 새로운 명령을 추가하고 그다음에 붙어 있는 명령들은 버린다.

처음으로 명령 패턴을 이용해 레벨 에디터에 다중 취소 기능을 추가한 날은 천재가 된 기분이었다. 명령 패턴이 얼마나 쉽고 제대로 작동하던지 크게 감동을 받았다. 명령을 통해서만 데이터 변경을 가능하게 만들어야 하기 때문에 손이 좀 가긴 하지만, 한번 만들어놓고 나면 나머지는 어려울 게 없다.

 

 

4.2. 클래스만 좋고, 함수형은 별로인가?

앞에서 명령은 일급 함수나 클로저와 비슷하다고 했지만, 지금까지 보여준 예제에서는 전부 클래스만 사용했다. 함수형 언어에 익숙한 독자라면 함수 얘기를 왜 안 하는지 궁금할 것이다.

예제를 이렇게 만든 이유는 C++이 일급 함수를 제대로 지원하지 않기 때문이다. 함수 포인터에는 상태를 저장할 수 없고, 펑터functor는 이상한 데다가 여전히 클래스를 정의해야 한다. C++11에 도입된 람다는 메모리를 직접 관리해야 하기 때문에 쓰기가 까다롭다.

그렇다고 다른 언어에서도 명령 패턴에 함수를 쓰면 안 된다는 얘기는 아니다. 언어에서 클로저를 제대로 지원해준다면 안 쓸 이유가 없다! 어떻게 보면 명령 패턴은 클로저를 지원하지 않는 언어에서 클로저를 흉내 내는 방법 중 하나일 뿐이다. 

자바스크립트로 게임을 만든다면 유닛 이동 명령을 다음과 같이 만들 수 있다.

function makeMoveUnitCommand(unit, x, y) {

  // 아래 function이 명령 객체에 해당한다:

  return function() {

    unit.moveTo(x, y);

  }

}

 

클로저를 여러 개 이용하면 실행취소도 지원할 수 있다.

function makeMoveUnitCommand(unit, x, y) {

  var xBefore, yBefore;

  return {

    execute: function() {

      xBefore = unit.x();

      yBefore = unit.y();

      unit.moveTo(x, y);

    },

    undo: function() {

      unit.moveTo(xBefore, yBefore);

    }

  };

}

 

함수형 프로그래밍에 익숙한 독자라면 다 아는 얘기겠지만, 그렇지 않은 독자에게는 조금이나마 도움이 되었으면 한다. 내가 볼 때 명령 패턴의 유용성은 함수형 패러다임이 얼마나 많은 문제에 효과적인지를 보여주는 예이기도 한다.

 

 

4.3. 관련자료

 * 명령 패턴을 쓰다 보면 수많은 Command 클래스를 만들어야 할 수 있다. 이럴 때에는 구체 상위 클래스concrete base class에 여러 가지 편의를 제공하는 상위 레벨 메서드를 만들어놓은 뒤에 필요하면 하위 클래스에서 원하는 작동을 재정의할 수 있게 하면 좋다. 이러면 명령 클래스의 execute 메서드가 [[하위 클래스 샌드박스 패턴]]으로 발전하게 된다.

 * 예제에서는 어떤 액터가 명령을 처리할지를 명시적으로 지정했다. 하지만 계층 구조 객체 모델에서처럼 누가 명령을 처리할지가 그다지 명시적이지 않을 수도 있다. 객체가 명령에 반응할 수도 있고 종속 객체에 명령 처리를 떠넘길 수도 있다면 ‘책임 연쇄Chain of Responsibilty’ 패턴이라고도 볼 수 있다.

 * 어떤 명령은 처음 예제에 등장한 JumpCommand 클래스처럼 상태 없이 순수하게 행위만 정의되어 있을 수 있다. 이런 클래스는 모든 인스턴스가 같기 때문에 인스턴스를 여러 개 만들어봐야 메모리만 낭비한다. 이 문제는 [[경량 패턴]]으로 해결할 수 있다.

댓글 입력
자료실

최근 본 상품0