Command Pattern
Command Pattern은 게임에서 입력처리에 많이 사용된다.
실제로 사용해보면 여러가지 편리한 점이 많은데 가장 멋진 점을 뽑으라면 플레이어가 옵션에서 임의로 입력키를 바꿀 수 있는 기능을 구현할 수 있다!
아주 간단한 예제를 통해서 Command Pattern을 정리한다.
마우스 왼쪽 클릭을 입력하면 이동 명령을 Do 하면서 Stack에 쌓는다.
반대로 마우스 오른쪽 클릭을 입력하면 Stack을 거슬러 올라가면서 Un Do한다.
▼ 유니티 예제
▼ 개념
Command Pattern의 개념은 그렇게 어렵지 않다.
단순하게 접근하면 명령어 클래스를 하나 만들고 그걸 변수로서 사용하는 것 뿐이다.
구현 로직은 아래와 같다.
1. ICommand라는 인터페이스를 만든다.
public interface ICommand
{
void ExecuteDo();
void ExecuteUndo();
}
2. 실제 작동할 Command를 추가한다.
public class CommandMoveTo : ICommand
{
public CommandMoveTo(GameManager _gameManager, Vector3 _startPos, Vector3 _destPos)
{
m_gameManager = _gameManager;
m_destination = _destPos;
m_startPosition = _startPos;
}
public void ExecuteDo()
{
m_gameManager.MoveTo(m_destination);
}
public void ExecuteUndo()
{
m_gameManager.MoveTo(m_startPosition);
}
GameManager m_gameManager;
Vector3 m_destination;
Vector3 m_startPosition;
}
3. 특정 조건 마다 Command를 변경 혹은 저장한다.
var leftClickPoint = GetLeftClickPosition();
if (leftClickPoint != null)
{
CommandMoveTo moveto = new CommandMoveTo(this, m_player.transform.position, leftClickPoint.Value);
m_invoker.ExecuteDo(moveto);
}
▼ 전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
/* COMMAND INTERFACE */
public interface ICommand
{
void ExecuteDo();
void ExecuteUndo();
}
/* INVOKER : COMMAND STACK */
public class Invoker
{
public Invoker()
{
m_commands = new Stack<ICommand>();
}
public void ExecuteDo(ICommand command)
{
if (command != null)
{
m_commands.Push(command);
m_commands.Peek().ExecuteDo();
}
}
public void ExecuteUndo()
{
if (m_commands.Count > 0)
{
m_commands.Peek().ExecuteUndo();
m_commands.Pop();
}
}
Stack<ICommand> m_commands;
}
public GameObject m_player;
private Invoker m_invoker;
/* MOVE_TO : A COMMAND */
public class CommandMoveTo : ICommand
{
public CommandMoveTo(GameManager _gameManager, Vector3 _startPos, Vector3 _destPos)
{
m_gameManager = _gameManager;
m_destination = _destPos;
m_startPosition = _startPos;
}
public void ExecuteDo()
{
m_gameManager.MoveTo(m_destination);
}
public void ExecuteUndo()
{
m_gameManager.MoveTo(m_startPosition);
}
GameManager m_gameManager;
Vector3 m_destination;
Vector3 m_startPosition;
}
/* SMOOTH MOVEMENT WITH DELTA_TIME WITH COROUTINE */
public IEnumerator MoveToInSeconds(GameObject _gameObject, Vector3 _endPos, float _second)
{
float elapsedTime = 0;
Vector3 startingPos = _gameObject.transform.position;
_endPos.y = startingPos.y;
while (elapsedTime < _second)
{
_gameObject.transform.position = Vector3.Lerp(startingPos, _endPos, (elapsedTime / _second));
elapsedTime += Time.deltaTime;
yield return null;
}
_gameObject.transform.position = _endPos;
}
public Vector3? GetLeftClickPosition()
{
if (Input.GetMouseButtonDown(0))
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hitInfo))
{
return hitInfo.point;
}
}
return null;
}
private void Start()
{
m_invoker = new Invoker();
}
private void Update()
{
/* LEFT CLICK */
var leftClickPoint = GetLeftClickPosition();
if (leftClickPoint != null)
{
CommandMoveTo moveto = new CommandMoveTo(this, m_player.transform.position, leftClickPoint.Value);
m_invoker.ExecuteDo(moveto);
}
/* RIGHT CLICK */
if (Input.GetMouseButtonDown(1))
{
m_invoker.ExecuteUndo();
}
}
public void MoveTo(Vector3 pt)
{
IEnumerator moveto = MoveToInSeconds(m_player, pt, 0.5f);
StartCoroutine(moveto);
}
}
여러 파일로 나누면 블로그에 정리하기가 귀찮아져서 그냥 GameManager.cs 한 파일로 작성했다.
실제로 게임을 개발할 때는 당연히 나눠서 작성해야 할 것이다.
GameManager는 입력처리 말고도 충분히 바쁜 친구니까.
?(Null-Conditional Operator)
C++에는 없는 생소한 연산자인 Null 상태 연산자를 한번 써봤다.
간단히 말하면 C#에서 제공하는 변수형에 null 체크를 할 수 있는 추가적인 기능을 제공한다.
위의 코드에서 사용했던걸 보면 이해하기 편하다.
public Vector3? GetLeftClickPosition()
{
if (Input.GetMouseButtonDown(0))
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hitInfo))
{
return hitInfo.point;
}
}
return null;
}
마우스 왼쪽 클릭이 들어오는 순간의 위치값을 Vector3로 전달하는 의도를 가진 함수의 코드다.
프로그래머라면 언제라도 예외상황이 있을 수 있다고 가정하고 예외처리를 해주는 것이 좋은데 여기서 예상할 수 있는 예외상황은 Vector3가 아닌 null값이 반환되는 경우다.
C++이었다면 Vector3로 나올 수 없는 값을 반환하거나 따로 bool값을 사용하거나 해야한다.
상대적으로 늦게 탄생한 것 덕분인지 C#에서는 ?연산자를 통해서 null을 반환할 수 있는 편의 기능이 있다.