# 명령패턴이란 ?
요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 원하는 시점에 이용할 수 있도록, 매서드 이름, 매개변수 등의 요청에 사용되는 정보를 로깅, 취소 할 수 있는 패턴이다.
GoF (Gang of Four)의 디자인 패턴 책에서는 "요청 자체를 캡슐화 하는 것입니다. 이를 통해 요청이 서로 다른 사용자(Client)를 매개변수로 만들고, 요청을 대기시키거나 로깅(Logging)하여 되돌릴 수 있는 연산을 지원합니다."라고 정의하였다.
명령 패턴에는 명령(Command), 수신자(Receiver), 발동자(Invoke), 클라이언트(Client)의 4가지의 용어가 따른다.
# 명령 패턴의 UML
위의 이미지는 GoF의 디자인 패턴 책에서 제공하는 UML이다.
- Command
요청을 실행하기 위한 Interface를 선언한다.
- ConcreteCommand
receiver object와 action간의 바인딩을 정의한다.
receiver에서 대응하는 동작을 Invoking하여 실행시키는 것을 구현한다.
- Client
Command에 요청을 수행하도록 요청한다.
- Receiver
받은 요청을 어떻게 수행할지에 대한 세부 동작을 가지고 있다.
# 명령 패턴의 시퀀스 다이어그램
위의 이미지는 GoF의 디자인 패턴 책에서 제공하는 Sequence Diagram이다.
# 캐릭터 조작에 사용한 UML
위는 실제 구현에 있어서 응용한 명령 패턴의 UML이다.
# 코드 - Command.cs
요청을 받을시 그 요청을 실행하기 위한 명령을 만드는 가상클래스이다.
using UnityEngine; | |
public abstract class Command { | |
public abstract void execute(Actor actor); | |
} |
# 코드 - DefineCommand.cs
요청을 받을시 그 요청을 대상에게 요청하여 실행한다.
using UnityEngine; | |
public class CommandIDLE : Command { | |
public override void execute(Actor actor) { | |
actor.IDLE(); | |
} | |
} | |
public class CommandWALK : Command { | |
public override void execute(Actor actor) { | |
actor.WALK(); | |
} | |
} | |
public class CommandJUMP : Command { | |
public override void execute(Actor actor) { | |
actor.JUMP(); | |
} | |
} | |
public class CommandATTACK : Command { | |
public override void execute(Actor actor) { | |
actor.ATTACK(); | |
} | |
} |
# 코드 - Invoke.cs
이 곳에서 요청이 발생하고, 그 요청에 대응하는 명령을 호출한다.
using UnityEngine; | |
public class Invoke : MonoBehaviour { | |
public Actor actor; | |
private void Update() { | |
Command command = InputHandler(); // 요청 받기 | |
if (command != null) command.execute(actor); // 요청 실행 | |
} | |
private Command InputHandler() { | |
if (Input.GetKeyDown(KeyCode.Space)) | |
return new CommandJUMP(); | |
if (Input.GetKeyDown(KeyCode.Mouse0)) | |
return new CommandATTACK(); | |
if (Input.GetKey(KeyCode.W) || | |
Input.GetKey(KeyCode.S) || | |
Input.GetKey(KeyCode.A) || | |
Input.GetKey(KeyCode.D)) | |
return new CommandWALK(); | |
return null; | |
} | |
} | |
# 코드 - Actor.cs
요청에 대한 동작을 지니고 있는 부모 객체다.
using UnityEngine; | |
/* 해당 클래스 컴포넌트는 반드시 Rigidbody를 | |
지니고 있어야한다. | |
이 클래스를 컴포넌트로 넣을시, | |
해당객체에 Rigidbody가 없다면, | |
자동으로 Rigidbody를 추가한다. | |
해당객체에 Rigidbody가 있다면, | |
무시. | |
*/ | |
[RequiredComponent(typeof(Rigidbody))] | |
public abstract class Actor : MonoBehaviour { | |
public abstract void IDLE(); | |
public abstract void WALK(); | |
public abstract void JUMP(); | |
public abstract void ATTACK(); | |
} |
# 코드 - Player.cs
실질적으로 요청이 있을시, 그 요청을 수행할 때, 그 요청이 적용되는 객체다.
using UnityEngine; | |
public class Player : Actor { | |
private Rigidbody rb = null; | |
private Collider col = null; | |
public float moveSpeed = 3f; | |
public float jumpForce = 7f; | |
public float hitDistance = 0.35f; // 캐릭터가 지면에 닿아있는지 체크하기 위한 거리 | |
public LayerMask groundLayers; // 해당 Layer들만 지면으로 지정 | |
[System.NonSerialized] public bool grounded = true; // 지면에 닿아있는지의 여부 | |
[System.NonSerialized] public bool isCasting = false; // 공격을 시전중인지의 여부 | |
[System.NonSerialized] public bool canAttack = true; // 공격이 가능한지의 여부 | |
public float attackCooldown = 2f; // 해당 공격의 재사용까지 걸리는 시간 | |
private float attackCooldownEnd = 0f; // 해당 공격을 재사용할 수 있게 되는 시간 | |
private float attackCooldownRemaining() { return Time.time >= attackCooldownEnd ? 0 : attackCooldownEnd - Time.time; } | |
public float castingTime = 0.5f; // 해당 공격의 시전 시간 | |
private float castingTimeEnd = 0f; // 해당 공격의 시전이 끝나는 시간 | |
private float castingTimeRemaining() { return Time.time >= castingTimeEnd ? 0 : castingTimeEnd - Time.time; } | |
public Transform skillStartPosition = null; | |
public FireBall fireball = null; | |
pubilc void Awake() { | |
col = GetComponent<Collider>(); | |
rb = GetComponent<Rigidbody>(); | |
} | |
public override void IDLE() { | |
if (isCasting == true) return; | |
} | |
public override void WALK() { | |
if (isCasting == true) return; | |
if (Input.GetKey(KeyCode.W)) | |
transform.position += moveSpeed * Time.deltaTime * transform.forward; | |
if (Input.GetKey(KeyCode.S)) | |
transform.position += moveSpeed * Time.deltaTime * transform.forward * -1; | |
if (Input.GetKey(KeyCode.A)) | |
transform.position += moveSpeed * Time.deltaTime * transform.right * -1; | |
if (Input.GetKey(KeyCode.D)) | |
trnasform.position += moveSpeed * Time.deltaTime * transform.right; | |
} | |
public override void JUMP() { | |
if (grounded == false || | |
isCasting == true) return; | |
if (Input.GetKey(KeyCode.Space)) { | |
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); | |
} | |
} | |
public override void ATTACK() { | |
if (grounded == false || | |
canAttack == false || | |
isCasting == true) return; | |
if (Input.GetKey(KeyCode.Mouse0)) { | |
castingTimeEnd = Time.time + castingTime; // skill 시전시, 시전시간 가산 | |
isCasting = true; | |
} | |
} | |
private void Update() { | |
if (isCasting == true && | |
castingTimeRemaining() <= 0f) { | |
isCasting = false; | |
canAttack = false; | |
attackCooldownEnd = Time.time + attackCooldown; // 시전 완료시 재사용 시간 가산 | |
GameObject go = (GameObject)Instantiate(fireball.gameObject); | |
go.transform.position = skillStartPosition.position; | |
go.transform.rotation = skillStartPosition.rotation; | |
go.transform.localScale = Vector3.one; | |
go.SendMessage("SetFireBallMoveDirection", transform.forward, SendMessageOptions.DontRequireReceiver); | |
} | |
if (canAttack == false && | |
attackCooldownRemaining() <= 0f) { | |
canAttack = true; | |
} | |
if (grounded) hitDistance = 0.35f; | |
else hitDistance = 0.15f; | |
if (Physics.Raycast(transform.position - new Vector3(0, 0.15f, 0), | |
-transform.up, | |
hitDistance, | |
groundLayers)) | |
grounded = true; | |
else | |
grounded = false; | |
} | |
} |
# other - Time.time 이란?
이러한 구조를 가지고 있다.
즉, Time.time은 게임에서 돌아가고 있는 '게임 내의 System 시계'다 라고 생각하면 편하다.
(현재 시점 N에서 계속 시간이 흐르면, 어느 순간 (N + t)라는 시간에 도달하게 된다. 그 순간 시전이 완료되면서 공격이 나가게 된다.)
# 글을 마치면서...
동작에 대한 정보는 첨부파일에 담겨있다.
해당 명령 패턴에 의해 조작되는 캐릭터를 보고 싶다면, 첨부파일을 받아서 실행시켜보자.
# 참고 자료
1. GoF의 책 - 'Design Patterns'
2. 로버트 나이스트롬의 책 - 'Game Programming Patterns'
3. 위키백과의 '커맨드패턴' - https://goo.gl/UUkBab
'- Unity > 캐릭터 조작' 카테고리의 다른 글
State Pattern (상태 패턴) (0) | 2018.08.31 |
---|