본 글은 유니티에서 발간한 전자책 'Level up your programming with game programming patterns'을 정리한 글입니다. 원문은 다음 링크에서 확인하실 수 있습니다.
https://unity.com/kr/resources/level-up-your-code-with-game-programming-patterns
1. Command pattern
커맨드 패턴은 특정 일련의 작업을 추적하고 싶을 때 유용한 패턴이다. undo/redo 기능을 사용하거나 입력 이력을 목록으로 유지하는 게임을 플레이한 적이 있다면 커맨드 패턴이 사용되었을 것이다. 커맨드 패턴을 도식으로 나타내면 다음과 같다.
커맨드 패턴은 메서드를 직접 호출하는 대신에, 하나 혹은 그 이상의 메서드 호출을 커맨드 객체(Command object)에 캡슐화한다. 이러한 커맨드 객체를 큐 또는 스택과 같은 집합에 저장하여 이들의 실행 시점을 제어할 수 있다. 이것은 작은 버퍼로써 작동하며, 이후 일련의 작업을 나중에 재생하거나 취소할 수 있다.
커맨드 패턴을 구현하기 위해선 수행할 작업을 담을 커맨드 객체가 필요하다. 커맨드 오브젝트는 어떤 로직을 수행할 것인지, 어떻게 작업을 취소할 것인지를 포함한다.
2. The command object and command invoker
이를 구현하기 위한 많은 방법들이 존재하지만, 예시로 인터페이스를 사용하는 버전을 소개한다.
모든 커맨드 객체가 이러한 ICommand 인터페이스를 사용하게 될 것이다. 각 커맨드 객체는 고유의 Execute와 Undo 메서드를 가지므로, 다른 커맨드들을 추가하는 것이 기존의 커맨드들에 영향을 미치지 않게 된다.
이러한 커맨드들을 실행하고 취소할 클래스가 따로 필요할 것이다.
CommandInvoker는 일련의 커맨드 객체들을 보관하기 위한 undo 스택을 가진다. ExecuteCommand의 파라미터로 ICommand를 전달하여 해당 커맨드를 실행하고 스택에 푸쉬하며, UndoCommand에선 스택에서 커맨드를 꺼내 해당 커맨드를 취소한다.
3. Example: Undoable movement
당신의 게임에서 미로 속 플레이어를 움직이고 싶다고 가정해보자. 플레이어의 이동을 담당하는 PlayerMover 클래스를 다음과 같이 만들 수 있을 것이다.
플레이어의 이동이 커맨드 패턴을 따르게 하기 위해선 위 Move 메서드를 커맨드로 삼아야 될 것이다. Move 메서드를 직접 호출하는 대신에 ICommand 인터페이스를 구현하는 MoveCommand 클래스를 다음과 같이 만들 수 있다.
MoveCommand는 ICommand에 따라 적절한 Execute와 Undo 메서드를 구현하며, 생성자에서 Execute와 Undo에 필요한 파라미터를 넘겨 받는다.
위 클래스를 이용하여 커맨드 객체를 만들고 필요한 파라미터들을 저장하고 나면, CommandInvoker의 static ExecuteCommand와 UndoCommand 메서드를 이용해 커맨드를 스택에 전달하여 실행하거나 취소할 수 있다.
예시의 클래스 다이어그램은 다음과 같다.
입력을 처리하는 InputManager는 PlayerMove의 Move 메서드를 직접 호출하는 게 아니라 다음의 RunPlayerCommand 메서드를 호출하여 커맨드를 통해 플레이어의 이동을 제어한다.
RunPlayerCommand 메서드는 MoveCommand의 객체, 즉 새로운 커맨드 객체를 만들고 이를 CommandInvoker에 전달하여 실행한다. 이후 적절한 키 입력과 RunPlayerCommand 메서드를 연결하면 된다. 전체 코드는 다음에서 확인할 수 있다.
4. Pros and cons
작업을 다시 실행하는(replay) 기능과 되돌리는(undo) 기능을 간단히 구현할 수 있다. 또한, 커맨드 버퍼(스택)를 사용하여 특정 조작으로 작업을 순차적으로 재생할 수도 있다. 가령 특정 버튼을 순서대로 누르면 콤보 동작을 발동하는 격투 게임에서, 커맨드 패턴을 사용하여 플레이어의 작업을 저장하면 이러한 콤보 설정이 훨씬 간단해질 것이다.
반면에 커맨드 패턴은 다른 디자인 패턴과 마찬가지로 추가적인 구조를 도입하게 한다. 이러한 추가적인 클래스와 인터페이스가 커맨드 패턴을 이용하는 데에 충분한 이점을 제공하는지 고려해보아야 한다.
5. Improvements
커맨드 패턴을 사용할 때 다음과 같은 사항들을 고려해볼 수 있다.
- 더 많은 커맨드를 사용해라.
위 샘플 프로젝트는 MoveCommand라는 하나의 커맨드 타입만을 사용한다. ICommand를 구현하는 여러 커맨드 객체를 생성하고 CommandInvoker를 사용하여 그들을 추적할 수 있다.
- redo 기능을 추가하기 위해선 새로운 스택을 추가해야 한다.
redo 기능을 추가하기 위해 redo를 추적하는 별도의 스택을 만들어야 한다. 그리고 어떤 커맨드를 undo할 때, 그 커맨드를 undo 스택에서 꺼낸 후 redo 스택에 푸쉬하고, redo할 때 반대로 하면 된다. 새로운 커맨드가 들어올 때는 redo 스택을 비워야 할 것이다.
- 커맨드 버퍼에 다른 종류의 집합을 사용할 수도 있다.
first in, first out (FIFO)를 원한다면 queue를 쓰는 것이 편리할 수 있다. list를 사용한다면 현재 활성화된 인덱스를 추적하여 해당 인덱스 이전의 커맨드들을 undo 할 수 있고, 이후의 커맨드들을 redo 할 수 있도록 하는 방식으로 구현할 수 있다.
- 스택의 사이즈를 제한해라.
undo 및 redo 작업이 너무 많이 쌓이면 불필요하게 메모리를 많이 사용할 수 있다. 스택의 사이즈를 게임에 따라 필요한 만큼으로 제한해라.
- 커맨드의 생성자에 필요한 파라미터들을 모두 전달해라.
위 예시의 MoveCommand에서 확인할 수 있듯이, 커맨드의 생성자에 파라미터들을 전달하는 것이 로직을 캡슐화하는 데에 도움을 준다.
'Unity > Study' 카테고리의 다른 글
Unity C#) 디자인 패턴 가이드 #7. 옵저버 패턴(Observer pattern) (2) | 2024.03.19 |
---|---|
Unity C#) 디자인 패턴 가이드 #6. 스테이트 패턴(State pattern) (0) | 2024.03.17 |
Unity C#) 디자인 패턴 가이드 #4. 싱글톤 패턴(Singleton pattern) (3) | 2024.03.05 |
Unity C#) 디자인 패턴 가이드 #3. 오브젝트 풀(Object pool) (1) | 2024.03.01 |
Unity C#) 디자인 패턴 가이드 #2. 팩토리 패턴(Factory pattern) (0) | 2024.02.21 |