Command

선우·2022년 12월 29일
0

GameProgrammingPatterns

목록 보기
2/2

Command

  • Command는 명령의 의미를 지닌다.
  • Command 패턴은 명령을 나타내는 클래스의 인스턴스를 하나로 표현하는 방법.
  • 명령의 이력을 관리하고 싶을 때는, 그 인스턴스의 집합을 관리하면 된다. 명령 모음을 저장하면 동일한 명령을 실행하거나 여러 명령을 함께 새 명령으로 재사용할 수 있다.

    명령은 구체화된 메서드 호출입니다.
    GOF(Gang of Four) - 명령은 콜백을 대체하는 객체 지향입니다.


Configuring Input

  • 모든 게임 어딘가에는 사용자 입력(버튼 누름, 키보드 이벤트, 마우스 클릭 등)을 읽는 코드들이 존재한다.
  • 각 입력을 받아 게임에서 의미 있는 작업으로 변환한다.

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

    해당 함수는 게임 루프(Game Loop)에 의해 프레임당 한 번 호출된다.
    해당 코드는 사용자 입력을 게임 작업에 hard-wire형식으로 작동하지만 많은 게임에서는 사용자가 버튼 매핑 방법을 구성 할 수 있다.
    이를 지원하려면 jump() 및 fireGun()에 대한 직접 호출을 교체할 수 있는 것으로 전환해야 한다.
    Swapping out은 변수를 할당하는 것과 비슷하기에 게임 동작을 나타내는데 사용할 수 있는 객체가 필요하다. Enter : Command Pattern

    • 클래스 정의
    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() { fireGun(); }
    };
    
    // You get the idea...
    • 입력 처리기에서 각 버튼에 대한 명령을 포인터로 저장한다.
    class InputHandler
    {
    public:
      void handleInput();  
    
      // Methods to bind commands...
    
    private:
      Command* buttonX_;
      Command* buttonY_;
      Command* buttonA_;
      Command* buttonB_;
    };
    • 입력 처리 적용.
    void InputHandler::handleInput()
    {
      if (isPressed(BUTTON_X)) buttonX_->execute();
      else if (isPressed(BUTTON_Y)) buttonY_->execute();
      else if (isPressed(BUTTON_A)) buttonA_->execute();
      else if (isPressed(BUTTON_B)) buttonB_->execute();
    }
    • 이를 통해 각 입력이 직접 호출시 간접 참조 계층을 통해 실행된다.

Directions For Actors

  • 위의 명령 클래스는 플레이어의 아바타의 점프(Jump())와 발사(FireGun())과 같은 상위 수준의 기능을 암묵적으로 알고 있을때 명령을 내릴수 있는 것과 같이 상당히 제한적이다.
  • 이러한 상정된 연결은 명령의 유용성을 제한한다.
  • JumpCommand는 플레이어를 점프할 수 있게하는 유일한 명령이다. 이러한 제한을 완화시키기 위해 명령된 객체를 직접 찾는 함수를 호출하는 대신 순서를 지정할 객체를 전달한다.
    class Command
    {
    public:
      virtual ~Command() {}
      virtual void execute(GameActor& actor) = 0;
    };
    • GameActor라는 게임 세계의 캐릭터를 나타내는 "게임 객체"클래스가 존재한다.
    • excoute()라는 명령이 다음과 같이 선택한 캐릭터에 대한 메서드를 호출할 수 있도록 전달한다.
    class JumpCommand : public Command
    {
    public:
      virtual void execute(GameActor& actor)
      {
        actor.jump();
      }
    };
    • 이제 이 클래스 하나를 사용하여 게임의 어떤 캐릭터를 뛰어다니게 할 수 있다.
    • 입력 처리기와 명령을 받아 올바른 개체에서 호출하는 부분이 누락되어 있다.
    • 먼저 handleInput()에 명령을 반환할 수 있게 변경한다.
    Command* InputHandler::handleInput()
    {
      if (isPressed(BUTTON_X)) return buttonX_;
      if (isPressed(BUTTON_Y)) return buttonY_;
      if (isPressed(BUTTON_A)) return buttonA_;
      if (isPressed(BUTTON_B)) return buttonB_;
    
      // Nothing pressed, so do nothing.
      return NULL;
    }
    • 어떤 액터를 전달할지 모르기 때문에 명령을 즉시 실행할 수가 없다. 여기에서 명령이 구체화된 호출이라는 사실을 활용한다 -> 호출이 실행될 때 지연시킬 수 있다.
    • 그런 다음 해당 명령을 받아 플레이어를 나타내는 액터에서 실행하는 코드가 아래와 같이 필요하다.
    Command* command = inputHandler.handleInput();
    if (command)
    {
      command->execute(actor);
    }
    • 액터가 플레이어의 캐릭터에 대한 참조라고 가정하면 사용자의 입력에 따라 플레이어를 올바르게 구동하므로 첫 번째 예에서와 동일한 동작으로 돌아간다.
    • 그러나 명령과 명령을 수행하는 캐릭터 사이에 간접적인 레이어를 추가하면 약간의 능력이 생긴다.
    • 이제 명령을 실행하는 캐릭터를 변경하여 플레이어가 게임의 캐릭터를 제어하도록 할 수 있다.
    • AI와 Command를 수행하는 actor 코드 사이의 분리는 유연성을 제공한다.
    • Command를 선택하는 AI와 Command를 수행하는 actor 코드 사이의 분리는 우리에게 많은 유연성을 준다.

