Search
Duplicate

명령 패턴(Command pattern)

게임이 아니여도 명령패턴은 정말 다양한 분야에서 사용되고 있다.
명령 패턴은 메서드(함수) 호출을 실체화 한 것이다.

입력키 변경 및 실행

A버튼은 무기를 바꾸는 동작
B 점프를 하는 동작을 실행
X는 총아을 발사하는 동작
Y 방패를 올리는 동작
위 패턴을 정말 간단하게 구현 예를 보자
class InputHandler { public: void HandleInput() { if (isPressed(BUTTON_X)) Fire(); else if (isPressed(BUTTON_Y)) Defence(); else if (isPressed(BUTTON_A)) SwapWeapon(); else if (isPressed(BUTTON_X)) Jump(); } //... }
Python
복사
그래서 간단하게는 단순한 함수의 호출로 구현이 가능하다.
여기서 우리는 디자인패턴을 조금 더 깊게 공부하기 위해서 해당 함수에 실행되는 동작 하나하나를 하나의 클래스를 이용해서 객체로 바꾸어보자.
class Command { public: virtual ~Command() {} virtual void Execute() = 0; }; class JumpCommand : public Command { public: void Execute() override { std::cout << "Jump" << std::endl; } }; class FireCommand : public Command { public: void Execute() override { std::cout << "Fire" << std::endl; } }; class InputHandler { public: void HandleInput() { if (isPressed(BUTTON_X)) mButtonX->Execute(); else if (isPressed(BUTTON_Y)) mButtonY->Execute(); else if (isPressed(BUTTON_A)) mButtonA->Execute(); else if (isPressed(BUTTON_B)) mButtonB->Execute(); } private: Command* mButtonX; Command* mButtonY; Command* mButtonA; Command* mButtonB; };
Python
복사

게임 오브젝터(액터) 에게 명령하기

방금 정의한 command 클래스는 잘동작하긴 하겠지만 한계가 존재한다. jump() 나 fire()같은 함수가 플레이어 객체를 찾아서 실행을 해야한다.
이렇게 해당 로직 함수들이 플레이어와 긴밀하게 연결되어 있어 (커플링)
그러다 보니 유연성, 유용성이 떨어지게 된다. 현재 JumpCommand는 오직 플레이어 캐릭터만 점프 할 수 있도록 실행된다고 볼수 있다.
플레이어 외에도 몬스터, 여러가지 객체들 다 점프를 할수 있다면 코드를 새로 작성하지 않고 JumpCommand클래스를 재사용 하면 굉장히 편리해 질수 있다.
커맨드 클래스들이 플레이나 특정객체를 변수로 들고 있어서 실행할 수 있게 하지말고 외부에서 점프시킬 객체를 인자로 받아서 점프를 실행시키자.
class Command { public: virtual ~Command() {} virtual void Execute(GameObject* actor) = 0; }; class JumpCommand : public Command { public: void Execute(GameObject* actor) override { actor->Jump(); } };
Python
복사
이제 jumpCammand 클래스 하나로 게임이 등장하는 여러가지 캐릭터들이 다 점프가 가능하게 되었다. 남은것은 입력 핸들러에서 받아 적당한 객체의 메서드를 호출하는 명령 객체를 연결하는 작업뿐이다.
Command* InputHandler::handleInput() { if (isPreseed(BUTTON_X)) return buttonX; if (isPreseed(BUTTON_Y)) return buttonY; if (isPreseed(BUTTON_A)) return buttonA; if (isPreseed(BUTTON_B)) return buttonB; //아무것도 누르지 않았다면, 아무것도 하지 않느다. return nullptr; }
Python
복사
어떤 오브젝트를 매개변수로 넘겨줘야 할지 모르기 떄문에 handleInput()에서는 명령을 실행할수 없다.
그렇기 때문에 여기에서는 함수 호출시점을 지연시켜 주는 것이다.
다음 명령으로 객체를 받아서 플레이어를 대표하는 GameObject 객체에 적용하는 코드가 필요하다.
Command* command = inputHandler.handleInput(); if (command) { command->execute(actor); }
Python
복사
GameObject가 플레이어 캐릭터라면 유저 입력에 따라 동작하기 때문에 처음 예제와 기능상 다를 게 없다. 하지만 명령과 오브젝트 사이에 추상 계층 한 단계 더 둔 덕분에, 소소한 기능이 하나 추가 되었다. 명령을 실행할 때 actor(gameObject)만 바꿔주면 게임에 있는 어떤 액터라도 제어 할수 있게 되었다.
사실 플레이어가 다른 엑터를 제어하는 일은 일반적이지 않다. 하지만 비슷하면서도 자주 사용 되는 기능이 있다. 예를들어 AI가 제어하는 캐릭터를 생각해보자. 다양한 AI가 존재한다고 했을때 … (npc, monster….) 각각 다른 동작을 실행시킬수 있다.
command객체를 선택하는 AI와 이를 실행하는 엑터를 디커플링 시키면서, 액터마다 AI모듈을 다 다르게 적용 할 수 있고, 기존 AI를 변형시켜 새로운 성향의 AI를 만들수도 있다. 더 나아가 플레이어 캐릭터에 AI를 연결해서 자동으로 실행되는 데모를 만들어 볼수도 있다.
엑터를 제어하는 command를 일급 객체로 만든 덕분에, 메서드를 직접 호출하는 형태가 아니라 한단계 추상화를 거쳐서 디커플링을 제거 할 수 있다.
입력 핸들러나 AI 같은 코드에서는 명령 객체를 만들어 스트림에 밀어 넣는다. 결론적으로 엑터에서는 명령 객체를 받아서 동작을 실행한다.

실행취소와 재실행

명령 패턴 중에서도 가장 잘 알려진 패턴이다. 명령 객체가 어떤 작업을 실행 할 수 있다면, 이를 실행취소 할수 있게 만드는 것도 어렵지 않다.
실행 취소 기능은 원치 않는 행동을 되돌릴 수 있는 전략게임 자주 볼 수 있다. 특히 게임개발 툴을 만들 때 필수적으로 사용된다.
실행 취소기능을 구현하려면 굉장히 어렵지만, 명령패턴을 사용하면 쉽게 만들수 있다. 싱글플레이 턴제 게임에서 유저가 어림짐작해서 전략을 실행해볼수 있는 이동 취소기능 만들어 봅시다.
명령 객체에서 이를 이용해서 입력처리를 추상화 해두었다. 플레이어 이동도 캡슐화 되어있다. 유닛을 옮기는 명령을 만들어보자
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_, y_; };
Python
복사
MoveUnitCommand 클래슨 이전 예제와 다르다. 이전 예제에서는 명령에서 변경하려는 액터를 인자로 받아서 명령과 액터사이를 추상화 시켜 격리했습니다. 이번에는 이동하려는 유닛을 위치값을 생성자에서 받아서
MoveUnitCommand 와 바인딩( Unit* 객체의 주소를 알고 있음) 시켰다. 이 커맨드는 보편적인(인자로 받는 경우)작업이 아니라 게임에서 구체적으로 실질적인 이동을 담당하고 있다.
이는 명령패턴 구현을 어떻게 변형할 수 있는지를 보여준다. 처음 예제의 경우에는 어떤일을 하는지가 중요하고 명령 객체하나가 매번 재사용 된다.
이번에는 명령 클래스는 특정 시점에 발생될 일을 표현하다는 거에서 조금더 구체적이다. 이를 테면 입력핸들러 코드는 플레이어가 이동을 선택 할 때마다 명령 인스턴스를 생성해야한다.
Command* handleInput() { Unit* unit = getSelectedUnit(); if (isPressed(BUTTON_UP)) { // Move the unit up one. int destY = unit->y() - 1; return new MoveUnitCommand(unit, unit->x(), destY); } if (isPressed(BUTTON_DOWN)) { // Move the unit down one. int destY = unit->y() + 1; return new MoveUnitCommand(unit, unit->x(), destY); } // Other moves... return NULL; }
Python
복사
MoveUnitCommand클래스가 일회용이 된다. 이게 장점이 된다는 거에요. 명령을 취소 하루 있도록 순수가상함수 undo()를 정의한다.
class Command { public: virtual ~Command() {} virtual void execute() = 0; virtual void undo() = 0; };
Python
복사
undo()에서는 excute()에서 변경하는 게임상태로 바꿔주면 된다. 실행 취소 기능을 넣어보자
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() { // Remember the unit's position before the move // so we can restore it. xBefore_ = unit_->x(); yBefore_ = unit_->y(); unit_->moveTo(x_, y_); } virtual void undo() { unit_->moveTo(xBefore_, yBefore_); } private: Unit* unit_; int xBefore_, yBefore_; int x_, y_; };
Python
복사
잘 보면 MoveUnitCommand의 클래스 상태가 몇개 추가 되었다. 유닛이 이동 한후에 이전 위치를 기억하기위해서 before변수가 추가되었다.
이동을 취소 할 수 있게 하려면 이전에 실행했던 명령을 저장 해둬야 한다. 우리가 ctrl+z를 막 누르고 있을때, 이전 명령어의 undo() 실행되는 거다.
가장 최근 명령만 기억하는 대신, 명령목록을 유지하고 현재명령이 무엇이지만 알고 있으면 된다. 유저가 명령을 실행하면, 새로 생성된 명령을 목록 맨 뒤에 추가하고, 이를 현재명령으로 기억하면 된다.
유저가 실행 취소를 선택하면 현재 명령을 실행취소하고 현재 명령을 가리큰 포인터를 한칸 뒤로 이동하다. 몇번 실행 취소 한 뒤에 새로운 명령을 실행하면 , 현재 명령 뒤에 새로운 명령어를 추가 하고 그 다음에 붙어 있는 명령들은 지워준다.

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

위 예제에서는 처음에 간단하게 표현한 클래스를 계속 복잡하게 패턴화시켜 함수형 객체로 표현하였다. 자 그러면 과연 패턴을 사용하는 경우와 간단하게 함수만을 이용해서 해당 패턴을 표현하는 경우 어떤것이 더 좋은가?
상황에 따라서 내가 만들고자 하는 기능이 간단한 경우라면 함수형이 더 좋은 성과를 낼 수 있다. 작업시간도 훨씬 줄어든다. 다만 유지보수가 추가적으로 들어가야하고 또한 추가적인 컨텐츠가 생기는 경우라면 클래스를 이용해서 패턴을 사용하는가 더 좋을 수 있다.
만약에 javascript같은 함수형 프로그래밍에 익숙한 경우라면 조금더 효율적으로 명령패턴을 사용할수 있을 것이다.
function makeMoveUnitCommand(unit, x, y) { // This function here is the command object: 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); } }; }
JavaScript
복사
명령패턴 연습해보기 C++ 콘솔창을 이용하여 숙제를 해주세요.
플레이어와 그 플레이어의 행동 3가지에 맞는 커맨드 클래스(이동, 대쉬, 점프) 를 구현해라.
이동은 한칸식 이동하면된다. 좌우
대쉬는 앞으로(오른쪽)만 2칸씩
점프는 위로(1칸 이동)
(추가숙제 - 못해도 괜찮다.)그리고 추가적으로 z를누르면 이전 동작으로 돌아가게끔 undo를 구현하여 커맨드패턴을 활용해보라 2차원 std::vector<std::vector<char>>..의 맵을 만드시고 해당맵에서 플레이어가 커맨드 패턴을 이용해서 이동하는 기능을 구현 해주세요. 그리고 맵에 해당정보들이 화면에 그려줘리.
#include <iostream> #include <vector> #include <Windows.h> class Player { public: Player(char ch, int y, int x) : mCh(ch) , mY(y) , mX(x) { } int GetY() const { return mY; } int GetX() const { return mX; } char GetChar() const { return mCh; } private: char mCh; int mY; int mX; }; int main() { std::vector<std::vector<char>> map ( 20, std::vector<char>(20,'_') ); Player player('P', 10, 10); while (true) { char input = 0; std::cin >> input; // print player map[player.GetY()][player.GetX()] = player.GetChar(); //print map system("cls"); Sleep(100); for (size_t y = 0; y < 20; y++) { for (size_t x = 0; x < 20; x++) { std::cout << map[y][x]; } std::cout << std::endl; } } return 0; }
JavaScript
복사