기록공간

1장. Command Pattern (명령 패턴) 본문

Game Design Pattern, Logic

1장. Command Pattern (명령 패턴)

입코딩 2020. 2. 1. 17:54
반응형

"요청 자체를 캡슐화 하는 것입니다. 이를 통해 서로 다른 사용자(client)를 매개변수로 만들고,

요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산을 지원합니다." (GoF의 디자인 패턴 311p)

 


명령패턴을 매우 간결하게 요약하면 다음과 같다.

 

명령 패턴은 메서드 호출을 실체화(reify) 한 것이다.

 

실체화는 '실제하는 것으로 만든다'라는 뜻으로, 프로그래밍 분야에서는 무엇인가를 '일급(first-class)'으로 만든다는 뜻으로 통한다. 

 

즉, 어떤 개념을 변수에 저장하거나 함수에 전달할 수 있도록 데이터, 즉 객체로 바꿀 수 있다는 것을 의미한다. 여기에서 명령 패턴을 '메서드 호출을 실체화한 것'이라고 한 것은 함수 호출을 객체로 감쌌다는 의미이다.

 

GoF에서는 '명령 패턴은 콜백을 객체지향적으로 표현한 것'이라고 정의한다.


입력키 변경

모든 게임에는 버튼이나 키보드, 마우스를 누르는 등의 유저 입력을 읽는 코드가 있다. 이런 코드는 입력을 받아서 게임에서 의미 있는 행동으로 전환한다.

 

간단하게 구현해보면 다음과 같다.

void InputHandler::HandleInput()
{
	if (IsPressed(ButtonA)) jump();
	else if (IsPressed(ButtonB)) fireGun();
	else if (IsPressed(ButtonX)) swapWeapon();
	else if (IsPressed(ButtonY)) lurchIneffectively();
}

 

입력 키 변경을 불가능하게 만들겠다면 위 코드처럼 해도 되겠지만, 많은 게임이 키를 바꿀 수 있게 해준다.

 

키 변경을 지원하려면 jump()나 fireGun()처럼 메서드를 직접 호출하지 말고 교체 가능한 무언가로 바꿔야 한다. 이 '교체' 작업에 객체를 이용한 명령 패턴이 등장할 때다.

 

일단 상위 클래스부터 정의하자.

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() { fire(); }
};

// 나머지 생략..

입력 핸들러 코드에 각 버튼별로 Command 클래스 포인터를 저장한다.

(포인터는 모든 객체가 연결되어 있어 NULL이 아니라고 가정한다.)

class InputHandler
{
public:
	void HandleInput();
private:
	Command* _ButtonX;
	Command* _ButtonY;
	Command* _ButtonA;
	Command* _ButtonB;
};

이제 입력 처리는 다음 코드로 위임된다.

void InputHandler::HandleInput()
{
	if (IsPressed(ButtonA)) _ButtonA->execute();
	else if (IsPressed(ButtonB)) _ButtonB->execute();
	else if (IsPressed(ButtonX)) _ButtonX->execute();
	else if (IsPressed(ButtonY)) _ButtonY->execute();
}

직접 함수를 호출하던 코드 대신에, 합 겹 우회하는 계층이 생겼다.


액터에게 지시하기

방금전에 정의한 Command 클래스는 한계가 있다. jump()와 같은 전역 함수가 플레이어 캐릭터 객체를 암시적으로 찾아서 꼭두각시 인형처럼 움직이게 할 수 있다는 가정이 깔려 있다는 점에서 상당히 제한적이다.

 

이렇게 커플링이 되다보니 Command 클래스의 유용성이 떨어진다. 이런 제약을 유연하게 만들기 위해서 제어하려는 객체를 함수에서 직접 찾게 하지 말고 밖에서 전달해주자.

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

여기서 Actor는 게임 월드를 돌아다니는 캐릭터를 대표하는 '게임 객체' 클래스이다. Command를 상속받은 클래스는 execute()가 호출될 때 GameActor 객체를 인수로 받기 때문에 원하는 액터의 메서드를 호출할 수 있다.

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

이제 JumpCommand 클래스 하나로 게임에 등장하는 어떤 캐릭터라도 폴짝거리게 할 수 있다. 남은 것은 입력 핸들러에서 입력을 받아 적당한 객체의 메서드를 호출하는 명령 객체를 연결하는 코드 뿐이다. 먼저 HandleInput()에서 명령 객체를 반환하도록 변경한다.

Command* Input::HandleInput()
{
	if (IsPressed(ButtonA)) return _ButtonA;
	else if (IsPressed(ButtonB)) return _ButtonB;
	else if (IsPressed(ButtonX)) return _ButtonX;
	else if (IsPressed(ButtonY)) return _ButtonY;
    
    // 아무것도 누르지 않았다면, 아무것도 하지 않는다. 
	return NULL;
}

어떤 액터를 매개변수로 넘겨줘야 할지 모르기 때문에 HandleInput()에서는 명령을 실행할 수 없다. 여기에서는 명령이 실체화된 함수 호출이라는 점을 활용해서, 함수 호출 시점을 지연한다.

 

다음으로 명령 객체를 받아서 플레이어를 대표하는 Actor 객체에 적용하는 코드가 필요하다.

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

처음 예제와 기능상 다를 게 없다. 하지만 명령과 액터 사이에 추상 계층을 한 단계 둔 덕분에, 소소한 기능이 하나 추가되었다. 명령을 실행할 때 액터만 바꾸면 플레이어가 게임에 있는 어떤 액터라도 제어 할 수 있게 되었다.

 

