Command Pattern(명령 패턴)

2024. 6. 6. 11:20Game Develop/Design Pattern

명령 패턴은 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();
}

 

위 코드로 각 버튼 마다 특정 행위를 할 수 있도록 구현했지만, 사용자 입맛에 맞게 키변경을 할 순 없을 것이다.

많은 게임들이 입력키 변경을 지원하기 때문에 위의 코드로는 한계가 있다.

따라서 키 변경을 지원하기 위해선 메서드를 직접 호출하는 것이 아닌 게임 행동을 나타내는 객체가 필요하다.

 

따라서 입력키 변경이 가능한 코드로 다시 작성을 해주도록 하겠다.

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 SwapCommand : public Command{
public:
    virtual void execute() { swapWeapon(); }
};

class LurchCommand : public Command{
public:
    virtual void execute() { lurchIneffectively(); }
};

 

 

 

입력 핸들러 코드를 만들어 각 버튼별로 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 클래스의 유용성이 떨어진다. 따라서 이러한 제약을 유연하게 만들기 위해 제어하려는 객체를 함수에서 직접 찾게하지 말고 밖에서 전달해주면 제어 결합도로 결합도를 낮출 수 있다.

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

JumpCommand 클래스 하나로 어떤 객체든 점프하도록 할 수 있다. 입력 핸들러에서 입력을 받아 적당한 객체의 메서드를 호출하는 명령 객체를 연결하는 코드만 작성해주면 된다.

Command* InputHandler::handleInput{
    if(isPressed(BUTTON_X) return BUTTON_X;
    if(isPressed(BUTTON_Y) return BUTTON_Y;
    if(isPressed(BUTTON_A) return BUTTON_A;
    if(isPressed(BUTTON_B) return BUTTON_B;
    
    // 아무것도 누르지 않았다면, 아무것도 하지 않는다.
    return NULL;
}

 

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

따라서 명령 객체를 받아 플레이어를 대표하는 GameActor 객체에 적용하는 코드가 필요하다.

 

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

 

게임 객체스크립트에 위의 코드를 추가해주기만 하면, 플레이어는 자신의 캐릭터 뿐만 아니라 액터만 바꿔주면 플레이어가 게임에 있는 어떤 액터라도 제어할 수 있다.

 

최근에 핫한 그랑블루라는 게임을 예로들어 설명하겠다. 플레이어는 자신의 캐릭터를 포함한 4명의 파티원을 꾸려 전투할 수 있다. 플레이어가 제어하는 캐릭터를 제외한 나머지 3명의 캐릭터는 AI가 제어한다. 나머지 3명의 캐릭터에도 AI 엔진과 액터 사이에 인터페이스 용으로 사용할 수 있다. 따라서 플레이어는 취향껏 자신이 제어하고 싶은 객체를 교환할 수 있게 된다.

 

Command 라는 일급 객체로 만들었기 때문에 내용 -> 공통 -> 제어 순으로 결합도를 낮출 수 있었다.

더 나아가 스트림이나 큐로 만들어 실행 취소 기능도 어렵지 않게 구현할 수 있다.

이러한 기능은 전략게임을 개발할 때 많은 도움이 된다.

 

위의 코드를 추상화 해두었기 때문에 약간만 수정하면 정상작동 할 수 있다.

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

 

위 코드는 유닛을 x, y 좌표로 움직이도록 하는 코드이다.

 

이번에 만들 명령 클래스는 특정 시점에 발생할 일을 표현할 수 있어야하기 때문에 명령 인스턴스를 생성해야 한다.

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

 단계별로 명령 인스턴스를 생성할 수 있도록 구현하였다.

이제 명령을 취소할 수 있도록 undo() 를 정의해보자.

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

 

아까 작성해둔 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_;
};​

 

 

움직이기 전의 위치를 저장하기 위해서 xBefore_, yBefore_ 변수가 추가 됐다.

 

따라서 위의 코드를 좀 더 구체적으로 작성하면,

  포인터를 옮기는 것으로 실행 취소를 여러번 실행하는 것도 가능하고 재실행을 여러번 하는 것도 가능하다 또, 실행 취소를 여러번 실행하고 새로운 행동을 하면 현재 명령뒤에 새로운 명령을 추가하고 그 뒤에 붙어있는 나머지 명령들은 제거하면 된다.