Game Development, 게임개발/디자인패턴

Command Pattern, 명령 패턴 [디자인패턴]

게임이 더 좋아 2021. 9. 27. 20:11
반응형
728x170

 

요약

요청 자체를 캡슐화 하는 것으로

서로 다른 클라이언트를 매개변수로 만들고 

요청을 대기시키거나 로깅하고 되돌릴 수 있는 연산을 지원하도록 함.

????

요약인데 알 수 없는 말이다.

 


 

요약을 한답시고 했는데

솔직히 디자인패턴을 처음 본 사람의 입장으로 전혀 와닿지 않는 설명이다.

 

로버트가 표현하길 명령 패턴은 "메서드 호출을 실체화하는 것"이라고 했다.

실체화라는 말은 어떠한 것을 변수에 저장하거나 함수에 전달할 수 있도록 데이터로 만든다.

즉, 메서드를 객체로 바꿀 수 있다는 것을 뜻한다.

 

그렇다면 메서드 호출을 객체로 만들었다는 이야기다.

 


 

예를 들며 살펴보자

게임의 입력을 생각해보자.

우리는 오락실에서 (나는 오락실 세대다)

메탈슬러그에는 기본적으로 A,B,C가 있다. 차례대로 공격, 점프, 수류탄이었다.

 

