# 명령패턴이란 ?


요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 원하는 시점에 이용할 수 있도록, 매서드 이름, 매개변수 등의 요청에 사용되는 정보를 로깅, 취소 할 수 있는 패턴이다.


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);
}
view raw Command.cs hosted with ❤ by GitHub



# 코드 - 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;
}
}
view raw Invoke.cs hosted with ❤ by GitHub



# 코드 - 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();
}
view raw Actor.cs hosted with ❤ by GitHub



# 코드 - 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;
}
}
view raw Player.cs hosted with ❤ by GitHub

# other - Time.time 이란?


이러한 구조를 가지고 있다.

즉, Time.time은 게임에서 돌아가고 있는 '게임 내의 System 시계'다 라고 생각하면 편하다.

(현재 시점 N에서 계속 시간이 흐르면, 어느 순간 (N + t)라는 시간에 도달하게 된다. 그 순간 시전이 완료되면서 공격이 나가게 된다.)



# 글을 마치면서...


Command.unitypackage


동작에 대한 정보는 첨부파일에 담겨있다.

해당 명령 패턴에 의해 조작되는 캐릭터를 보고 싶다면, 첨부파일을 받아서 실행시켜보자.



# 참고 자료

 1. GoF의 책 - 'Design Patterns'

 2. 로버트 나이스트롬의 책 - 'Game Programming Patterns'

 3. 위키백과의 '커맨드패턴' - https://goo.gl/UUkBab

'- Unity > 캐릭터 조작' 카테고리의 다른 글

State Pattern (상태 패턴)  (0) 2018.08.31

+ Recent posts