일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 입방체 매핑
- 장치 초기화
- 노멀 맵핑
- 직교 투영
- InputManager
- Dynamic Indexing
- 게임 프로그래밍
- direct3d
- TCP/IP
- 네트워크 게임 프로그래밍
- effective C++
- 게임 디자인 패턴
- DirectX12
- Direct3D12
- 큐브 매핑
- Render Target
- C++
- 절두체 컬링
- 디퍼드 렌더링
- 게임 클래스
- Deferred Rendering
- gitlab
- FrameResource
- 조명 처리
- 네트워크
- 동적 색인화
- gitscm
- DirectX
- Frustum Culling
- light
- Today
- Total
코승호딩의 메모장
[명령 패턴] 본문
명령 패턴이란 메서드 호출을 실체화한 것이다. 즉, 함수 호출을 객체로 감쌌다는 의미이다. 명령 패턴을 사용할 수 있는 대표적인 예시는 게임 속에서 입력키를 변경하는 것이다.
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)) loadGun();
}
입력을 받아서 게임 속 행동으로 전환하는 것은 가장 간단하게 다음과 같이 구현할 수 있을 것이다. 그러나 많은 게임들은 키를 사용자가 바꿀 수 있게 해준다. 따라서 jump나 fireGun 같은 함수가 아닌 교체 가능한 무언가로 바꿔야 한다.
class Command {
public:
virtual ~Command() {}
virtual void execute() = 0;
}
class JumpCommand : public Command {
public:
virtual void execute() { jump(); }
}
다음과 같이 함수들을 Command라는 공통의 객체로 바꾸고 각 행동별 하위 클래스를 생성한다.
class InputHandler {
public:
void handleInput() {
if (isPressed(BUTTON_X)) buttonX->execute();
if (isPressed(BUTTON_Y)) buttonY->execute();
if (isPressed(BUTTON_A)) buttonA->execute();
if (isPressed(BUTTON_B)) buttonB->execute();
};
private:
Command* buttonX;
Command* buttonY;
Command* buttonA;
Command* buttonB;
}
위와 같이 입력 핸들러 코드에는 각 버튼별로 Command의 포인터를 저장하는 것이다. 이렇게 직접 함수를 호출하던 코드 대신 한 겹 우회하는 계층이 생김으로써 InputHandler의 Command 포인터에 다양한 명령을 바인드하여 사용할 수 있을 것이다.
위 예제는 잘 동작하지만 jump나 fireGun같은 전역 함수가 객체를 찾아야 한다는 점에서 커플링이 깔려 있다고 할 수 있다. 또한 현재 JumpCommand 클래스는 오직 플레이어 캐릭터만 점프하게 만들 수 있다. 따라서 이런 제약을 디커플링하기 위해서 객체를 밖에서 전달해 주도록 한다.
class Command {
public:
virtual ~Command() {}
virtual void execute(GameActor& actor) = 0;
}
class JumpCommand : public Command {
public:
virtual void execute(GameActor& actor) { actor.jump(); }
}
이런식으로 코드를 짜게 된다면 게임에 등장하는 모든 객체에서 이 명령어를 사용할 수 있다. 다음으로 입력 핸들러에서 입력을 받아 객체의 메서드를 호출하는 명령 객체를 연결하도록 한다.
Command* InputHandler::handleInput() {
if (isPressed(BUTTON_X)) return buttonX;
else if (isPressed(BUTTON_Y)) return buttonY;
else if (isPressed(BUTTON_A)) return buttonA;
else if (isPressed(BUTTON_B)) return buttonB;
return nullptr;
}
어떤 액터를 매개변수로 넘겨줘야 할지 모르기 때문에 위 함수에서는 명령을 실행할 수 없고 지연한다.
Command* command = inputHandler.handleInput();
if (command) command->execute(actor);
Scene이나 다른 프레임 워크에서 다음과 같이 명령 객체를 받아서 원하는 액터를 적용하면 된다.
다음은 콘솔 프로그램을 사용하여 간단하게 명령 패턴을 구현해 보자
InputHandler 클래스는 내부적으로 키 변수들을 가지고 있고 생성자에서 각각 커맨드들을 바인드해준다. 키를 변경하고 싶다면 delete를 해주고 새로운 커맨드를 바인드하면 될 것이다. 그리고 Command 클래스에서는 각각의 Command들을 정의하고 Execute 함수에서 각 정의에 맞는 플레이어의 함수를 넣어준다. 만약 플레이어 뿐만 아니라 몬스터도 움직이고 싶다면 플레이어와 몬스터를 상속하는 액터 클래스를 정의하고 액터를 넘겨주면 될 것이다. 그리고 각 함수들은 가상 함수로 선언 및 정의하면 될 것이다.
그리고 키 입력 콜백 함수에서 InputHandler에 어떠한 키가 눌렸는지를 넘겨 주고 리턴 값으로 어떤 키가 눌렸는지를 반환 받는다. 만약 오른쪽 키를 눌렀다면 InputHandler의 mRight가 반환될 것이다. 그리고 반환 받은 명령에 넘기고 싶은 액터를 넘겨 주고 실행을 하면 잘 움직일 것이다. 오른쪽 영상은 위 프로그램을 실행 후 키보드 입력을 통해 움직이는 모습이다.
이렇게 명령 패턴을 사용하면 실행취소 기능도 쉽게 만들 수 있다. 만약 턴제 게임에서 유저의 이동 취소 기능을 추가한다고 하자.
class Command {
public:
virtual ~Command() {}
virtual void execute() = 0;
virtual void undo() = 0;
}
명령 상위 클래스에 undo라는 되돌리기 함수를 생성한다.
class MoveUnitCommand : public Command {
public:
MoveUnitCommand(Unit* unit, int x, int y)
: mUnit(unit), mX(x), mY(y), mBeforeX(0), mBeforeY(0) {
}
virtual void execute() {
mBeforeX = mUnit->GetX();
mBeforeY = mUnit->GetY();
mUnit->move(mX, mY);
}
virtual void undo() {
mUnit->move(mBeforeX, mBeforeY);
}
private:
Unit* mUnit;
int mX, mY;
int mBeforeX, mBeforeY;
}
Command* handleInput() {
Unit* mUnit = GetSelectedUnit();
if (isPressed(BUTTON_UP)){
int destY = mUnit->GetY() - 1;
return new MoveUnitCommand(mUnit, mUnit->GetX(), destY);
}
if (isPressed(BUTTON_DOWN)){
int destY = mUnit->GetY() + 1;
return new MoveUnitCommand(mUnit, mUnit->GetX(), destY);
}
return nullptr;
}
위 코드는 앞서 봤던 액터와 명령 사이를 추상화로 격리 시킨 예제와 다르다. 구체적인 게임에서의 실제 이동을 담고 있으며 앞의 코드는 명령 객체 하나가 매번 재사용되었지만 이번 코드에서는 특정 시점에서 발생될 일을 표현한다는 점에서 좀 더 구체적이다. 입력 핸들러 코드는 플레이어가 이동을 선택할 때마다 명령 인스턴스를 생성한다.
명령1 | 명령2 | 명령3 | 명령4 | 명령5 | 명령6 | 명령7 | 명령8 | 명령9 | 명령10 |
이렇게 인스턴스를 생성하여 명령을 실행한다면 컨테이너를 통해서 명령 목록을 유지할 수 있을 것이다. 위와 같이 undo를 통해 이전 명령으로 돌아간 다음 현재 명령을 가리키는 포인터를 뒤로 보내면 된다. 만약 실행 취소를 하고 새로운 명령을 실행했다면 현재 명령 뒤에 붙어 있는 명령들은 버리면 그만이다.
다음은 콘솔 프로그램을 사용하여 undo(실행 취소)와 redo(재실행)을 구현해보자
위 코드는 Command 클래스에 Undo 명령어를 추가하였고 이를 상속 받는 하위 클래스들을 없애고 특정 시점에서 발생될 일을 HandleInput 함수로 구체적인 실제 이동을 이동시켜줬다. MovePlayerCommand 클래스에서는 이전 위치를 기억하기 위해 mBeforePos변수에 이전 값을 넣어 줬다.
이렇게 이전 코드처럼 원래 있던 Command 객체를 재사용하는 것이 아닌 플레이어가 이동할 때마다 새로운 Command 인스턴스를 생성하여 CommandQueue에 보관함으로써 이전 명령들을 기억할 수 있다.
CommmandQueue에서는 내부적으로 명령들을 보관할 vector와 현재 실행해야 하는 명령의 인덱스를 가지고 있기 때문에 명령이 추가 되었다면 vector에 명령을 추가한다. 주의할 점은 실행할 현재 명령의 인덱스가 가장 최신의 것이 아닌 undo를 통해 전으로 돌아가고 나서 새로운 명령을 추가한 상태라면 명령의 끝에 인덱스를 추가하는 것이 아니라 현재 명령의 다음 인덱스에 추가해야 하고 그 뒤에 있던 명령들은 모두 삭제를 해줘야 한다는 것이다. 또한 Undo는 현재 인덱스의 Undo 실행 명령을 통해 이전으로 돌아가고 인덱스를 1 줄여야 한다. Redo에서는 그냥 현재 인덱스를 1 증가시키고 그 명령을 실행할 뿐이다.
위 영상은 redo와 undo를 사용하여 실행 취소 및 재실행을 사용한 프로그램이다. 생각보다 쉬운 코드로 명령 패턴을 사용한 redo와 undo를 구현할 수 있었다. 게임에서는 플레이어가 제어하는 캐릭터 뿐만 아니라 AI가 제어하는 캐릭터들도 많다. 같은 명령 패턴을 AI 엔진과 액터 사이에 인터페이스용으로도 사용할 수 있다. 이렇게 액터를 제어하는 Command를 객체화한 덕분에 메서드를 직접 호출하는 형태의 강한 커플링을 제거할 수 있었다.
이번 구현한 코드에서는 굳이 Command Queue를 전역이나 다른 FrameWork에서 생성하기 보다는 그냥 인풋 핸들러에 만들면 더 좋지 않았을까 싶다. Command Queue에서도 액터의 정보가 필요하고 인풋 핸들러에서도 액터의 정보가 필요하기 때문이다. 만약 AI 엔진을 만든다면 AI 클래스에서도 Command Queue과 같은 스트림을 내부적으로 가지고 있으면 아마 코드를 짜는데 더 편할 것이다.
이렇게 명령 패턴을 사용하다 보면 Command 클래스가 많아진다. 이럴 때는 구체 상위 클래스에 여러 가지 편의를 제공하는 상위 레벨 메서드를 만들어놓은 뒤 필요하면 하위 클래스에서 원하는 작동을 재정의할 수 있게 하면 좋다. 이렇게 하면 명령 클래스의 execute 메서드를 하위 클래스 샌드박스 패턴으로 발전시킬 수 있다.
또한 JumpCommand 클래스처럼 상태 없이 행위만 정의되어 있는 클래스는 모든 인스턴스가 같기 때문에 인스턴스를 여러 개 만드는것을 다음 나올 경량 패턴으로 해결할 수 있다.
'디자인 패턴 > 게임 프로그래밍 패턴' 카테고리의 다른 글
[관찰자 패턴] (0) | 2023.09.27 |
---|---|
[경량 패턴] (0) | 2023.09.26 |
[게임 프로그래밍 패턴] (0) | 2023.09.25 |