액터를 제어하는 Command를 일급 객체로 만든 덕분에, 메서드를 직접 호출하는 형태의 강한 커플링을 제거 할 수 있었다.

 

명령을 큐나 스트림으로 만드는 것도 생각해보자. 입력 핸들러 같은 코드에서 명령 객체를 만들어 스트림에 밀어 넣으면 액터에서는 명령 객체를 받아서 호출한다. 큐를 둘 사이에 끼워 넣음으로써, 생산자(producer)와 소비자(consumer)를 디커플링할 수 있게 되었다.


실행취소와 재실행

명령 객체가 어떤 작업을 실행할 수 있다면, 이를 실행취소 할 수 있게 만드는 것도 어렵지 않다.

실행취소 기능은 원치 않는 행동을 되돌릴 수 있는 전략 게임에서 많이 볼 수 있으며, 게임 개발 툴에서는 필수이다. 

 

그냥 실행취소 기능을 구현하려면 굉장히 어렵지만, 명령 패턴을 이용하면 쉽게 만들 수 있다. 싱글 플레이어 턴제 게임에서 유저가 어림짐작보다는 전략에 집중할 수 있도록 이동 취소 기능을 추가한다고 해보자. 

 

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

class MoveUnitCommand : public Command
{
public:
	MoveActorCommand(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;
};

이전 예제에서는 명령에서 변경하려는 액터와 명령 사이를 추상화로 격리시켰지만 이번에는 이동하려는 유닛과 위치 값을 생성자에서 받아서 명령을 명시적으로 바인드했다. 

 

이는 명령 패턴 구현을 어떻게 변형할 수 있는지 잘 보여준다. 처음 예제 같은 경우, 어떤 일을 하는지를 정의한 명령 객체 하나가 매번 재사용된다. 입력 핸들러 코드에서는 특정 버튼이 눌릴 때마다 여기에 연결된 명령 객체의 execute()를 호출했었다. 

 

이번에 만든 명령 클래스는 특정 시점에 발생될 일을 표현한다는 점에서 좀 더 구체적이다. 이를테면, 입력 핸들러 코드는 플레이어가 이동을 선택할 때마다 명령 인스턴스를 생성해야 한다.

Command* Input::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),
	{}
    
	virtual void execute()
	{
		// 나중에 이동을 취소할 수 있도록 원래 유닛 위치를 저장한다.
		_BeforeX = _unit->X();
		_BeforeY = _unit->Y();
		_unit->moveTo(_x, _y);
	}
    
	virtual void undo()
	{
		_unit->moveTo(_BeforeX, _BeforeY);
	}
    
private:
	Unit* _unit;
	int _x;
	int _y;
	int _BeforeX = 0;
	int _BeforeY = 0;
};

유닛이 이동한 후에는 이전 위치를 알 수 없기 때문에, 이동을 취소할 수 있도록 이전 위치를 _BeforeX, _BeforeY 맴버 변수에 따로 저장한다.

 

우리가 Ctrl+Z를 막 누르고 있을 때, 계속 이전 명령의 undo()가 실행되고 있는 것이다.(혹은 이미 실행취소했다면 redo()를 호출해 명령을 재실행한다.)

 

여러 단계의 실행취소를 지원하는 것도 그다지 어렵지 않다. 가장 최근 명령만 기억하는 대신, 명령 목록을 유지하고 '현재'명령이 무엇인지만 알고 있으면 된다. 유저가 명령을 실행하면, 새로 생성된 명령을 목록 맨 뒤에 추가하고, 이를 '현재'명령으로 기억하면 된다.

 

유저가 '실행취소'를 선택하면 현재 명령을 실행취소하고 현재 명령을 가리키는 포인터를 뒤로 이동한다. '재실행'을 선택하면, 포인터를 다음으로 이동시킨 후에 해당 포인터를 실행한다. 유저가 몇 번 '실행취소'한 뒤에 새로운 명령을 실행한다면, 현재 명령 뒤에 새로운 명령을 추가하고 그 다음에 붙어있는 명령들은 버린다.


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

앞에서 명령은 일급 함수와 비슷하다고 했지만, 예제에서 전부 클래스만 사용해서 만든 이유는 C++이 일급 함수를 제대로 지원하지 않기 때문이다. 함수 포인터에는 상태를 저장할 수 없고, functor는 이상한 데다가 여전히 클래스를 정의해야 한다. 람다는 메모리를 직접 관리해야 하기 때문에 쓰기가 까다롭다. 

 

그렇다고 다른 언어에서도 명령 패턴에 함수를 쓰면 안된다는 얘기는 아니다. 언어에서 클로저를 제대로 지원해준다면 안 쓸 이유가 없다! 어떻게 보면 명령 패턴은 클로저를 지원하지 않는 언어에서 클로저를 흉내 내는 방법 중 하나일 뿐이다.

 

자바 스크립트로 게임을 만든다면 유닛 이동 명령을 다음과 같이 만들 수 있다.

function makeMoveUnitCommand(unit, x, y)
{
     // 아래 function이 명령 객체에 해당한다.
     return function() 
     {
        unit.moveTo(x, y);
     }
}

클로저를 여러 개 이용하면 실행취소도 지원할 수 있다.

function makeMoveUnitCommand(unit, x, y)
{
    var xBefore, yBefore;
    return
    {
      execute: function()
      {
       xBefore = unit.x();
       yBefore = unit.y();
       unit.moveTo(x, y);
      },
      undo: function() 
      {
       unit.moveTo(xBefore, yBefore);
      }
    };
}

자료의 출처는 http://gameprogrammingpatterns.com/command.html입니다.

반응형
Comments