목차
커맨드 패턴
커맨드 패턴이란, 모든 동작(실행된 무언가)을 객체화 시킴으로써 어떤, 명령이 발생했을 때 해당 동작을 실행하는 패턴입니다.
만약, 아래와 같은 코드가 있다고 합시다.
public class CharacterController : MonoBehaviour
{
private void Update()
{
if(Input.GetKeyDown("space"))
{
Jump();
}
}
private void Jump()
{
//점프 코드
}
}
C#
복사
만약 캐릭터의 컨트롤러 위와 같이, 커맨드 패턴을 쓰지 않고 개발했다면 Jump는 캐릭터 컨트롤러에 종속됩니다.
즉, 어떠한 명령과 동작 자체가 종속되어있는 것입니다.
커맨드 패턴은 동작을 객체화 시킴으로써, 명령과 동작을 분리합니다.
public class InputCommandManager : MonoBehaviour
{
private CharacterController _characterController;
private Command _jumpCommand;
private void Update()
{
if(Input.GetKeyDown("space"))
{
_jumpCommand.Excite(_characterController);
}
}
public void SetCharacterController(CharacterController characterController)
{
_characterController = characterController;
if(_jumpCommand.isNull()) _jumpCommand = new JumpCommand();
}
}
C#
복사
위 처럼, Jump라는 동작을 객체화 시킴으로써 Jump의 동작을 CharacterController의 명령에 종속되지 않게 합니다.
커맨드 패턴 다이어 그램
커맨드 패턴의 기본 클래스를 살펴보겠습니다.
•
Invoker(호출자) : 명령을 실행하는 방법을 알고, 실행한 명령을 등록할 수 있는 객체입니다.
•
Receiver(수신자) : 명령을 받아서 수행하는 객체입니다.
•
Command(커맨드 베이스) : 동작을 객체화 시킬 때, 무조건 상속받아야될 인터페이스입니다.
•
ConcreteCommand(커맨드 객체) : 실제 동작을 객체화 시킨 인스턴스입니다.
커맨드 패턴 장단점
장점
1.
분리 : 커맨드 패턴은 실행 방법을 아는 객체에게서 작업을 호출하는 객체를 분리할 수 있습니다. 이 러한 이유로 즐겨찾기, 시퀀스 작업을 수행하는 중개자를 추가할 수 있습니다.
2.
시퀀싱 : 커맨드 패턴은 되돌리기/다시 하기 기능, 매크로, 명령 큐의 구현을 허용하고 사용자의 입력을 큐에 넣는 작업을 용이하게 합니다.
단점
1.
복잡성 : 각 명령이 그 자체로 클래스입니다. 즉, 객체로써 존재합니다. 모든 동작들은 수많은 클래스들로 이루어져 있어 만약 동작이 많다면, 그 만큼 클래스가 많아지게 됩니다. 모든 동작들에 대해 유지수를 위해 커맨드 패턴을 완벽하게 이해야됩니다.
커맨드 패턴을 사용하는 경우
•
실행 취소 : 대부분 텍스트와 이미지 에디터에서 볼 수 있는 실행 취소 및 재실행 시스템을 구현합니다.
ㄴ 실행 취소, 재실행은 메먼토 패턴으로도 구현할 수 있습니다. 여기서 메맨토 패턴에서 스택에 기록하는 각, 동작들이 커맨드 패턴으로 구현된 동작 객체가 되는 것이죠.
•
매크로 : 공격 혹은 방어 콤보를 기록하고 자동으로 입력 키에 적용하여, 실행할 수 있는 매크로 기록 시스템을 구현합니다.
ㄴ 제가 스킬 1,2,3을 매크로 1에 넣어뒀다면 , 해당 스킬들은 SkillCommandManager의 스택에 쌓여 각각의 상황에 맞춰 실행됩니다.
•
자동화 : 봇이 자동으로 그리고 순차적으로 실행할 명령 집합을 기록하는 자동화 과정 혹은 행동을 구현합니다.
ㄴ 봇 자체의 모든 행동을 스테이트 패턴으로 기록하고 스테이트에 따라 어떠한 행동을 커맨드 패턴의 행동으로 구현한다는 얘기입니다. 좀 어렵지만, 여러 동작들을 객체화 시킴으로써, 누가 어떤 상황에 어떤 동작을 몇 개 실행할 것 인지를 결정합니다.
커맨드 패턴을 이용한 리플레이 시스템
커맨드
public interface Command
{
public void Execute();
}
C#
복사
동작의 모든 클래스들은 해당 Command를 상속합니다.
만약, 다른 커맨드 abstract Class Base를 만들더라도 해당 Commnad 인터페이스는 반드시 상속받아야됩니다.
public class Attack : Command
{
private Controller _controller = null;
public Attack(Controller controller)
{
_controller = controller;
}
public override void Excute()
{
_controller.Attack();
}
}
C#
복사
public class Move : Command
{
private Controller _controller = null;
public Move(Controller controller)
{
_controller = controller;
}
public override void Excute()
{
_controller.Move();
}
}
C#
복사
public class Defence : Command
{
private Controller _controller = null;
public Defence(Controller controller)
{
_controller = controller;
}
public override void Excute()
{
_controller.Defence();
}
}
C#
복사
이처럼 간단한 커맨드 패턴 동작들을 만들었습니다. 해당 동작 들을 사용하면, 컨트롤러는 각각 Attack,Move,Defence 라는 동작을 실행하게 될 것입니다.
public class Invoker : MonoBehavipur
{
private bool _isRecord = false; // 현재 레코딩 중인지
private bool _isReplay = false; // 현재 리플레이 중인지
private float _elapsedRecordTime = 0.0f; // 레코드 타임
private float _elapsedReplayTime = 0.0f; // 리플레이 타임
private SortedList<(float recordTime, Command currentCommand)> _recordsCommand = new ();
public void Excute(Command command)
{
command.Excute();
if(_isRecord) _recordsCommand.Add((_elapsedRecordTime, command));
}
public void Record()
{
_elapsedRecordTime=0.0f;
_isRecord = false;
}
public void FixedUpdate()
{
_elapsedRecordTime += Time.fixedDeltaTime;
_elapsedReplayTime += Time.fixedDeltaTime;
if(_isReplay)
{
if(_recordsCommand.Count() == 0) return;
if(Mathf.Approximately(_elapsedReplayTime, _recordsCommand.First().recordTime)
_recordsCommand.First().Excute();
_recordsCommand.RemoveAt(0);
}
}
}
C#
복사
위와 같이 리플레이 시스템과 기본적인 호출자 시스템을 갖고 있는 Invoker를 만들었습니다.
이제, 플레이도중 Excute되는 모든 커맨드 동작들은 Invoker를 통해 _recordsCommand에 기록되며, Replay가 동작할 때, 다시 차례로 Replay가 작동합니다.