명령 (Command)

 

요청 자체를 캡슐화하는 것입니다. 이를 통해 요청이 서로 다른 사용자(client)를 매개변수로 만들고, 요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산을 지원합니다.

 

한줄로 요약하면 메서드 호출을 실체화 한 것이다.

실체화는 무엇인가를 일급(first-class)으로 만들었다는 뜻이고, 메서드 호출을 일급으로 만들었다는 것은 함수를 객체로 감쌌다는 의미이다.

프로그래밍 언어에 따라 콜백, 일급 함수, 함수 포인터, 함수 객체, 클로저 등으로 불린다.

 

입력키 변경

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 InputHandler {
public:
    void handleInput();
private:
    Command* buttonX;
    Command* buttonY;
    Command* buttonA;
    Command* buttonB;
};

void InputHandler::handleInput()
{
    if (isPressed(BUTTON_X)) buttonX->execute();
    ...
}

 

직접 호출하는 방식에서 한 단계 우회하는 계층이 생겼다. 이것이 명령 패턴의 핵심이다.

 

 

액터에게 지시하기

위의 코드는 전역 함수가 플레이어 캐릭터를 직접 찾아서 동작하기 때문에 플레이어 캐릭터에만 적용될 수 있다.

함수가 객체를 직접 찾게 하지 말고 인자로 넘겨주면 조금 더 유연하게 사용할 수 있다.

 

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

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

 

 

이제 InputHandler가 키입력과 명령을 동시에 처리하게 하지 않고, 키입력시 해당하는 명령 객체를 반환하게 수정하면 된다.

 

Command* InputHandler::handlerInput()
{
    if (isPressed(BUTTON_X)) return buttonX;
    if ...
    return nullptr;
}

Command* command = inputHandler.handlerInput();
if (command) command->execute(actor);

 

 

액터와 명령 사이에 추상 계층을 한 단계 더 두었기 때문에 코드에 유연성이 생겼다.

기존과 다르게 플레이어 뿐만 아니라 AI 로직을 담당하는 객체도 위의 기능을 사용하여 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_;
};

 

이번에는 유닛과 위치 값을 생성자에서 받아서 명령과 명시적으로 바인드했다.

 

Command* handleInput() // 멤버 함수가 아니다
{
    Unit* unit = getSelectedUnit();
    if (isPressed(BUTTON_UP)) {
        int destY = unit->y() - 1;
        return new MoveUnitCommand(unit, unit->x(), destY); // 명령 객체 새로 생성
    }
    ...
    return nullptr;
}

 

 

기존 명령과는 조금 다른점이 있는데, 다른 명령 객체가 매번 재사용 되는것과 달리 이동은 좌표가 매번 다르기 때문에 플레이어가 이동을 선택할 때마다 명령 객체를 새로 생성해야 한다.

 

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_->x(); // 이동 전 좌표 저장
        yBefore_ = unit_->y();
        unit_->moveTo(x_, y_);
    }
    
    virtual void undo() {
        unit_->moeTo(xBefore_, yBefore_); // 저장된 기존 좌표 사용
    }
private:
    Unit* unit_;
    int x_, y_;
    int xBefore_, yBefore;
};

 

실행취소를 위해 상태 몇 가지가 추가된다.

만약 재실행(redo)를 구현한다면, 가장 최근 명령 하나만 기억하는게 아니라 컨테이너 등에 명령들을 담아서 현재 명령이 무엇인지만 가리키고 있으면 된다.

실행취소한뒤에 새로운 명령을 실행하면 뒤쪽에 저장된 명령들은 모두 버리고 새로운 명령을 저장한다.

 

참고로 재실행은 게임 플레이에서 잘 쓰이지 않을수도 있지만 리플레이 기능에는 매우 자주 쓰인다.

매 프레임마다 전체 게임 상태를 저장하는 것이 아니라 명령들만 저장해서 순서대로 실행시키면 되기 때문이다.

 

 

클래스만 좋고, 함수형은 별로인가?

그런것은 아니다. 다만 상태를 저장할 필요가 있기 때문에 클래스를 사용할 뿐이다.

클로저를 제대로 지원해주는 언어라면 당연히 사용해도 된다.

+ Recent posts