이번엔 디자인 패턴 중에서 커맨드 패턴을 만들어 보겠습니다.
먼저 이 커맨드 패턴을 이용해서 사용자가 입력한 동작을 저장해두고 다시 리플레이 할수있도록
간단 하게 시스템을 만들겠습니다.
여기서 만드는 시스템은 진짜 리플레이로 사용하는 용도는 아니고 그냥 커맨드 패턴을 사용 예시입니다.
데이터를 캡슐화 해서 다른 객체로 전달 하는 과정을 보여주는 과정 입니다.
캡슐화에 대해서 잘 모른다면 한번 공부 하고오시면 좋을꺼같습니다.
커맨드 패턴 기본 구조
참조 - 위키 백과
위에 사진에서 보면
invoker 호출자
receiver 명령받아서 실행할 객체
Command 받은 ConcreteCommand 가있고 invoker 가 호출 할수있는 execute 매서드를 보유하고있다.
더 자세히 보는건 아래에 코드로 보면서 다시 설명할 예정이다.
커맨드 패턴의 장단점
장점
분리: 커맨드 패턴은 호출과 작업하는 객체를 분리가 가능하다.
입력 대기: 되돌리기 다시 하기 기능 / 매크로 / 명령 큐 등등 구현이 가능해서 사용자가 입력한 입력을 큐에 넣는 작업을 용이 하게 할수있다.
단점
복잡성: 하나에 명령이 하나의 클래스로 존재 한다. 입력이 많아지는경우 이 모든게 다 클래스로 하나하나 나눠서 만들어 줘야 하기에 유지 보수 적으로 패턴을 잘 이해해서 사용해야한다.
이런 장 단점으로 커맨드 패턴을 사용하는경우는
실행취소: 대부분 텍스트와 이미지 앱 같은 경우에서 보는 ctrl + z 같은 실행 취소 y 같은 재실행 같은 시스템 구현한다.
매크로: 공격 또는 방어 여러스킬을 이어서 기록하고 자동으로 입력할수있게 기록하는 매크로 기록 시스템에서도 사용 가능하다.
자동화: ai 적이 자동으로 또는 순차적으로 할 행동을 미리 입력해서 사용이 가능하다.
이제 커맨드 패턴을 이용해서 간단한 리플레이 기능을 만들어보자.
먼저 Command들을 부모 클래스인 추상 클래스를 구현한다.
Command.cs
public abstract class Command
{
public abstract void Execute();
}
그아래에있는 이제 커맨드를 받아서 처리 하는 클래스 4개를 만들껀데 상하좌우 4방향으로 이동하는 클래스를 만들예정이다.
코드 4가지를 연속으로 보여 주겠습니다. 4가지 전부 Command 클래스를 상속 받아서 구현했다.
PlayerController는 아래에 이어서 만들예정이니 에러 나도 그냥 넘어가자
MoveDown.cs
public class MoveDown : Command
{
private PlayerController _controller;
public MoveDown(PlayerController controller)
{
_controller = controller;
}
public override void Execute()
{
_controller.Movedown();
}
}
MoveUp.cs
public class MoveUp : Command
{
private PlayerController _controller;
public MoveUp(PlayerController controller)
{
_controller = controller;
}
public override void Execute()
{
_controller.Moveup();
}
}
TurnLeft.cs
public class TurnLeft : Command
{
private PlayerController _controller;
public TurnLeft(PlayerController controller)
{
_controller = controller;
}
public override void Execute()
{
_controller.Turn(PlayerController.Direction.Left);
}
}
TurnRight.cs
public class TurnRight : Command
{
private PlayerController _controller;
public TurnRight(PlayerController controller)
{
_controller = controller;
}
public override void Execute()
{
_controller.Turn(PlayerController.Direction.Right);
}
}
이렇게 각 명령을 다 개별 클래스로 캡슐화 해서 분리 했다.
리플레이 시스템을 작동할수있는 Invoker 호출자를 이어서 작성 해보자.
여기서는 C#에있는 SortedList를 사용한다.
Invoker.cs
public class Invoker : MonoBehaviour
{
private bool _isRecording;
private bool _isReplaying;
private float _replayTime;
private float _recordingTime;
private SortedList<float, Command> _recordedCommands = new SortedList<float, Command>();
public void ExecuteCommand(Command command)
{
command.Execute();
if (_isRecording)
_recordedCommands.Add(_recordingTime, command);
Debug.Log($"record Time : {_recordingTime}");
Debug.Log($"record command : {command}");
}
public void Record()
{
_recordingTime = 0.0f;
_isRecording = true;
}
public void Replay()
{
_replayTime = 0.0f;
_isReplaying = true;
if (_recordedCommands.Count <= 0)
Debug.LogError("No commands to replay!");
_recordedCommands.Reverse();
}
private void FixedUpdate()
{
if (_isRecording)
_recordingTime += Time.fixedDeltaTime;
if (_isReplaying)
{
_replayTime += Time.fixedDeltaTime;
if (_recordedCommands.Any())
{
if (Mathf.Approximately(_replayTime, _recordedCommands.Keys[0]))
{
Debug.Log($"replay Time : {_isReplaying}");
Debug.Log($"reply command : {_recordedCommands.Values[0]}");
_recordedCommands.Values[0].Execute();
_recordedCommands.RemoveAt(0);
}
}
else
{
_isReplaying = false;
}
}
}
}
위에 코드를 잠시 설명 해주면
Invoker가 새로운 커맨드를 실행 할때마다 _recordedCommands 목록에 추가해주고
_isRecording 를 둬서 기록 중일때만 추가하게 둔다.
FixedUpdate()를 사용해서 커맨드를 기록하고 리플레이 했다.
여기선 커맨드 패턴을 익히기 위해서 리플레이와 커맨드 패턴을 같이 사용했는데 역시 리플레이와 커맨드 패턴 자체도 나눠서 기록 저장할수있게 별도 클래스도 두는것을 추천한다.
그러면 이제 입력값을 Invoker에게 호출 할수있는 InputHandler 클래스를 구현한다.
InputHandler.cs
public class InputHandler : MonoBehaviour
{
private Invoker _invoker;
private bool _isReplaying;
private bool _isRecording;
private PlayerController _playerController;
private Command _buttonA, _buttonD, _buttonW, _buttonS;
void Start()
{
_invoker = gameObject.AddComponent<Invoker>();
_playerController = FindObjectOfType<PlayerController>();
_buttonA = new TurnLeft(_playerController);
_buttonD = new TurnRight(_playerController);
_buttonW = new MoveUp(_playerController);
_buttonS = new MoveDown(_playerController);
}
void Update()
{
if (!_isReplaying && _isRecording)
{
if (Input.GetKeyUp(KeyCode.A))
_invoker.ExecuteCommand(_buttonA);
if (Input.GetKeyUp(KeyCode.D))
_invoker.ExecuteCommand(_buttonD);
if (Input.GetKeyUp(KeyCode.W))
_invoker.ExecuteCommand(_buttonW);
if (Input.GetKeyUp(KeyCode.S))
_invoker.ExecuteCommand(_buttonS);
}
}
void OnGUI()
{
if (GUILayout.Button("Start Recording"))
{
_playerController.ResetPosition();
_isReplaying = false;
_isRecording = true;
_invoker.Record();
}
if (GUILayout.Button("Stop Recording"))
{
_playerController.ResetPosition();
_isRecording = false;
}
if (!_isRecording)
{
if (GUILayout.Button("Start Replay"))
{
_playerController.ResetPosition();
_isRecording = false;
_isReplaying = true;
_invoker.Replay();
}
}
}
}
코드를 보면 커맨드를 등록하고 wasd 로 Invoker를 이용해서 커맨드 호출 및 실행을 하게 된다.
GUI로 간단한 버튼을 둬서 리플레이 기능 관련된 동작을 심어주었다.
이제 아마 PlayerController 가 없어서 에러가 날텐데 이어서 만들어 주겠습니다.
PlayerController.cs
public class PlayerController : MonoBehaviour
{
public enum Direction
{
Right = -1,
Left = 1
}
public void Moveup()
{
transform.Translate(Vector3.up);
}
public void Movedown()
{
transform.Translate(Vector3.down);
}
public void Turn(Direction direction)
{
if (direction == Direction.Left)
transform.Translate(Vector3.left);
if (direction == Direction.Right)
transform.Translate(Vector3.right);
}
public void ResetPosition()
{
transform.position = new Vector3(0.0f, 0.0f, 0.0f);
}
}
클래스를 구조는 보인다. 커맨드에서 Moveup, Movedown, Turn 이 3가지를 호출 하는 구조이다.
이러면 나온 에러는 다 사라지고 테스트할 준비는 이제 끝났다.
유니티로 가서 테스트 해보자.
씬하나 열고 Cube 하나 만든 다음에
InputHandler와 PlayerController 를 넣어주자.
그후 실행 하게 되면 버튼이 3개 나오는데
Start Recording 눌러서 녹화하고 Start Replay 눌러서 하던 행동이 그대로 나오는지 확인해보자
여기서 나오는 커맨드 패턴을 이용한 리플레이는 그냥 보여주기 위해 만든거기때문에 더 낳은 구조를 가능 커맨드 패턴이 존재한다.
여기서 구조만 확립하고 넘어가서 더 심화된 패턴을 공부해보자.
참조