일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Zenject
- two pointer
- Modern C++
- Project
- stack
- level1
- Gold
- level3
- 8-Puzzle
- Flyweight Pattern
- solid 원칙
- programmers
- algorithm
- binary search
- effective C++
- SWEA
- 프로세스 상태
- Unity
- dirtyflag pattern
- 3D RPG
- Silver
- 프로그래머스
- BFS
- Euclidean
- Bronze
- knapsack Problem
- trie
- BOJ
- PrefixSum
- LEVEL2
- Today
- Total
Patrick's Devlog
[Unity] Command Pattern 본문
Command Pattern
커맨 패턴은 커맨드들을 패킹해서 택배처럼 나르는 것으로 비유할 수 있다. 하나의 명령을 객체에 직접 호출하는 것이 아닌, 패킹해 쏘는 방식이다. 해당 패턴은 각각의 요청을 객체의 형태로 캡슐화해 명령을 실행하는 객체(Receiver)와 명령을 내리는 객체(Invoker)로 분리되어 있다. 이러한 특성으로 인해 단일 책임 원칙을 따르게 된다. 새로운 명령을 추가할 때마다 기존 코드를 수정할 필요없이 새로운 ConcreteCommand 클래스를 추가하면 된다. 이 특성 또한 SOLID 원칙에서 개방 폐쇠 원칙을 따른다.
커맨드 패턴은 서로 다른 요청들을 큐에 저장하거나 로그로 기록하는 등의 처리가 가능하며, 결합도를 낮추고 명령을 추상화함으로써 코드의 유연성과 재사용성을 높인다. 그리고 동일한 명령에 대해 다양한 매개변수를 사용할 수 있어 명령을 더욱 유연하게 정의할 수 있다. 로그나 명령어 등을 기록하고 처리하다보니 툴을 제작할 때 유용하게 사용된다. 게임에서는 리플레이 등에서 사용될 수 있다.
예시
커맨드 패턴을 사용하지 않고 Input Manager와 PlayerMover를 구현한다고 하면 아래의 그림과 같이 나오게 된다.
InputManager에서 PlayerMover를 바로 호출해서 사용이 되나, 커맨드 패턴은 그런 식으로 가는 것이 아니라 아래의 그림처럼 여러 단계를 거치게 된다.
PlayerMover를 InputManager가 직접호출하지 않고 있으므로 강하게 참조하고 있지 않는다. 이를 코드로 구성하면 아래와 같다.
public interface ICommand {
public void Execute();
public void Undo();
}
// ---
public class MoveCommand : ICommand { // Command Interface 구체화
private PlayerMover _playerMover; // 참조용 (직접 가지고 있지 않음)
private 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); // 뒤집어서 이동(Undo)
...
}
}
지금은 Move로만 예시를 들었으나, 추후 구현할 때 Attack이나 다양한 곳에서 사용될 수 있다. ICommand를 상속받아 확장하면 된다.
대략 그림처럼 커맨드가 호출되면 Stack에 담고 Undo 시 Stack에서 빠져나오는 코드를 예시에서 구현한다. 다만 유의해야 할 점은 Undo를 여러 번 진행했을 때, 새 커맨드를 진행해버리면 Redo는 다시할 수 없게 된다.
커맨드를 관리하는 코드는 아래와 같다.
public class CommandInvoker { // 커맨드 관리
private static Stack<ICommand> _undoStack = new Stack<ICommand>();
private static Stack<ICommand> _redoStack = new Stack<ICommand>();
// Stack을 지니고 호출 해줌
public static void UndoCommand() {
if (_undoStack.Cound > 0) {
ICommand activeCommand = _undoStack.Pop();
_redoStack.Push(activeCommand);
activeCommand.Undo(); // Undo 호출 -> MoveCommand에서는 Back이 실행됨
}
}
public static void RedoCommand() {
if (_redoStack.Count > 0) {
ICommand activeCommand = _redoStack.Pop();
_undoStack.Push(activeCommand);
activeCommand.Execute(); // Redo 호출 -> MoveCommand에서는 다시 Move 진행
}
}
public static void ExecuteCommand(ICommand command) { // Undo, Redo가 아닌 커맨드 실행
command.Execute();
_undoStack.Push(command); // Undo Stack에 push
_redoStack.Clear(); // redo stack clear
}
InputManager의 코드는 아래와 같다.
public class InputManager : MonoBehaviour {
[Header("Button Controls")]
[SerializeField] Button forwardButton;
[SerializeField] Button backButton;
... // 방향키, undo, redo 버튼 생성
[SerializeField] private PlayerMover player;
private void Start() {
// button set up
forwardButton.OnClick.AddListener(OnForwardInput);
backButton.OnClick.AddListener(OnBackInput);
...
}
private void RunPlayerCommand(PlayerMover playerMover, Vector3 movement) {
...
if (playerMover.IsValidMove(movement)) { // 이동 가능한 영역인지
ICommand command = new MoveCommand(playerMover, movement);
// 방향 및 이동(movement) 지정 및 커맨드 생성
CommandInvoker.ExecuteCommand(command);
// CommandInvoker에게 ExecuteCommand 실행
}
}
...
private void OnLeftInput() {
RunPlayerCommand(player, Vector3.left);
}
private void OnRightInput() {
RunPlayerCommand(player, Vector3.right);
}
private void OnUndoInput() {
CommandInvoker.UndoCommand();
}
private void OnRedoInput() {
CommandInvoker.RedoCommand();
}
}
코드는 그렇게 어렵지 않고, RunPlayerCommand에서 CommandInvoker에게 명령어를 보내주면 된다. 여기서, Stack을 두가지 사용하기 싫으면 Queue로도 구현할 수 있다.
위치대로 Undo, Redo를 실행해주면 된다. Queue를 이용해 커맨드 패턴을 사용하는 예제는 Unity 본사에서 제공하는 Level up your code : unity pattern combo 프로젝트를 참고하면 된다. 해당 프로젝트는 격투 게임을 예제로 커맨드 패턴이 적용되어 있다.
단점
명령, 호출자, 수신자 등으로 인해 복잡성이 증가하고, 명령마다 별도의 ConcreteCommand 클래스를 작성해야 하므로 클래스의 수가 증가하게된다. 그리고 명령 객체를 생성하고 관리하는 오버헤드가 발생한다.
참고 문서
'Unity > Design Pattern' 카테고리의 다른 글
[Unity] Object Pool Pattern (0) | 2024.10.03 |
---|---|
[Unity] Factory Pattern (0) | 2024.10.01 |
[Unity] Observer Pattern (0) | 2024.09.27 |
[Unity] State Pattern (1) | 2024.09.26 |
[Unity] SOLID 원칙 (0) | 2024.09.24 |