디자인패턴 - Command(명령) 패턴
Intro
Command(명령) 패턴이란 “요청자체를 캡슐화한 것” 이다.
처음엔 잘 와닿지 않았지만, 예제를 보면서 이해가 되었다.
예제
아래는 키 입력을 받는 코드이다.
public class InputHandler : MonoBehaviour
{
private void Update()
{
GetInput();
}
private void GetInput()
{
if (Input.GetKeyDown(KeyCode.Space)) Jump(); // 스페이스바 : 점프
else if (Input.GetKeyDown(KeyCode.A)) Attack(); // A : 공격
else if (Input.GetKeyDown(KeyCode.LeftControl)) Crouch(); // 좌컨트롤 : 웅크리기
else if (Input.GetKeyDown(KeyCode.LeftShift)) Run(); // 좌쉬프트 : 달리기
}
void Jump()
{
Debug.Log("점프");
}
void Attack()
{
Debug.Log("공격");
}
void Crouch()
{
Debug.Log("웅크리기");
}
void Run()
{
Debug.Log("달리기");
}
}
위 코드는 키를 입력받는 부분과 행동을 실행하는 부분이 결합되어있다.
게임에 따라 다르겠지만, 대부분의 게임은 키 변경 옵션을 지원하기때문에
이렇게 코드를 작성하게되면, 키 변경 기능을 만들기 어렵다.
따라서 키 입력을 받았을 때 수행하는 행동을 인스턴스화 해야한다.
명령패턴의 기본 형태
Command 인터페이스는 모든 행동들의 상위 클래스가 되는 인터페이스이다.
public interface Command
{
void Execute() {}
}
이제 각 행동마다 Command를 상속받은 하위 클래스를 만들어주면 된다.
public class Jump : Command
{
void Execute()
{
Debug.Log("점프");
}
}
public class Attack : Command
{
void Execute()
{
Debug.Log("공격");
}
}
public class Crouch : Command
{
void Execute()
{
Debug.Log("웅크리기");
}
}
public class Run : Command
{
void Execute()
{
Debug.Log("달리기");
}
}
이렇게 하면 InputHandler 에서는
-
입력과 행동 바인딩
-
입력을 받아 해당 입력에 매핑된 Command를 실행
두가지만 하면 된다.
public class InputHandler : MonoBehaviour
{
private Dictionary<KeyCode, Command> buttons = new();
private void Start()
{
buttons.Add(KeyCode.Space, new Jump());
buttons.Add(KeyCode.A, new Attack());
buttons.Add(KeyCode.LeftControl, new Crouch());
buttons.Add(KeyCode.LeftShift, new Run());
}
private void Update()
{
GetInput();
}
private void GetInput()
{
if (Input.GetKeyDown(KeyCode.Space)) buttons[KeyCode.Space].Execute();
else if (Input.GetKeyDown(KeyCode.A)) buttons[KeyCode.A].Execute();
else if (Input.GetKeyDown(KeyCode.LeftControl)) buttons[KeyCode.LeftControl].Execute();
else if (Input.GetKeyDown(KeyCode.LeftShift)) buttons[KeyCode.LeftShift].Execute();
}
}
딕셔너리를 사용해, 입력된 키를 검색하여 그에 매핑된 행동을 실행해주는 방식이다.
하지만 아직도 문제가 있다.
위 방식은 어떤 객체의 Command를 실행할 것인지가 정해져있다.
따라서, 가한 입력을 어느 오브젝트에서 실행할 것인지에 대한 정보를 Execute에서 받아줄 수 있도록 한다.
actor를 인자로 넘겨주는 형태
public interface Command
{
void Execute(in GameObject actor) {}
}
public class Jump : Command
{
void execute(in GameObject actor)
{
Debug.Log($"{actor.name} 점프");
}
}
public class Attack : Command
{
void execute(in GameObject actor)
{
Debug.Log($"{actor.name} 공격");
}
}
public class Crouch : Command
{
void execute(in GameObject actor)
{
Debug.Log($"{actor.name} 웅크리기");
}
}
public class Run : Command
{
void execute(in GameObject actor)
{
Debug.Log($"{actor.name} 달리기");
}
}
public class InputHandler : Monobehaviour
{
// ...
private GameObject actor;
private void GetInput()
{
if (Input.GetKeyDown(KeyCode.Space)) buttons[KeyCode.Space].Execute(actor);
else if (Input.GetKeyDown(KeyCode.A)) buttons[KeyCode.A].Execute(actor);
else if (Input.GetKeyDown(KeyCode.LeftControl)) buttons[KeyCode.LeftControl].Execute(actor);
else if (Input.GetKeyDown(KeyCode.LeftShift)) buttons[KeyCode.LeftShift].Execute(actor);
}
}
이렇게 하면
actor를 바꿔주는것으로, 조종할 오브젝트를 변경할 수 있다.
Undo
명령 패턴으로 가능한것이 실행취소(Undo) 기능이다.
메서드를 실체화 시켜놨기 때문에 이를 순서대로 저장할수도 있게된 것이다.
public interface Command
{
public void Execute() {}
public void Undo() {}
}
public class MoveUnit : Command
{
private Transform unit;
private Vector3 delta;
public MoveUnit(Transform unit, Vector3 delta)
{
this.unit = unit;
this.delta = delta;
}
public void Execute()
{
unit.Traslate(delta);
}
public void Undo()
{
unit.Translate(-1 * delta);
}
}
InputHandler 에서는 이동 명령을 호출할 때마다 새로운 MoveUnit를 생성하고,
생성된 MoveUnit를 저장함으로써 Undo를 할 수 있게된다.
public class InputHandler : MonoBehaviour
{
private GameObject actor;
private void Awake()
{
actor = gameObject;
}
private void Update()
{
GetInput();
}
public Command GetInput()
{
if(Input.GetKeyDown(KeyCode.UpArrow))
{
Command UpArrow = new MoveUnit(actor.transform, Vector3.up * Time.deltaTime);
return UpArrow;
}
return null;
}
}
방향키를 입력할 때마다 MoveUnit를 생성하고,
이를 스택에 저장하여 Undo를 할 때 가장 최근의 Command를 Pop 하는 방식으로 작업 취소를 한다.
댓글남기기