일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 29 | 30 | 31 |
- 포인터
- BehaviorTree
- CPP
- 언리얼 엔진5
- cpp개발
- 배열
- effectivec++
- Unreal Engine
- C++
- 생성자
- AI
- C언어
- 언리얼엔진
- 디자인패턴
- 게임개발
- 자료구조
- Unreal
- Unreal Engine5
- 언리얼엔진5
- 프로그래밍
- 게임프로그래밍패턴
- 게임 개발
- UE5
- 언리얼
- 프로세스
- 복사생성자
- 소멸자
- 데이터구조
- 언리얼5
- 복사대입연산자
- Today
- Total
리얼 개발
[게임 디자인 패턴] Part 2. 디자인 패턴 - 명령 패턴 본문
Part 2. 디자인 패턴 다시보기
명령 패턴
요청 자체를 캡슐화하는 것입니다. 이를 통해 요청이 서로 다른 사용자(client)를 매개변수로 만들고, 요청을 대기 시키거나 로깅하며, 되돌릴 수 있는 연산을 지원합니다. (GoF 본문 설명)
GoF의 디자인 패턴에 나와 있는 명령패턴에 대한 설명이다. 너무 난해하게 표현해 받아들이기 쉽지 않다. 저자는 다음과 같이 매우 간결하게 요약했다.
명령 패턴은 메서드 호출을 실체화한 것이다.
‘실체화’는 ‘실제하는 것으로 만든다’라는 뜻이다. 프로그래밍 분야에서는 무엇인가를 ‘일급(first-class)’로 만든다는 뜻으로 통한다.
(*) 일급 객체(First Class Object)란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.
‘실체화’니 ‘일급’이니 하는 말은 어떤 개념을 변수에 저장하거나 함수에 전달할 수 있도록 데이터, 즉 객체로 바꿀 수 있다는 것을 의미한다. 여기에서 명령 패턴을 ‘메서드 호출을 실체화한 것’이라고 한 것은 함수 호출을 객체로 감쌋다는 의미다.
이는 ‘콜백’, ‘일급 함수’, ‘함수 포인터’, ‘클로저’, ‘부분 적용 함수’ 와 비슷한 개념이다. GoF는 책의 뒤에서 ‘명령 패턴은 콜백을 객체지향적으로 표현한 것’이라고 정의한다.
예제) 입력키 변경
모든 게임에는 버튼이나 키보드, 마우스를 누르는 등의 유저 입력을 읽는 코드가 있다. 이런 코드는 입력을 받아서 게임에서 의미 있는 행동으로 전환된다. 정말 간단하게 구현해보자.
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();
}
코드는 쉽게 이해할 수 있지만 입력 키 변경은 불가능하다. 하지만 많은 게임이 키를 바꿀 수 있게 해준다. 따라서 키 변경을 지원하려면 Jump()나 FireGun() 같은 함수를 직접 호출하지 말고 교체 가능한 무엇인가로 바꿔야 한다. 이때 명령 패턴이 등장한다.
게임에서 할 수 있는 행동을 실행할 수 있는 공통 상위 클래스부터 정의한다.
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(); }
};
class SwapWeaponCommand : public Command
{
public:
virtual void Execute() { SwapWeapon(); }
};
class Lurch : public Command
{
public:
virtual void Execute() { LurchInEffectively(); }
};
입력 핸들러 코드에는 각 버튼별로 Command 클래스 포인터를 지정한다.
class InputHandler
{
public:
InputHandler();
void HandleInput();
private:
Command* ButtonX;
Command* ButtonY;
Command* ButtonA;
Command* ButtonB;
};
//사실 생성자 부분은 저자가 생략한 부분이다.
//그래서 읽는데 불편함이 있을 것 같아 추가했다.
InputHandler::InputHandler()
{
ButtonX = new JumpCommand;
ButtonY = new FireCommand;
ButtonA = new SwapWeaponCommand;
ButtonB = new Lurch;
}
이제 입력 처리는 다음 코드로 위임된다.
void InputHandler::HandleInput()
{
if (isPreesed(BUTTON_X)) ButtonX->Execute();
else if (isPreesed(BUTTON_Y)) ButtonY->Execute();
else if (isPreesed(BUTTON_A)) ButtonA->Execute();
else if (isPreesed(BUTTON_B)) ButtonB->Execute();
}
(*) 모든 버튼에 명령 객체가 연결되어 있다고 가정하기 때문에 NULL 검사를 따로 하지 않는다. 아무것도 하지 않는 버튼을 지원하되 NULL 검사는 피하고 싶다면, Execute()가 비어 있는 Command 클래스를 정의한 뒤에 버튼 핸들러 포인터가 NULL 대신 이 객체를 가리키게 하면 된다. 이런 걸 널 객체(Null Object) 패턴이라고 한다.
직접 함수를 호출하던 코드 대신에, 한 겹 우회하는 계층이 생겼다. 여기까지가 명령 패턴의 핵심이다.
하지만!
방금 정의한 Command 클래스에는 Jump()나 FireGun() 같은 전역 함수가 플레이어 캐릭터 객체를 암시적으로 찾아서 꼭두각시 인형처럼 움직이게 할 수 있다는 가정이 깔려있다는 점에서 상당히 제한적이다. 이렇게 커플링이 가정에 깔려 있다 보니 Command 클래스의 유용성이 떨어진다. 제어하려는 객체를 함수에서 직접 찾게 하지 말고 밖에서 전달해주자!
class Command
{
public:
virtual ~Command();
virtual void Execute(GameActor& Actor) = 0;
};
class JumpCommand : public Command
{
public:
virtual void Execute(GameActor& Actor) { Actor.Jump(); }
};
class FireCommand : public Command
{
public:
virtual void Execute(GameActor& Actor) { Actor.FireGun(); }
};
class SwapWeaponCommand : public Command
{
public:
virtual void Execute(GameActor& Actor) { Actor.SwapWeapon(); }
};
class Lurch : public Command
{
public:
virtual void Execute(GameActor& Actor) { Actor.LurchInEffectively(); }
};
Command* InputHandler::HandleInput()
{
if (isPreesed(BUTTON_X)) return ButtonX;
else if (isPreesed(BUTTON_Y)) return ButtonY;
else if (isPreesed(BUTTON_A)) return ButtonA;
else if (isPreesed(BUTTON_B)) return ButtonB;
return nullptr;
}
어떤 액터를 매개변수로 넘겨줘야 할지 모르기 때문에 HandleInput() 에서는 명령을 실행할 수 없다. 여기에서는 명령이 실체화된 함수 호출이라는 점을 활용해서, 함수 호출 시점을 지연한다.
다음으로 명령 객체를 받아서 플레이러를 대표하는 GameActor 객체에 적용하는 코드가 필요하다.
Command* command = inputHandler.HandleInput();
if (command)
{
command->Execute(Actor);
}
이제 명령을 실행할 때 액터만 바꾸면 플레이어가 게임에 있는 어떤 액터라도 제어할 수 있게 되었다. 사실 플레이어가 다른 액터를 제어하는 기능은 일반적이지 않다. 하지만 AI가 다른 캐릭터를 제어하고, 같은 명령 패턴을 AI 엔진과 액터 사이에 인터페이스용으로 사용할 수 있다. 즉, AI코드에서 원하는 Command 객체를 이용하는 식이다.
아래는 실행 가능하도록 내 나름 코드를 추가해보았다.
class GameActor
{
public:
void Jump() {}
void FireGun() {}
void SwapWeapon() {}
void LurchInEffectively() {}
};
class Command
{
public:
virtual ~Command();
virtual void Execute(GameActor& Actor) = 0;
};
class JumpCommand : public Command
{
public:
virtual void Execute(GameActor& Actor) { Actor.Jump(); }
};
class FireCommand : public Command
{
public:
virtual void Execute(GameActor& Actor) { Actor.FireGun(); }
};
class SwapWeaponCommand : public Command
{
public:
virtual void Execute(GameActor& Actor) { Actor.SwapWeapon(); }
};
class Lurch : public Command
{
public:
virtual void Execute(GameActor& Actor) { Actor.LurchInEffectively(); }
};
/*****************************************************************************/
/*****************************************************************************/
#include "Command.h"
enum class InputButton
{
BUTTON_X,
BUTTON_Y,
BUTTON_A,
BUTTON_B
};
class InputHandler
{
public:
InputHandler();
Command* HandleInput();
void SetInputButton(InputButton button);
private:
Command* ButtonX;
Command* ButtonY;
Command* ButtonA;
Command* ButtonB;
InputButton CurrentButton;
};
InputHandler::InputHandler() : CurrentButton(InputButton::BUTTON_X)
{
ButtonX = new JumpCommand;
ButtonY = new FireCommand;
ButtonA = new SwapWeaponCommand;
ButtonB = new Lurch;
}
Command* InputHandler::HandleInput()
{
switch (CurrentButton)
{
case InputButton::BUTTON_X:
return ButtonX;
case InputButton::BUTTON_Y:
return ButtonY;
case InputButton::BUTTON_A:
return ButtonA;
case InputButton::BUTTON_B:
return ButtonB;
default:
return nullptr;
}
}
void InputHandler::SetInputButton(InputButton button)
{
CurrentButton = button;
}
void ApplyActor(GameActor& Actor, InputButton button)
{
InputHandler inputHandler;
inputHandler.SetInputButton(button);
Command* command = inputHandler.HandleInput();
if (command)
{
command->Execute(Actor);
}
}
예제) 실행취소와 재실행
명령 객체가 어떤 작업을 실행할 수 있다면, 이를 실행취소(Undo) 할 수 있게 만드는 것도 어렵지 않다.실행취소 기능은 전략 게임에서 자주 볼 수 있고, 게임 개발 툴에는 필수다.
class Unit
{
public:
void MoveTo(int x, int y) { this->x = x; this->y = y; }
int GetX() { return x; }
int GetY() { return y; }
private:
int x;
int y;
};
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)
: unit_(unit), x_(x), y_(y), xBefore_(0), yBefore_(0) {}
virtual void Execute()
{
xBefore_ = unit_->GetX();
yBefore_ = unit_->GetY();
unit_->MoveTo(x_, y_);
}
virtual void Undo()
{
unit_->MoveTo(xBefore_, yBefore_);
}
private:
Unit* unit_;
int xBefore_;
int yBefore_;
int x_;
int y_;
};
이동을 취소할 수 있도록 이전 위치를 xBefore_, yBefore_ 멤버 변수에 따로 저장 후 Ctrl+Z 와 같은 커맨드가 입력될 시 Undo()를 실행한다.
여러 단계의 실행취소를 지원하는 것도 어렵지 않다. 가장 최근 명령만 기억하는 대신, 명령 목록을 유지하고 ‘현재’ 명령이 무엇인지만 알고 있으면 된다. 유저가 명령을 실행하면, 새로 생성된 명령을 목록 맨 뒤에 추가하고, 이를 ‘현재’ 명령으로 기억하면 된다. 유저가 ‘실행취소’를 선택하면 현재 명령을 실행취소하고 현재 명령을 가리키는 포인터를 뒤로 이동한다. ‘재실행’을 선택하면 포인터를 다음으로 이동시킨 후에 해당 포인터를 실행한다.
(*) ‘재실행’은 게임에서 잘 쓰이지 않을 수도 있지만 ‘리플레이’는 게임에서 자주 쓰인다. 무식하게 구현하자면 매 프레임마다 전체 게임 상태를 저장하면 되겠지만, 메모리를 너무 많이 먹는다. 대신 많은 게임에서는 전체 개체가 실행하는 명령 모두를 매 프레임 저장한다. 게임을 ‘리플레이’할 때는 이전에 저장한 명령들을 순서대로 실행해 게임을 시뮬레이션 한다.
관련자료
- 명령 패턴을 쓰다 보면 수 많은 Command 클래스를 만들어야 할 수 있다. 이럴 때에는 구체 상위 클래스(concrete base class)에 여러 가지 편의를 제공하는 상위 레벨 메서드를 만들어놓은 뒤에 필요하면 하위 클래스에서 원하는 작동을 재정의할 수 있게 하면 좋다. 이러면 명령 클래스의 execute 메서드가 하위 클래스 샌드박스 패턴으로 발전하게 된다.
- 예제에서는 어떤 액터가 명령을 처리할지를 명시적으로 지정했다. 하지만 계층 구조 객체 모델에서처럼 누가 명령을 처리할지가 그다지 명시적이지 않을 수도 있다. 객체가 명령에 반응할 수도 있고 종속 객체에 명령 처리를 떠넘길 수도 있다면 GoF의 책임 연쇄 패턴이라고 볼 수 있다.
- 어떤 명령은 처음 예제에 등장한 JumpCommand 클래스처럼 상태 없이 순수하게 행위만 정의되어 있을 수 있다. 이런 클래스는 모든 인스턴스가 같기 때문에 인스턴스를 여러 개 만들어봐야 메모리만 낭비한다. 이 문제는 경량 패턴을 해결할 수 있다. (싱글톤을 권하지 않음)
'디자인 패턴' 카테고리의 다른 글
[디자인 패턴] ObjectPool Pattern (0) | 2025.01.06 |
---|---|
[게임 디자인 패턴] Part 2. 디자인 패턴 - 옵저버 패턴 (1) | 2024.07.08 |
[게임 디자인 패턴] Part 2. 디자인 패턴 - 경량 패턴 (0) | 2024.07.05 |
[게임 디자인 패턴] Part 1. 도입 (1) | 2024.07.02 |