[게임 프로그래밍 패턴] Chapter2 명령

Jangmanbo·2023년 8월 15일
0

명령 패턴
메소드 호출을 실체화, 즉 일급 객체로 만드는 것을 의미

바꿔 말하면 함수 호출을 객체로 감쌌다는 의미이다.


명령 패턴의 예시

1. 입력키 변경

명령 패턴을 사용하지 않은 경우

void InputHandler::handleInput() {
	if (isPressed(Button_X)) jump();
    else if (isPressed(Button_X)) fireGun();
    else if (isPressed(Button_A)) swapWeapon();
    else if (isPressed(Button_B)) lurchIneffectively();
}

handleInput함수가 매 프레임마다 호출되어 해당 키 입력에 따른 함수를 호출한다.
이때 문제점은 키 변경이 불가능하다는 것!

명령 패턴을 사용한 경우

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

Command라는 추상 클래스 선언하고

class JumpCommand : public Command {
public:
	virtual void execute() { 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_X)) buttonY_->execute();
    else if (isPressed(Button_A)) buttonA_->execute();
    else if (isPressed(Button_B)) buttonB_->execute();
}

InputHandler는 명령 패턴을 사용하지 않을 때와는 달리 직접 함수를 호출하지 않는다.
따라서 각 버튼의 Command객체만 바꿔주면 키 변경도 지원할 수 있게 되었다.

그러나 jump(), fireGun()과 같이 (아마도 플레이어를 조작하는) 전역함수만을 실행할 수 있다는 문제가 있다.


2. 액터에게 지시하기

플레이어 캐릭터만이 아니라 그 어떤 액터에게도 지시할 수 있도록,
제어하려는 객체를 함수가 직접 찾지 않고 밖에서 전달해주자!

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

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

Command의 자식 클래스는 execute()가 호출될 때 인자로 GameActor 객체를 전달받음으로써 어떤 객체든 조작할 수 있게 되었다.

Command* InputHandler::handleInput() {
	if (isPressed(Button_X)) return buttonX_;
    else if (isPressed(Button_X)) return buttonY_;
    else if (isPressed(Button_A)) return buttonA_;
    else if (isPressed(Button_B)) return buttonB_;
    
    return NULL:
}

어떤 액터를 조작할지는 handleInput에서 모르기 때문에, 입력 처리에서는 단순히 해당 입력에 대한 Command 객체만 리턴한다. 즉, 함수 호출 시점을 지연시킨다.

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

actor만 바꾸어도 어떤 액터든 제어할 수 있게 되었다!

이렇게 입력을 처리할 때 직접 함수를 호출하는 방식에서 명령 패턴을 사용하면서 입력 처리와 액터 사이의 커플링을 제거했다.


3. 실행취소와 재실행

명령 패턴을 이용한다면 실행취소 기능을 쉽게 구현할 수 있다.

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

아까 만든 JumpCommand와 같이 MoveUnitCommand은 어떠한 객체를 이동시키는 명령이다.
그치만 액터와 명령 사이를 추상화로 격리한 JumpCommand와 달리 MoveUnitCommand는 이동시킬 객체와 위치값을 생성자에서 받아서 명시적으로 바인드했다.

  • JumpCommand
    • 어떤 일을 하는지 정의한 클래스로, 입력 핸들러의 멤버변수인 JumpCommand 객체는 매번 재사용된다.
  • MoveUnitCommand
    • 게임에서의 구체적일 실제 이동을 나타내는 클래스로, 입력 핸들러는 플레이어가 이동할 때마다 MoveUnitCommand 객체를 생성한다.

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

실행취소를 지원하는 undo함수를 만들고 handleInput에서는 매번 새로운 MoveUnitCommand와 같은 명령 객체들을 리턴하고 이를 리스트에 저장해둔다.

실행취소를 할 때마다 현재 명령을 가리키는 포인터를 뒤로 이동하면서 undo를 호출하면 실행취소 기능 완성!
반대로 재실행을 하려면 포인터를 다음으로 이동하여 execute를 실행하면 된다.


4. 클래스 vs 함수형

명령을 함수로 구현하지 않은 이유는 C++이 클로저를 제대로 지원하지 않기 때문이다.

C++

vector<function<int(int)>> vec;

void func()
{
  auto num = 3;

  vec.emplace_back(
    [&](int value) { return value + num; }
  );
}

int main()
{
  func();
  auto funcc = vec.at(0);
  cout<<funcc(3);	// 쓰레기값
}

C++은 별도의 처리가 없다면 클로저로 캡쳐된 변수는 선언된 스코프와 수명이 동일하다.
따라서 실제 람다 함수가 실행되는 순간에 해당 변수가 유효하지 않을 수 있다.

참고: [C++] 람다식(functional) 사용법과 클로저(closure)

C++에서 클로저 지원하기
1. shared_ptr 변수를 캡쳐

auto obj = std::make_shared<SomeClass>();
auto lambda = [obj]() { /* 코드 */ };
  1. bind로 함수와 변수 바인딩
int x = 5;
std::function<int()> func = std::bind([](int val) { return val; }, x);

JavaScript

const a = () => {
    let i = 0;
    return () => {
        i += 1;
        return i;
    }
}
const b = a();

console.log(b());	// 1
console.log(b());	// 2
console.log(b());	// 3

반면 자바스크립트는 클로저를 통해 함수의 상태를 저장할 수 있어서 굳이 클래스를 이용한 명령 패턴을 사용할 필요가 없다.

0개의 댓글