Undo and Redo

  • Command 객체가 작업을 할 수 있을 때, Command 실행 취소는 하나의 작은 단계이다.
  • Undo, 실행 취소는 마음에 들지 않는 동작을 롤백할 수 있는 일부 전략 게임에서 사용될 뿐만아니라 게임 제작에 있어서 자주 사용하는 기능이다.
  • Command Pattern 없이 실행 취소를 구현하는 것은 어렵다.
  • 싱글 플레이어 턴 기반 게임에서 사용자들은 추측보다는 전략에 더 집중할 수 있도록 동작을 취소할 수 있도록 하고 싶다.
    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_;
    };
    • Input handler를 추상화하여 Command를 편리하게 사용하기 위해 플레이어의 움직임(move)를 Command 캡슐화 되어있다.
    • 해당 코드는 유닛의 이동.
    • 이전의 명령은 수정한 actor에서 Command를 추상화하여 이동 중인 유닛에 바인딩 하려 하였다. 이는 여러 컨텍스트에서 사용할 수 있는 일반적인 이동 작업이 아닌 게임의 턴 순서에서 구체적인 동작이다.
    • 이는 Command Pattern이 구현되는 방식의 변화를 강조한다.
    • 여기서 Command는 더 구체적으로 특정한 시점에 할 수 있는 것을 나타내며 이것은 다음과같이 플레이어가 동작을 선택할 때마다 Input Handling 코드가 이것의 Instance를 생성한다는 것을 의미한다.
    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;
    }
    • Command가 한 번만 사용된다는 사실은 곧 이점이 될 것이다.
    • 명령을 실행 취소할 수 있도록 하려면 각 Command 클래스에서 구현해야 하는 다른 작업을 정의한다.
    class Command
    {
    public:
      virtual ~Command() {}
      virtual void execute() = 0;
      virtual void undo() = 0;
    };
    • undo() 메서드는 해당 execute() 메서드에 의해 변경된 게임 상태를 되돌린다.
    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_;
    };
    • 클래스에 더 많은 state를 추가해줌으로써 유닛이 움직일때 이전의 위치(xBefore, yBefore)를 기록하여 해당 동작을 취소하고 싶을때 돌아갈 수 있도록 허용해준다.
    • 플레이어가 동작을 취소할 수 있도록 마지막으로 실행한 명령을 유지한다.
    • Ctrl-z를 누르면 우리는 그 명령의 undo()메서드를 호출한다. (이미 실행 취소한 경우 다시 실행이 되어 명령을 다시 실행한다.)
    • 마지막 Command를 기억하는 대신 Command 목록과 현재 Command에 대한 참조를 유지함으로써 플레이어가 Command를 실행할 때 목록에 추가하고 "현재"를 가리킨다.
    • 플레이어가 Undo를 선택하면 현재 Command를 실행 취소(undo)하고 현재 포인터를 뒤로 이동한다.
    • redo할 때, 포인터를 전진시킨 다음 그 Command를 실행한다.
    • 일부 Command를 실행 취소한 후 새 Command를 선택하면 현재(돌아간 Command)Command 이후의 목록에 있는 모든 항목이 삭제된다.
profile
누누의 잡다저장소

0개의 댓글