void InputHandler::handleInput(){
	if( isPressed(BUTTON_A) Attack();
    else if(isPressed(BUTTON_B) Jump();
    else if(isPressed(BUTTON_C) BOMB();
}

 

일반적으로 사용자 입력을 받는 함수는 매 프레임마다 호출되어 사용자의 입력을 감지한다.

이러한 코드는 거의 누구든지 이해할 수 있는 수준이다.

 

하지만 오락실이 아닌 에뮬레이터로 게임을 돌려봤다면

우리는 키보드의 A를 눌렀을 때 꼭 공격이 아니라 점프가 되게 만들 수 있다는 것을 안다.

++현재도 많은 게임에서 키를 커스터마이징하는 것이 가능하다.

 

그렇다면 위에서 어느 부분을 고쳐야 하는가?

 

역시나 그렇다. A,B,C 의 jump 부분을 A에서 실행할 수 있게 고쳐줘야 한다.

그럼 키를 바꿀 때마다 코드를 바꿔줘야하는 것이 정말 말이 안된다.

 

 

이제 명령패턴을 쓸 때가 왔다.

 

게임에서 할 수 있는 행동을 상위 클래스부터 정의해보자

-> 이것이 선행되어야 한다.

 

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

가장 상위 클래스다.

**인터페이스에 반환 값이 없는 메서드 하나밖에 없다면 명령패턴일 가능성이 농후하다.

 

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

 

뭐 이 2가지를 만들었다면 

bomb은 어떻게 구현할 지 예상이 갈 것이다.

하여튼

이렇게 구현하고 InputHandler 코드에는 Command 클래스 포인터를 저장하는 것이다.

-> 정확히 말하면 우리가 Command를 하기위해 Command를 상속받은 객체들이다.

class InputHandler{
public:
    void handleInput();
    // 명령을 바인드, bind 한다. -> 즉, command를 bind 시켜주는 것이다.
    // 이 책을 참고한 모두들이 여기를 그냥 넘어가는데...?
    //해당 객체를 선언하는 것과 같다.
    buttonA_ = new AttackCommand();
    buttonB_ = new JumpCommand();
    buttonC_ = ...
    
    //위와 같이 만들어야 할 것이다.
private:
    Command* buttonA_;
    Command* buttonB_;
    Command* buttonC_;
};

 

이렇게 구현한다면 처음에는 버튼을 직접 할당했던 코드가 이렇게 바뀐다.

++ buttonA_는 AttackCommand이기 execute()를 거쳐서 attack()이 실행될 것이다.

void InputHandler::handleInput(){
	if( isPressed(BUTTON_A) buttonA_ -> execute();
    else if(isPressed(BUTTON_B) buttonB_ -> execute();
    else if(isPressed(BUTTON_C) buttonC_ -> execute();
}

 

즉, 직접 함수를 호출하는 대신에

한 단계를 더 거쳐서 호출을 하는 것이다.

 


 

아니 알겠는데... 키변경때문이야?? 딱 그게 명령패턴을 쓰는 이유야??

 

절대 아니다.

 

우선 앞의 Command 클래스에서는 위 예제만으로 보면 동작이야 잘하겠지만 한계가 있다.

jump나 attack 같은 전역 함수가 플레이어의 캐릭터 객체를 암시적으로 찾아서 움직이게 할 수 있다는 가정이 깔려있다는 것이다.

즉, Coupling이 되어있다고 말할 수 있는데

이렇게 커플링이 발생하면 우리가 유연하게 쓰려고하는 Command 클래스도 그 활용도가 떨어진다.

 

예를 들어 JumpCommand 클래스는 플레이어 캐릭터만 점프하게 만들 수 있다.

이런 제약을 없애기 위해 제어하려는 객체를 함수에서 직접 찾게 하지 말고 밖에서 전달하자.

 

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

 

여기서 GameActor란 게임 월드를 돌아다니는 캐릭터를 대표하는 게임 객체 클래스다.

Command를 상속받은 클래스는 execute가 호출될 때 GameActor 객체를 인수로 받기 때문에 

원하는 Actor에 대한 메서드를 호출할 수 있다.

** 다시 말하면 해당 객체를 받아 명령을 수행할 수 있다는 말이다.

 

 

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

 

JumpCommand로 Actor 객체를 인수로 받아 해당 객체에서의 jump() 커맨드를 실행시키도록 만들었다.

입력핸들러에서 객체의 메서드를 호출하는 명령 객체만 연결하면 된다.

 그것은 아래와 같이 한다.

 

Command* InputHandler::handleInput(){
    if( isPressed(BUTTON_A) return buttonA_;
    else if(isPressed(BUTTON_B) return buttonB_;
    else if(isPressed(BUTTON_C) return buttonC_;
    
    //입력이 없다면
    return NULL;
}

 

Command 객체를 가리키는 포인터를 반환했다면

해당 객체에서 실행시켜야 한다.

어떤 Actor를 매개변수로 넘겨줘야 할지 모르기 때문에 handleInput( )에서 명령을 실행할 수는 없는 것이다.

위의 코드에서는 명령이 "실체화"된 함수 호출임을 이용하여

함수 호출 시점을 지연하고

명령 객체를 받아서 GameActor 객체에 적용하는 것이다.

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

 

그렇다면

actor 가 플레이어 자신이라면

유저 입력에 따라 동작하기 때문에 처음 예제랑 다를 바가 없어보인다.

하지만 명령과 액터 사이의 추상 계층 덕분에 장점이 추가되었다.

** 명령을 실행할 때 액터만 바꾸면 플레이어가 게임에 있는 어떤 액터라도 제어할 수 있게 된 것이다.

 

사실 플레이어가 다른 액터를 제어하는 기능은 없었다.

++메탈슬러그만 봐도 모덴군을 움직일 수는 없다.

Metal Slug 3

하지만 실제로 메탈슬러그 3를 보면 모덴군이 외계인에 맞서 플레이어를 도와준다.

 

즉, AI가 제어하는데, 같은 명령 패턴을 AI 엔진과 액터 사이의 인터페이스로 사용하는 것이다.

AI 코드에서 원하는 Command 객체를 이용하는 식이 되겠다.

 

Command 객체와 선택하는 AI와 이를 실행하는 액터에 대해서 디커플링함으로써 코드가 유연해졌다.

 

예를 들면 기존 AI를 짜 맞춰서 새로운 성향을 만들 수 도 있다.

적을 더 공격적으로 만들고 싶다?

공격 명령을 더욱 잘 실행하면 된다. 또한 일반적인 보스패턴과 같이

액터를 제어하는 Command를 객체로 만들었기 떄문에

메서드를 직접 호출하는 형태의 커플링을 제거했고

명령을 stream이나 queue형태로 만들어 보스 몬스터의 패턴을 구현할 수도 있게 되었다.

 


 

한 번 더 예를 들어서 알아보자

명령 객체가 어떤 작업을 실행할 수 있다면

이를 실행취소할 수 있게 만들 수 있을까?

라는 생각도 든다.

 

실행취소 기능은 원치 않는 행동을 되돌릴 수 있는 전략 게임같은 곳에서 많이 나온다.

 

그냥 실행취소 기능을 구현한다면 굉장히 어려울 수 있지만 명령 패턴을 이용해서 쉽게 만들 수 있다.

싱글플레이어 턴제 게임같은 경우에서 실행취소를 구현해보자 ex) 삼국지

이동 예약도 가능한 반면 이동 취소도 가능하다.

 

어떤 유닛을 옮기는 명령을 생각해보자

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 클래스는 처음의 예제와 다르다. 

처음 예제에서는 명령을 변경하려는 액터와 명령 사이를 추상화로 격리시켰지만

여기서는 이동하려는 유닛과 위치 값을 생성자에게서 받아서 명령과 명시적으로 바인드, bind 했다.

(이 부분 Unit_ -> moveTo(x_, y_) )

MoveUnitCommand 명령 인스턴스는 "무엇인가를 움직이는" 보편적인 작업이 아니라 

게임에서의 구체적인 실제 이동을 하는 것이다.

 

이는 앞서 보여줬던 명령 패턴의 변형버전이라고 생각하면 되겠다.

처음의 명령 패턴의 경우

어떤 일을 하는지를 정의한 명령 객체 하나가 매번 재사용되었다.

** 이 부분 buttonA_ = new AttackCommand();

계속 execute()가 되어 attack()만 재사용되었다.

 

입력 핸들러에서 특정 버튼이 눌릴 때마다 연결된 명령 객체의 execute를 호출했다.

 

이번에 만든 명련 패턴의 경우

특정 시점에 발생될 일을 표현한다는 점에서 구체적이었다.

다시 말해서 입력 핸들러에서는 플레이어가 이동을 선택할 때마다 명령 인스턴스가 생기는 것이다.

** return new MoveUnitCommand(...) 가 생김 -> 해당 생성자(...) 가 다르면 다른 명령임

unit이 다르면 움직이는 객체가 다를 것이고 x가 다르면 움직이는 위치가 다를 것이다.다른 명령이 되는 것이다.

 

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), x_(x), y_(y)
    {    }
    
    
    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_;
};

 

플레이어가 만약 이동을 취소한다면

이동을 취소할 수 있도록 이전의 위치를 저장하고 있어야 한다.

여러 단계의 명령취소도 가능하다.

다만 명령 스택목록과 같이 최근 명령부터 오래된 명령까지 저장되어있는 경우에나 가능하겠다.

**스택으로 명령을 LIFO 해야하는 이유는.. 가장 최근 것을 되돌려야 하기 때문이다.

**명령 취소의 취소도 가능하다. 재실행이라는 이름으로 쓰인다.

실제 게임에서는 잘 쓰이진 않지만 **리플레이에서 쓰인다.

 

명령패턴으로 

실행, 실행취소에 관한 기능을 구현할 수 있었다.

명령을 통해서 데이터 변경을 하기 때문에 처음엔 불편하겠지만 유연한 코드가 되었다.

 


 

마지막으로 다시 정리해보자

 

어떠한 메서드가 객체로 캡슐화되었고

캡슐화 되었기 때문에 다른 클래스에 의존하지 않고 

행동 단위로 나누어 생각해서 다양한 객체에 활용할 수 있게 되었다.

즉, 실행될 기능을 캡슐화함으로써 재사용성이 높은 클래스를 설계할 수 있다.

반응형
그리드형