Chapter 14. 명령 패턴(Command Pattern)
카테고리: Design Pattern
인프런에 있는 이재환님의 강의 게임 디자인 패턴 with Unity 를 듣고 정리한 필기입니다. 😀
Chapter 14. Command Pattern
🔔 개념
명령 패턴은 함수 호출을 자체를 실체화, 즉 객체로 감싼 것이다.
- 요청 자체를 캡슐화 하는 것.
- 이를 통해 요청이 서로 다른 사용자를 매개 변수로 만들고, 요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산을 지원한다.
- 함수 호출을 객체로 만들었기 때문에 디커플링으로 코드가 유연하다.
- 게임 프로그래밍에서의 사용 예
- 입력 키 변경
- 실행 취소, 재실행
🔔 커맨드 패턴이 적용 안된 예제
📜csCannon
총알 오브젝트에 붙는다.
using UnityEngine;
using System.Collections;
public class csCannon : MonoBehaviour
{
float power = 900.0f;
Vector3 velocity;
void Start()
{
velocity = transform.forward * power;
GetComponent<Rigidbody>().AddForce(velocity); // 생성 되자마자 900 크기의 속도로 발사 됨
StartCoroutine ("DeleteCannon"); // 0.5초 뒤 총알 파괴
}
IEnumerator DeleteCannon() {
yield return new WaitForSeconds (0.5f);
Destroy(this.gameObject);
}
}
📜csPlayerNormal
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class csPlayerNormal : MonoBehaviour {
public GameObject shield; // 쉴드 오브젝트(비활성화 상태가 디폴트다)
public GameObject cannon; // 총알 프리팹
public Transform firePos; // 총알이 생성될 위치가 될 빈 오브젝트
void Update () {
if (Input.GetKeyDown ("a")) {
Attack ();
} else if (Input.GetKeyDown ("b")) {
Defense ();
}
}
void Attack() // 공격. 총쏘기
{
Debug.Log("Attack");
Instantiate (cannon, firePos.position, firePos.rotation); // 📜csCannon 의 Start()가 실행되며 총알 생성되고 날라가고 0.5초뒤 파괴
}
void Defense() // 방어.
{
Debug.Log("Defense");
shield.SetActive(true); // 쉴드를 활성화
StartCoroutine (Defense(0.5f)); // 0.5초 뒤 쉴드 비활성화
}
IEnumerator Defense(float tm) {
yield return new WaitForSeconds (tm);
shield.SetActive(false);
}
}
문제점
void Update () {
if (Input.GetKeyDown ("a")) {
Attack ();
} else if (Input.GetKeyDown ("b")) {
Defense ();
}
}
공격을 E
키로 하고 싶다면? 사용자가 Key 를 바꾸고 싶다면? 사용자가 직접 update() 문 코드를 바꾸지 않는 이상 사용자 정의 키를 사용할 수 없다. 커맨드 패턴을 사용하면 Key 를 바꿀 수 있다.
🔔 커맨드 패턴이 적용 된 예제
명령 자체를 클래스로 만든다.
🌼 예제 1
📜csCannon
public class csCannon : MonoBehaviour
{
float power = 900.0f;
Vector3 velocity;
void Start()
{
velocity = transform.forward * power;
GetComponent<Rigidbody>().AddForce( velocity );
StartCoroutine ("DeleteCannon");
}
IEnumerator DeleteCannon() {
yield return new WaitForSeconds (0.5f);
Destroy(this.gameObject);
}
}
📜CommandKey
// 인터페이스 : Execute() 메소드만 있는 추상클래스
public abstract class CommandKey {
public GameObject shield;
public GameObject cannon;
public Transform firePos;
public MonoBehaviour mono;
public virtual void Execute() {}
}
📜CommandAttack
// Concrete Command 객체 : 직접적으로 동작하는 객체
public class CommandAttack : CommandKey {
public CommandAttack(MonoBehaviour _mono, GameObject _shield,
GameObject _cannon, Transform _firePos)
{
this.shield = _shield;
this.cannon = _cannon;
this.firePos = _firePos;
this.mono = _mono;
}
public override void Execute()
{
Attack();
}
void Attack()
{
Debug.Log("Attack");
GameObject.Instantiate(cannon, firePos.position, firePos.rotation);
}
}
- 📜CommandAttack의 Excute() 오버라이딩
- 👉 Attack() 호출
📜CommandDefense
// Concrete Command 객체 : 직접적으로 동작하는 객체
public class CommandDefense : CommandKey {
public CommandDefense(MonoBehaviour _mono, GameObject _shield,
GameObject _cannon, Transform _firePos)
{
this.shield = _shield;
this.cannon = _cannon;
this.firePos = _firePos;
this.mono = _mono;
}
public override void Execute()
{
Defense();
}
void Defense()
{
Debug.Log("Defense");
shield.SetActive(true);
mono.StartCoroutine(Defense(0.5f));
}
IEnumerator Defense(float second) {
yield return new WaitForSeconds (second);
this.shield.SetActive(false);
}
}
- 📜CommandDefense Excute() 오버라이딩
- 👉 Defense() 호출
📜csPlayerCommand
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class csPlayerCommand : MonoBehaviour {
public GameObject shield;
public GameObject cannon;
public Transform firePos;
bool bCmd;
Text txt1;
Text txt2;
CommandKey btnA, btnB;
void Start () {
bCmd = true;
txt1 = GameObject.Find ("Text1").GetComponent<Text>();
txt2 = GameObject.Find ("Text2").GetComponent<Text>();
SetCommand();
}
// SetCommand() 메소드를 통해 버튼을 누르면 어떤 동작을 수행할지를 각 버튼에 등록
public void SetCommand()
{
if (bCmd == true) {
btnA = new CommandAttack(this, shield, cannon, firePos);
btnB = new CommandDefense(this, shield, cannon, firePos);
bCmd = false;
txt1.text = "A - Attack";
txt2.text = "B - Defense";
} else {
btnA = new CommandDefense(this, shield, cannon, firePos);
btnB = new CommandAttack(this, shield, cannon, firePos);
bCmd = true;
txt1.text = "A - Defense";
txt2.text = "B - Attack";
}
}
// 버튼을 누르면 단지 버튼의 Execute()만 호출
void Update () {
if (Input.GetKeyDown ("a")) {
btnA.Execute ();
} else if (Input.GetKeyDown ("b")) {
btnB.Execute ();
}
}
}
- 부모 타입
CommandKey
변수로CommandDefense
,CommandAttack
두 명령 객체를 업캐스팅 한다. 다형성- 단지 Excute() 하나만으로 어떤 타입의 객체를 참조하느냐에 따라 다르게 실행된다.
btA
,btB
는 SetCommand 함수에서 버튼 눌리는 것에 따라 다른 명령 객체CommandDefense
,CommandAttack
를 참조하게 된다.- 따라서 버튼이 눌림에 따라
A
키를 눌렀을 때btnA.Execute ()
에서 공격을 할 수도 있고 방어를 할 수도 있고 경우에 따라 다르게 코딩할 수 있게 되었음
- 단지 Excute() 하나만으로 어떤 타입의 객체를 참조하느냐에 따라 다르게 실행된다.
🌼 예제 2
📜CommandKey
// 인터페이스 : Execute() 메소드만 있는 추상클래스
public abstract class CommandKey {
public virtual void Execute(GameObject obj) {}
}
📜CommandAttack
// Concrete Command 객체 : 직접적으로 동작하는 객체
public class CommandAttack : CommandKey {
// 객체를 파라미터로 받아 어떤 객체라도 메서드를 호출하여 사용할 수 있도록 함
public override void Execute(GameObject obj)
{
// 객체와 메서드는 decoupling 관계
Attack(obj);
}
void Attack(GameObject obj)
{
Debug.Log(obj.name + " Attack");
obj.transform.Translate (Vector3.forward);
}
}
- 📜CommandAttack의 Excute() 오버라이딩
- 👉 Attack() 호출
📜CommandDefense
// Concrete Command 객체 : 직접적으로 동작하는 객체
public class CommandDefense : CommandKey {
public override void Execute(GameObject obj)
{
Defense(obj);
}
void Defense(GameObject obj)
{
Debug.Log(obj.name + " Defense");
obj.transform.Translate (Vector3.back);
}
}
- 📜CommandDefense Excute() 오버라이딩
- 👉 Defense() 호출
📜csPlayerCommand
각 오브젝트들에게 붙일 수 있다.
btnA
, btnB
가 참조하는 명령 객체는 언제든지 바꿀 수 있다는 장점.
public class csPlayerCommand : MonoBehaviour {
CommandKey btnA, btnB;
void Start () {
SetCommand();
}
void SetCommand()
{
btnA = new CommandAttack();
btnB = new CommandDefense();
}
public void BtnCommandA()
{
btnA.Execute(gameObject); // 이 스크립트가 붙은 오브젝트를 공격하게 함
}
public void BtnCommandB()
{
btnB.Execute(gameObject); // 이 스크립트가 붙은 오브젝트를 방어하게 함
}
}
🌼 예제 3
📜CommandKey
// 인터페이스 : Execute() 메소드만 있는 추상클래스
public abstract class CommandKey {
public virtual void Execute(Transform tr, Vector3 newPos) {}
public virtual void Undo(Transform tr) {}
}
- Undo 추가 !
- 이전 Transform 위치 기록
📜CommandMoveRight
// Concrete Command 객체 : 직접적으로 동작하는 객체
public class CommandMoveRight : CommandKey {
Vector3 prevPos;
public override void Execute(Transform tr, Vector3 newPos)
{
prevPos = tr.position;
tr.Translate(newPos);
}
public override void Undo(Transform tr)
{
tr.position = prevPos;
}
}
- 📜CommandAttack의 Excute() 오버라이딩
- 👉 Attack() 호출
📜CommandMoveForward
// Concrete Command 객체 : 직접적으로 동작하는 객체
public class CommandMoveForward : CommandKey {
Vector3 prevPos;
public override void Execute(Transform tr, Vector3 newPos)
{
prevPos = tr.position;
tr.Translate(newPos);
}
public override void Undo(Transform tr)
{
tr.position = prevPos;
}
}
- 📜CommandDefense Excute() 오버라이딩
- 👉 Defense() 호출
📜csPlayerCommand
플레이어 오브젝트에 연결
public class csPlayerCommand : MonoBehaviour
{
Stack<CommandKey> stack = new Stack<CommandKey>();
public void MoveForward()
{
CommandKey command = new CommandMoveForward();
stack.Push(command);
command.Execute(transform, new Vector3(0f, 0f, 1f));
}
public void MoveRight()
{
CommandKey command = new CommandMoveRight();
stack.Push(command);
command.Execute(transform, new Vector3(1f, 0f, 0f));
}
public void MoveUndo()
{
if (stack.Count > 0)
{
CommandKey command = stack.Pop();
command.Undo(transform);
}
}
}
스택으로 명령 객체들 관리. Undo 를 사용하기 위해서 이전 위치를 저장하기 위해서.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기