◎ 정의
커맨드 패턴(Command pattern)이란 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴이다. - 출처 : 위키백과 |
- 한마디로 쉽게 말하자면 메서드의 호출을 객체로 감싸서(캡슐화) 관리하는 패턴이다.
- 가령 캐릭터의 조작을 위한 기능을 구현할 때 가장 간단하게는 아래와 같이 작성할 것이다.
< Hero.cs >
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Hero : MonoBehaviour
{
private void Update()
{
CheckInputKey();
}
// 캐릭터 이동
void CheckInputKey()
{
if (Input.GetKey("a")) Attack();
else if (Input.GetKey("s")) Defend();
else if (Input.GetKey("d")) Jump();
else if (Input.GetKey("f")) Dash();
}
void Attack()
{
Debug.Log("캐릭터가 공격함.");
// 캐릭터 공격에 관한 처리
//...
}
void Defend()
{
Debug.Log("캐릭터가 방어함.");
// 캐릭터 방어에 관한 처리
//...
}
void Jump()
{
Debug.Log("캐릭터가 점프함.");
// 캐릭터 점프에 관한 처리
//...
}
void Dash()
{
Debug.Log("캐릭터가 돌진함.");
// 캐릭터 돌진에 관한 처리
//...
}
}
- 사용자가 입력키를 변경하는 기능을 지원하지 않는다면 위와 같이 작성해도 무방하겠지만 추후에 키 변경을
지원해야 한다면 MoveFoward() 나 MoveRight()와 같은 함수를 직접 호출하는 것이 아닌 특정 행동을 갖는
객체를 이용하여 동작하게끔 구현할 필요가 있다.
또한 객체로 관리하므로 명령어 실행은 물론, 실행한 명령어 저장, 취소, 재실행과 같은 처리 기능의 추가도 가능해진다.
◎ 활용
- 요청을 큐에 저장하는 방식으로 작업큐나 스케줄러와 같은 작업에 적용 가능
- 작업 내용을 로그로 기록하여 프로그램 실행 도중 에러 발생 시 롤백하는 기능에 적용
- 명령어 실행의 저장이 가능하므로 작업이 요청된 시점과 수행되는 시점을 분리하고자 할 때 응용이 가능
◎ 장점 / 단점
○ 장점
- 특정 작업에 대한 요청과 실제 작업을 수행하는 객체가 분리되어있으므로
시스템의 결합도가 낮아 확장성에 유리한 면을 갖는다. 즉 각 동작에 대한 객체들이 수정되어도 다른 객체가 영향을 받지 않는다.
○ 단점
- 각각의 명령에 대한 기능이 될 때마다 클래스를 추가해야 하므로 클래스의 개수가 무수히 늘어날 수 있다.
◎ 구현
○ 기본적인 커맨드 패턴의 적용
< Command.cs >
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Command
{
public virtual void Execute() { }
}
public class CommandAttack : Command
{
public override void Execute()
{
Attack();
}
void Attack()
{
Debug.Log("캐릭터가 공격함.");
// 캐릭터 공격에 관한 처리
//...
}
}
public class CommandDefend : Command
{
public override void Execute()
{
Defend();
}
void Defend()
{
Debug.Log("캐릭터가 방어함.");
// 캐릭터 방어에 관한 처리
//...
}
}
public class CommandJump : Command
{
public override void Execute()
{
Jump();
}
void Jump()
{
Debug.Log("캐릭터가 점프함.");
// 캐릭터 점프에 관한 처리
//...
}
}
public class CommandDash : Command
{
public override void Execute()
{
Dash();
}
void Dash()
{
Debug.Log("캐릭터가 돌진함.");
// 캐릭터 돌진에 관한 처리
//...
}
}
- 위의 <Hero.cs>에서 키 입력을 체크하는 부분에 캐릭터의 동작(Attack, Jump 등)을 한 번에 정의했던 구조와 달리 Command라는 부모 클래스를 선언하고 각각의 동작들은 Command 클래스를 상속받아서
Execute()를 재정의하여 각각의 기능이 실행되는 구조로 바꾸었다.
< Hero.cs >
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Hero : MonoBehaviour
{
// 사용할 각각의 버튼 객체
Command button_A, button_S, button_D, button_F;
private void Start()
{
InitCommand();
}
// 각 버튼의 객체에 적용할 클래스(기능)를 할당
void InitCommand()
{
button_A = new CommandAttack();
button_S = new CommandDefend();
button_D = new CommandJump();
button_F = new CommandDash();
}
private void Update()
{
// InitCommand()에서 할당 된 버튼객체의 Execute 메서드를 실행
if (Input.GetKey("a")) button_A.Execute();
else if (Input.GetKey("s")) button_S.Execute();
else if (Input.GetKey("d")) button_D.Execute();
else if (Input.GetKey("f")) button_F.Execute();
}
}
- 여기까지 커맨드 패턴을 적용시킬 경우 잘동작 하지만 해당 조작 기능을 적용시키는 주체가 고정되어있기 때문에
AttackCommand나 JumpCommand는 오로지 지정한 캐릭터만 동작하게 할 수 있으므로 Command 클래스의 유용성이 떨어진다.
이런 제약에서 벗어나기 위해서 제어하려는 객체를 함수의 파라미터로 받아서 연결시켜주는 작업이 필요하다.
○ 기본적인 커맨드 패턴 + Actor 추가
< Hero.cs >
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Actor
{
public void Attack()
{
Debug.Log("캐릭터가 공격함.");
// 캐릭터 공격에 관한 처리
//...
}
public void Defend()
{
Debug.Log("캐릭터가 방어함.");
// 캐릭터 방어에 관한 처리
//...
}
public void Jump()
{
Debug.Log("캐릭터가 점프함.");
// 캐릭터 점프에 관한 처리
//...
}
public void Dash()
{
Debug.Log("캐릭터가 돌진함.");
// 캐릭터 돌진에 관한 처리
//...
}
}
public class Hero : MonoBehaviour
{
// 사용할 각각의 버튼 객체
Command button_A, button_S, button_D, button_F;
// 액터 추가
Actor actor;
private void Start()
{
actor = new Actor();
InitCommand();
}
// 각 버튼의 객체에 적용할 클래스(기능)를 할당
void InitCommand()
{
button_A = new CommandAttack();
button_S = new CommandDefend();
button_D = new CommandJump();
button_F = new CommandDash();
}
Command GetCommand()
{
// InitCommand()에서 할당 된 커맨드 객체를 반환
if (Input.GetKey("a")) return button_A;
else if (Input.GetKey("s")) return button_S;
else if (Input.GetKey("d")) return button_D;
else if (Input.GetKey("f")) return button_F;
else return null;
}
private void Update()
{
Command command = GetCommand();
if(command != null)
{
command.Execute(actor);
}
}
}
< Command.cs >
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Command
{
public virtual void Execute(Actor actor) { }
}
public class CommandAttack : Command
{
public override void Execute(Actor actor)
{
actor.Attack();
}
}
public class CommandDefend : Command
{
public override void Execute(Actor actor)
{
actor.Defend();
}
}
public class CommandJump : Command
{
public override void Execute(Actor actor)
{
actor.Jump();
}
}
public class CommandDash : Command
{
public override void Execute(Actor actor)
{
actor.Dash();
}
}
- Actor 클래스를 추가하고 GetCommand()에서 각각의 버튼에 할당된 Command 객체를 반환시키고 Command 클래스를 상속받은 자식 클래스 안에 재정의 된 Execute 함수에서 파라미터로 넘겨받은 Actor 객체를 이용하여 원하는 기능을 가진 함수를 호출한다.
- Actor 클래스의 추가로 Actor만 바꾸면 유저가 키 조작으로 어떤 Actor든 조작이 가능하도록 되었다. 또한
유저가 조작하지 않는 AI 캐릭터의 조작도 같은 명령패턴으로 AI코드에서 원하는 Command 객체를 사용하도록 응용이
가능하다. 가령 AI 캐릭터에게 HP가 일정수준으로 줄어들 경우 더 공격적인 광폭화 모드를 추가하고 싶을 경우 해당
공격명령을 추가해서 사용하면 된다.
- 더 나아가서 플레이어 캐릭터에 AI를 연결하여 자동실행되는 데모 모드 구현도 응용이 가능할 것이다.
○ 기본적인 커맨드 패턴 + Actor 추가 + Undo(실행취소) 기능 추가
- 커맨드 패턴 사용 예 중에서도 가장 잘 알려져 있는 Undo기능을 추가해보고자 한다.
< Command.cs >
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Command
{
public virtual void Execute(Actor actor, Vector3 movePos) { }
public virtual void Undo(Actor actor) { } // Undo 추가
}
public class CommandMove : Command
{
Vector3 prevPos = Vector3.zero;
public override void Execute(Actor actor, Vector3 movePos)
{
prevPos = actor.transform.position;
actor.Move(movePos);
Debug.Log("캐릭터 이동 좌표 --> " + movePos);
}
public override void Undo(Actor actor)
{
actor.Move(prevPos);
}
}
< Hero.cs >
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Actor
{
public Vector3 pos = new Vector3(0, 0, 0);
public Transform transform;
public Actor(Transform tr)
{
transform = tr;
}
// 캐릭터를 이동시키는 함수
public void Move(Vector3 pos)
{
transform.position = pos;
}
}
public class Hero : MonoBehaviour
{
// 사용할 각각의 버튼 객체
Command button_A, button_S, button_D, button_F;
// 액터 추가
Actor actor;
// 액터의 Command를 담는 스택 선언
Stack<Command> stack = new Stack<Command>();
// Undo 키 체크용도 Flag
bool isPushUndoKey = false;
private void Start()
{
actor = new Actor(gameObject.transform);
}
Command GetCommand()
{
// Undo 키를 눌렀을 때 처리
if (Input.GetKeyDown("z"))
{
isPushUndoKey = true;
if(stack.Count > 0)
{
return stack.Pop();
}
}
// Undo 키가 눌리지 않았다면 이동 처리
if (Input.GetKeyDown("w"))
{
Command command = new CommandMove();
stack.Push(command);
return command;
}
return null;
}
private void Update()
{
isPushUndoKey = false;
Command command = GetCommand();
if(command != null)
{
if (isPushUndoKey)
{
command.Undo(actor);
}
else
{
command.Execute(actor, new Vector3(0, Random.Range(0, 10f), 0));
}
}
}
}
- 스택에 실행했던 Command 객체를 저장하고 Undo 할 때 하나씩 꺼내서 처리하면 커맨드 패턴을 이용한 Undo의
기능 적용이 가능하다.
- Undo 기능을 응용하여 Redo(재실행) 기능도 구현이 가능하다. 사용자가 실행한 명령 리스트를 저장하고 Undo나 Redo 요청에
따라 현재 표시할 명령 목록으로 이동하면 된다.
- Redo 기능은 게임에서 리플레이 시스템에도 응용이 가능하다.
리플레이 시스템을 구현할 때 매 프레임마다 게임상황 전체를 저장하기엔 메모리가 너무 많이 필요하므로 대신 전체 개체가 실행하는 명령 모두를 저장하고 리플레이 시에 저장한 명령들을 순차적으로 실행하는 방식으로 구현이 가능하다.
'Programming > 디자인패턴' 카테고리의 다른 글
[디자인패턴] 관찰자 패턴 (Observer Pattern) (0) | 2020.07.02 |
---|---|
[디자인패턴] 경량 패턴, 플라이웨이트 패턴 (Flyweight Pattern) (0) | 2020.07.01 |