Priv's Blog
7. Command pattern 본문
1. 커맨드 패턴 (Command pattern)
본래의 GoF 패턴 중 하나인 커맨드 패턴(Command Pattern)은 특정한 일렬의 행동들을 추적하고자 할 때 유용한 패턴입니다. 게임을 플레이하는 도중에 되돌리기/다시 실행하기 기능을 구현하고 싶거나, 사용자의 입력 데이터를 시간순으로 리스트에저장하는 히스토리 기능을 구현하고 싶을 때 커맨드 패턴을 사용할 수 있습니다. 플레이어가 실제로 전략을 실행하기 전에 몇 턴을 미리 구상해 놓을 수 있는 전략 게임을 상상해 보세요. 그게 바로 커맨드 패턴이랍니다.
커맨드 패턴은 메서드를 직접 깨우는(invoking) 방식 대신, “커맨드 오브젝트(command object)”라고 불리는 하나 이상의 메서드로 캡슐화할 수 있게 해줍니다.
이러한 커맨드 오브젝트들을 큐(queue) 또는 스택(stack) 자료구조와 같은 컬렉션에 저장하는 것은 여러분이 해당 오브젝트들이 실행되는 타이밍을 직접 조정할 수 있게 해줍니다. 이 기능들은 작은 버퍼와 유사합니다. 여러분은 잠재적으로 일렬의 행동들을 나중에 시행되게 하거나 되돌릴 수 있습니다.
커맨드 패턴을 구현하기 위해서는 수행할 행동들을 담아둘 제너럴 오브젝트가 필요합니다. 이 커맨드 오브젝트에는 수행될 로직과 수행한 행동을 어떻게 되돌릴 것인지에 대한 것들이 포함됩니다.
2. 커맨드 오브젝트와 커맨드 호출자(Invoker)
커맨드 오브젝트를 구현하는 방법은 몇 가지가 있습니다. 여기서는 인터페이스를 사용하는 방법에 대해 알아보도록 하겠습니다:
public interface ICommand
{
void Excute();
void Undo();
}
지금의 경우에는 게임을 플레이하면서 일어나는 모든 행동이 ICommand 인터페이스를 구현해야 합니다. (물론 추상 클래스를통해 인터페이스를 구현해도 됩니다)
각각의 커맨드 오브젝트들은 각자 가지고 있는 Execute 메서드와 Undo 메서드에 대한 책임을 지게 됩니다. 그러므로, 여러분의 게임에 더 많은 커맨드를 추가해도 기존의 커맨드에는 영향을 미치지 않을 것입니다.
커맨드를 실행하고 되돌리기 위한 다른 클래스가 필요할 것입니다. CommandInvoker 클래스를 만들어 줍시다. 커맨드 오브젝트들의 순서를 저장하는 스택을 되돌리기 위한 ExecuteCommand 메서드와 UndoCommand 메서드도 추가해 줍니다.
public class CommandInvoker
{
private static Stack<ICommand> undoStack = new Stack<ICommand>();
public static void ExecuteCommand(ICommand command)
{
command.Execute();
undoStack.Push(command);
}
public static void UndoCommand()
{
if (undoStack.Count > 0)
{
ICommand activeCommand = undoStack.Pop();
activeCommand.Undo();
}
}
}
3. 예시: 되돌리기가 가능한 움직임
자, 플레이어가 미로를 탐험하는 게임을 만들고 싶다고 상상해 봅시다. 플레이어의 위치를 이동시키는 책임을 지니고 있는PlayerMover 스크립트를 작성해야 합니다:
public class PlayerMover : MonoBehaviour
{
[SerializeField] private LayerMask obstacleLayer;
private const float boardSpacing = 1f;
public void Move(Vector3 movement)
{
transform.position = transform.position + movement;
}
public bool IsValidMove(Vector3 movement)
{
return !Physics.Raycast(transform.position, movement, boardSpacing, obstacleLayer);
}
}
Vector3 타입의 매개변수가 Move 메서드에 제공되고, 이 매개변수를 통해 사방위를 기반으로 플레이어를 움직이게 할 것입니다. 또한 레이캐스트(raycast)를 통해 적합한 LayerMask 안에 있는 벽들을 감지할 것입니다. 물론, 커맨드 패턴에 적용하고 싶은 것을 구현하는 것과 패턴 그 자체와는 별개입니다.
커맨드 패턴을 따르기 위해서는 PlayerMover의 Move 메서드를 오브젝트로 저장해야 합니다. Move 메서드를 직접 호출하는것 대신, 새로운 클래스, MoveCommand를 생성하고 ICommand 인터페이스를 구현해야 합니다.
public class MoveCommand : ICommand
{
PlayerMover playerMover;
Vector3 movement;
public MoveCommand(PlayerMover player, Vector3 moveVector)
{
this.playerMover = player;
this.movement = moveVector;
}
public void Execute()
{
playerMover.Move(movement);
}
public void Undo()
{
playerMover.Move(-movement);
}
}
ICommand 인터페이스는 여러분이 수행하고자 하는 동작들에 대한 데이터를 저장하는 Execute 메서드를 요구합니다. 여러분이 수행하고자 하는 로직이든지 어떤 것이든지 간에, movement 벡터를 통해 Move 메서드를 깨워야(Invoke) 합니다.
또한 ICommand 인터페이스는 씬을 이전 상태로 되돌리기 위한 Undo 메서드도 필요로 합니다. 이 경우, Undo 로직은movement 벡터를 빼서 본질적으로 플레이어를 반대 방향으로 이동시킵니다.
MoveCommand 메서드는 실행해야 할 모든 매개변수를 저장합니다. 생성자를 통해 이들을 설정하세요. 이 경우, 여러분은 적합한 PlayerMovet 컴포넌트와 movement 벡터를 저장해야 합니다.
커맨드 오브젝트를 한 번 생성하고 필요한 매개변수들을 저장하면, CommandInvoker 클래스의 정적 메서드인ExecuteCommand와 UndoCommand를 사용하여 MoveCommand를 전달하세요. 이는 MoveCommand 클래스의Execute 메서드 또는 Undo 메서드를 실행하고 undoStack에 저장된 커맨드 오브젝트를 추적할 것입니다.
InputManager는 PlayerMover의 Move 메서드를 직접 호출하지 않습니다. 그 대신, 별도의 메서드, RunMoveCommand를 추가하여 새로운 MoveCommand오브젝트를 생성한 뒤, CommandInvoker에 이를 전달합니다.
private void RunPlayerCommand(PlayerMover playerMover, Vector3 movement)
{
if (playerMover == null)
{
return;
}
if (playerMover.IsValidMove(movement))
{
ICommand command = new MoveCommand(playerMover, movement);
CommandInvoker.ExecuteCommand(command);
}
}
그런 다음, 버튼들의 여러 onClick 이벤트를 설정하여 4개의 movement 벡터를 가지고 RunPlayerCommand가 호출되도록만듭니다.
샘플 프로젝트를 직접 열어서 InputManager의 세부적인 사항들을 살펴보거나 여러분만의 키보드 또는 게임 패드의 입력값을사용하도록 코드를 수정해 보세요. 이제 플레이어는 미로를 탐색할 수 있게 되었습니다. Undo 버튼을 클릭하면 시작 지점의 사각형 위치로 돌아갈 수 있습니다.
4. 커맨드 패턴의 장단점
리플레이와 되돌리기 기능을 구현하는 것은 커맨드 오브젝트의 컬렉션을 생성하는 것만큼이나 간단합니다. 또한 커맨드 버퍼를사용하여 특정한 작업의 일렬을 되돌리는 것도 가능합니다.
예를 들어, 특정한 버튼을 순서대로 입력하면 콤보 이동 또는 콤보 공격이 발동되는 격투 게임을 개발한다고 가정해 봅시다. 플레이어가 수행한 행동들을 커맨드 패턴을 통해 저장하면 더 쉽게 이러한 콤보 액션 시스템을 구현할 수 있습니다.
그러나, 커맨드 패턴도 다른 디자인 패턴과 마찬가지로 더 많은 구조를 요구합니다. 여러분은 이러한 추가적인 클래스와 인터페이스들이 게임 내에 커맨드 오브젝트를 개발함으로써 충분한 이점을 제공하는지를 판단해 결정하셔야 합니다.
5. 커맨드 패턴 개량하기
기초를 익혀두면, 문맥에 따라 커맨드의 타이밍을 조절하고 상황에 따라 연속적으로 이를 재생하거나, 역순으로 재생할 수도 있습니다.
아래 항목들을 고려하여 커맨드 패턴을 개량해 보세요:
- 더 많은 커맨드 생성하기: , MoveCommand는 커맨드 오브젝트의 한 가지 타입만 포함하고 있습니다. 여러분은 원하는만큼 ICommand 인터페이스를 구현하는 새로운 커맨드 오브젝트를 만들고 CommandInvoker를 사용하여 이를 추적할 수 있습니다.
- 다시 하기(redo) 기능은 다른 스택을 추가하여 구현할 수 있습니다: 커맨드 오브젝트를 되돌릴 때, 별도의 스택에 값을 삽입(push)하도록 만들면 다시 하기(redo) 기능을 구현할 수 있습니다. 이 방법을 사용하면 간단하게 수행한 행동들을 되돌리거나다시 수행하는 시스템을 구현할 수 있습니다. 사용자가 완전히 새로운 행동을 할 때는 다시 하기 스택에 저장된 값을 지워주어야합니다. (함께 제공되는 샘플 프로젝트에서 구현 방법을 살펴보실 수 있습니다).
- 커맨드 오브젝트의 버퍼를 위한 다른 컬렉션 사용하기: 만약 선입선출(FIFO) 기반의 자료 구조를 다루고 싶을 때는 큐(Queue)가 더 적합할 수 있습니다. 만약 리스트를 사용한다면 현재 활성화된 인덱스를 추적해야 합니다. 활성 인덱스 전의 커맨드는 undo 명령을, 인덱스 이후의 커맨드는 redo 명령을 수행할 수 있습니다.
- 스택의 크기를 제한하기: Undo와 Redo 명령은 순식간에 통제 범위를 벗어날 수 있습니다. 스택의 크기를 마지막 커맨드의번호로 지정하세요.
- 필수적인 매개변수를 생성자로 전달하세요: 이는 로직의 캡슐화에 도움이 됩니다. MoveCommand 예시에서 살펴봤던 것처럼 말이죠.
CommandInvoker와 같은 다른 외부 오브젝트들은 커맨드 오브젝트의 내부 동작 구조를 볼 수 없습니다. 단순히 실행 또는 되돌리기 기능만 호출(Invoking)할 뿐입니다. 커맨드 오브젝트에는 생성자를 호출할 때 필요한 모든 데이터를 넘겨주세요.
수고하셨습니다!
'Unity Learn > Game Programming Patterns' 카테고리의 다른 글
9. Observer pattern (0) | 2023.11.23 |
---|---|
8. State pattern (0) | 2023.11.20 |
6. Singleton pattern (0) | 2023.10.31 |
5. Object pool (0) | 2023.10.23 |
4. Factory pattern (0) | 2023.10.11 |