명령 패턴이란?
쉽게 설명한다면 '메서드 호출을 실체화 한것'입니다.
실체화는 실체하는 것(인스턴스)을 만든다는 의미이며, 변수에 저장하거나 함수에
전달할 수 있도록 데이터, 즉 객체로 바꿀 수 있다는 걸 의미합니다.
명령 패턴은 '메서드 호출을 객체로 감싸는 것'을 의미합니다. 이는 함수 호출을
객체로 캡슐화하여, 나중에 그 작업을 실행할 수 있도록 합니다. 이 패턴은
콜백과 유사하지만, 콜백은 주로 비동기 상황에서 특정 이벤트를 처리할 때
사용합니다.
명령 패턴을 요약하자면 함수가 하는 명령(커맨드)를 인스턴스로 실체화하여
객체에게 함수가 하는 일을 맡기는 것입니다.
이제 명령 패턴이 어떻게 쓰이는지 알아봅시다.
입력키 변경
콘솔의 입력을 읽는 코드가 있고 그 코드를 호출한다고 가정해봅시다.
간단하게 구현한다면
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();
}
이런식으로 매 프레임 호출해 상태를 바꿔줄 수 있습니다.
코드는 쉽게 이해할 수 있지만, 입력 키 변경이 불가능합니다.
만약 입력 키를 안바꾼다면 상관없지만 수많은 게임들은 키를 바꿀 수 있습니다.
키 변경을 지원한다고 하면 고정된 함수보다 교체 가능한 무엇인가로 바꿔야합니다.
교체 가능한 무언가 -> 이동이 가능한 함수 -> 함수를 실체화해서 변경 -> 명령 패턴
이러한 이유로 명령 패턴이 사용됩니다.
게임에서 할 수 있는 행동을 실행할 수 있는 상위 클래스(Command)를 정의합니다.
그리고 하위 클래스에서 함수를 재정의하여 사용합니다.
// 상위 클래스를 추상 클래스로 설정
class Command {
public:
virtual ~Command() {};
virtual void excute() = 0;
};
// 자식 클래스에서 재정의하여 사용
class SwapWeaponCommand : public Command {
public:
virtual void excute() { jump(); }
};
class FireCommand : public Command {
public:
virtual void execute() { fireGun(); }
};
입력 핸들러 코드는 각 버튼별로 Command 클래스 포인터를 저장합니다.
(포인터로 저장해 입력 핸들링을 자유자재로 하기 위함)
(메모리 누수는 신경 안쓰고 기본적인 디자인 패턴 설계도)
class InputHandler {
public:
void handleInput();
// (생략) 명령 바인드할 메서드들
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();
}
이렇게 코드를 작성하면 직접 함수를 통해 고정된 값으로 전달되는 것이 아닌
함수로 코드를 우회해 더 유연하게 작성할 수 있도록 만들었습니다.
커플링 방지
기본적으로 만든 Command 클래스는 플레이어에서 Jump() 등의 함수를 호출하기
때문에 커플링이 되어있고 상당히 제한적인 느낌을 받습니다.
이렇다보니 Command의 유용성은 매우 떨어지며, 현재 JumpCommand와
SwapweaponCommand는 플레이어만이 사용할 수 있는 클래스가 되어버렸습니다.
이런 제약을 해결하기 위해 함수 인자에 Jump()를 할 수 있는 클래스를 전달해줍니다.
class Command {
public:
virtual ~Command() {}
virtual void execute(Actor& actor) = 0;
};
이로써 Command를 상속받은 클래스는 execute()를 호출할 때 Actor의 객체를
인수로 받기 때문에 원하는 Actor의 메서드를 호출할 수 있습니다.
class LurchCommand : public Command {
public:
virtual void execute(Actor& actor) {
actor.Lurch();
}
};
이렇게 작성하면 어떤 Actor든지 Lurch를 할 수 있게 만든 것입니다.
이제 HandleInput()을 다시 디커플링 상태로 바꿔보죠.
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;
}
다음으로 명령 객체를 받아서 Actor에 코드를 적용시켜보죠.
Command* command = inputHandler.handleInput();
if(command) {
command->execute(actor);
}
일단 이렇게 구현했지만 플레이어 캐릭터 커맨드라면 처음과 달라질 것이 없습니다.
다만 명령 <-- 추상 계승 --> 액터를 반영했기 때문에 게임에 어떤 Actor라도
추상 계층을 타고 넘아가 Command를 입력받을 수 있는 상태가 되는 것입니다.
일반적으로 플레이어가 모든 Actor들을 직접 조종하는 것은 아닙니다.
하지만, AI가 돌(Actor)를 굴려서 떨어뜨린다거나 AI가 AI를 돌격시키는 등의
다양한 인터페이스들을 구성할 수 있습니다. 즉, AI코드에서
원하는 Command 객체를 이용할 수 있습니다.
(상태 패턴과 연계가 된다. 추후에 상태 패턴도 알아보겠습니다.)
Actor를 제어하는 Command를 객체로 만들었기 때문에 메서드를 직접 호출하는
강력한 커플링을 제거해 유용하게 사용할 수 있습니다.
Event queue pattern을 이용해 추상 계층을 하나 더 만들어
명령 패턴을 훨씬 더 다방면화 시킬 수 있습니다.
(추후에 Event queue pattern 도 알아보겠습니다.)
다음 포스팅에서는 명령 패턴의 undo 와 redo를 알아보겠습니다.
'디자인 패턴 > 명령(Command)' 카테고리의 다른 글
Design pattern(1-2) - 명령 패턴(undo, redo) (0) | 2024.08.01 |
---|