Design pattern(1-2) - 명령 패턴(undo, redo)
이전 포스팅입니다.
https://savinglectures123.tistory.com/31
Design pattern(1-1) - 명령 패턴(command)
명령 패턴이란? 쉽게 설명한다면 '메서드 호출을 실체화 한것'입니다.실체화는 실체하는 것(인스턴스)을 만든다는 의미이며, 변수에 저장하거나 함수에전달할 수 있도록 데이터, 즉 객체로 바
savinglectures123.tistory.com
이전 포스팅에서 만든 Command는 Jump()같은 Command가 아닌
다른 방법으로도 패턴을 사용할 수 있습니다.
바로 실행취소(undo) 와 재실행(redo)가 있습니다.
실행취소는 원치 않는 행동을 되돌릴 수 있는 전략 게임에서 볼 수 있습니다.
게임 개발 툴(에디터, 임구이 등) 단연 에서 필수입니다.
(필자는 imgui를 이용해 맵 툴을 만들 때 undo, redo를 유용하게 사용함)
레벨 에디터에서 실행취소 기능을 제공해주지 않는다면 일단 자기자신부터 힘들고
일을 넘겼을 때 다른 작업자들에 미움을 살 수 있기 때문에 이러한 기능은
필수적입니다.
그냥 실행취소 기능을 처음부터 끝가지 복잡하기 때문에 싱글플레이어 턴제 게임에서
유저가 전략에 집중할 수 있도록 이동 취소 기능을 추가한다고 가정해봅시다.
이전 포스팅에서 명령 객체를 이용해 입력 처리를 추상화해둔 덕분에,
플레이어 이동도 명령에 캡슐화가 되어 있습니다.
유닛을 옮기는 명령을 코드로 옮겨봤습니다.
class MoveUnitCommad : public Command {
public:
MoveUnitCommad(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_;
};
MoveUnitCommad 를 명시적으로 바인딩해 실질적인 이동을 담고 있습니다.
이런 클래스가 있다면 명령 패턴을 구현할 수 있습니다.
입력 핸들러 코드에서는 특정 버튼이 눌릴 때마다 여기에 연결된 명령 객체의
execute()를 호출했었습니다.
이번에 만든 클래스는 특정 시점에 발생될 일을 표현해야합니다.
입력 핸들러 코드는 플레이어가 이동을 선택할 때 마다
명령 인스턴스를 생성해야합니다.
(일회용 객체라면 unique_ptr을 사용해 이동 연산자로 대입해줄 수 있습니다)
Command* handleInput() {
Unit* unit = getSelectedUnit();
if (!unit) return nullptr;
int x = unit->x();
int y = unit->y();
if (isPressed(BUTTON_UP)) {
y -= 1;
} else if (isPressed(BUTTON_DOWN)) {
y += 1;
} else if (isPressed(BUTTON_LEFT)) {
x -= 1;
} else if (isPressed(BUTTON_RIGHT)) {
x += 1;
} else {
return nullptr; // 이동 명령 없음
}
return new MoveUnitCommand(unit, x, y);
}
Command 클래스가 일회용객체이기 때문에 코드가 간결하고
유연하게 Command를 바꿀 수 있습니다.
이제 실행 취소를 위해 순수 가상함수 undo를 만들어보죠.
class Command {
public:
virtual ~Command() {}
virtual void execute() = 0;
virtual void undo() = 0;
};
그 다음 undo()에서는 excute()에서 실행하는 상태를 반대로 실행시켜주면
MoveUnitCommand 클래스의 실행취소(undo) 기능을 만들 수 있습니다.
class MoveUnitCommad : public Command {
public:
MoveUnitCommad(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_moveTo(xBefore_, yBefore);
}
private:
Unit* unit_;
int x_, y_;
int xBefore_, yBefore_;
};
MoveUnitCommand 에서 이전위치를 추가하여 이동을 취소할 수 있도록 이전 위치를
xBefore_, yBefore_ 멤버 변수에 따로 저장해놨습니다.
플레이어가 이동을 취소할 수 있게 하려면 이전에 실행했던 명령을 저장해야 합니다.
우리가 Ctrl + Z를 누르고 있을 때 프로그램에서는 undo()가 실행되고 있는 것입니다.
실행이 취소되면 명령의 위치를 옮기고 다음 커멘드를 기다립니다.
(실제로 이런 메모리 공간의 느낌은 아닙니다)
(그림만 보고 어떤 느낌인지 확인해주세요)
이렇게 추상 클래스인 커맨드를 사용해 상속된 여러 커맨드를
입력받아서 사용할 수 있습니다.
여러 커맨드를 입력받아 에디터상에서 실행취소와 재실행을 구현한다면
아래와 같이 자료구조 Stack(FIFO) 으로 구현할 수 있습니다.
class CommandManager {
private:
std::stack<std::unique_ptr<Command>> undoStack;
std::stack<std::unique_ptr<Command>> redoStack;
public:
void executeCommand(std::unique_ptr<Command> command) {
command->execute();
undoStack.push(std::move(command));
while (!redoStack.empty()) {
redoStack.pop();
}
}
void undo() {
if (!undoStack.empty()) {
auto command = std::move(undoStack.top());
undoStack.pop();
command->undo();
redoStack.push(std::move(command));
}
}
void redo() {
if (!redoStack.empty()) {
auto command = std::move(redoStack.top());
redoStack.pop();
command->execute();
undoStack.push(std::move(command));
}
}
};
undo() 호출하면 Stack에 있는 데이터를 지우고 redo()에 넣는 방식이며,
redo()도 비슷하게 Stack 데이터를 지우고 undo()에 넣는 방식입니다.
부모 클래스인 Command를 사용해 코드 유연성이 뛰어납니다.
이렇게 명령 패턴에 대해서 알아봤습니다.
다음 디자인 패턴 포스팅에서는 최적화와 매우 밀접한
경량 패턴에 대해 알아보겠습니다.