Even Idiots Can Make Game

명령 패턴

Date/Lastmod
Section
DEV
Categories
디자인 패턴

#1 유니티에서의 커맨드 패턴

일련의 특정 행동추적하려는 경우에 유용하다.

실행 취소/다시 실행 기능이 사용되거나, 입력의 내역이 목록으로 유지되는 게임을 플레이해본 적 있따면, 아마 커맨드 패턴을 사용하고 있을 확률이 높다.

메서드를 직접 호출하는 대신, 커맨드 패턴을 사용하면 “커맨드 오브젝트"라는 하나 이상의 메서드 호출을 캡슐화 할 수 있다.

커맨드 오브젝트를 대기열이나 스택과 같은 컬렉션에 저장하면, 오브젝트의 실행 타이밍을 제어할 수 있다. 이 저장된 일련의 행동을 나중에 재생할 수 있도록, 잠재적으로 지연하거나 실행을 취소할 수 있다.

커맨드 패턴을 구현하려면 행동을 포함한 일반 오브젝트, 즉 커맨드 오브젝트가 필요하다. 이 커맨드 오브젝트에는 로직을 수행할 작업과, 해당 작업을 실행 취소하는 방법이 포함된다.

public interface ICommand
{
  void Execute();
  void Undo();
}

모든 게임플레이 행동이 ICommand 인터페이스를 구현한다고 가정한다. (사실, 추상 클래스로도 가능하다.)

각 커맨드 오브젝트는 자체의 ExecuteUndo 메서드를 처리한다. 따라서, 게임에 더 많은 커맨드를 추가해도 기존의 커맨드에는 아무런 영향을 끼치지 않는다.

커맨드를 실행 및 취소하려면 다른 클래스가 필요한데, CommandInvoker 클래스이다.

public class CommandInvoker
{
  static Stack<ICommand> _undoStack = new();

  public static void ExecuteCommand(ICommand command)
  {
    command.Execute();
    undoStack.Push(command);
  }

  public static void UndoCommand()
  {
    if (undoStack.Count <= 0)
    {
      return;
    }

    ICommand activeCommand = undoStack.Pop();
    activeCommand.Undo();
  }
}

플레이어를 애플리케이션의 미로 안에서 이동하도록 구현하는 경우를 예로 들어보자.

플레이어의 위치 이동을 처리하는 PlayerMover를 작성한다.

public class PlayerMover : MonoBehaviour
{
  [SerializeField] LayerMask _obstacleLayer;

  const float _boardSpacing = 1f;

  public void Move(Vector3 movement)
  {
    transform.position += movement;
  }

  public bool IsValidMove(Vector3 movement)
  {
    return !Physics.Raycast
    (
      transform.position, movement,
      _boardSpacing, _obstacleLayer
    );
  }
}

여기에 커맨드 패턴을 적용해 보자. PlayerMoverMove 메서드를 오브젝트로 캡처한다. Move를 직접 호출하는 대신, ICommand 인터페이스를 구현하는 새 클래스 MoveCommand를 만든다.

public class MoveCommand : ICommand
{
  PlayerMover _playerMover;
  Vector3 _movement;

  public MoveCommand(PlayerMover player, Vector3 moveVector)
  {
    _playerMover = player;
    _movement = moveVector;
  }

  public void Execute()
  {
    _playerMover.Move(movement);
  }

  public void Undo()
  {
    _playerMover.Move(-movement);
  }
}

Execute는 구현하려는 로직을 저장한다.

MoveCommand가 실행하려는 모든 파라미터를 저장하는 것을 눈여겨 본다. 생성자를 통해 해당 정보를 받고 있다.

커맨드 오브젝트를 만들고 필요한 파라미터를 저장하면, CommandInvoker의 정적 ExecuteCommandUndoCommand 메서드가 MoveCommand에 전달된다.

그러면 MoveCommandExecute 또는 Undo가 실행되면서, 실행 취소 스택에서 커멘드 오브젝트가 추적된다.

PlantUML Diagram

입력을 받는 부분의 RunPlayerCommand에서는 다음과 같은 일이 발생한다.

private void RunPlayerCommand(PlayerMover playerMover, Vector3 movement)
{
  if (playerMover == null)
  {
    return;
  }

  if (playerMover.IsValidMove(movement))
  {
    ICommand command = new MoveCommand(playerMover, movement);
    CommandInvoker.ExecuteCommand(command);
  }
}

#2 장점과 단점

#2 응용

CommandInvoker는 커맨드 오브젝트의 내부 작업을 보지 않고 오직 ExecuteUndo만 호출한다. 생성자 호출할 때에만 전달한다.

comments powered by Disqus