UI
프로젝트를 진행하면서 가장 크게 느낀 것은 UI가 생각보다 시간을 많이 잡아먹는 요소라는 것이다. 스크립트 작성이 전체 일정의 30%를 차지한다면 UI는 60% 이상을 투자해야하기 때문이다.
따라서 UI에 소요되는 시간을 줄이면 전체적인 일정을 앞당길 수 있고, 이를 위해 간단한 기술, 잡기술에 대해 알아둘 필요가 있을 것이다.
UI
관리의 어려움다음과 같은 이유들로 우리는 UI 관리에 어려움을 갖게 된다.
→ 따라서 이러한 이유로 우리는 UI 요소들을 코드로써(UI Binding) 관리한다.
UI binding
Unity Editor 상에서 Drag하여 UI 요소들을 넣는 것이 아닌, 코드로 맵핑하여 등록하여 사용하면 위에 소개된 관리의 어려움을 상당 부분 해소하는 것이 가능하다.
public class BaseUI : MonoBehaviour
{
private Dictionary<string, GameObject> _gameObjectDic;
private Dictionary<string, Component> _componentDic;
private void Awake()
{
Init();
}
private void Init()
{
// 모든 UI 요소가 가지고 있는 RectTransform을 기준으로 UI 요소들을 가져온다.
// 매개변수로 IncludeInActive를 true로 하여 비활성화된 오브젝트 포함하여 가져온다.
RectTransform[] transforms = GetComponentsInChildren<RectTransform>(true);
_gameObjectDic = new Dictionary<string, GameObject>(transforms.Length * 4);
foreach (RectTransform child in transforms)
{
// 이름이 동일한 경우 중복으로 추가하지 않기 위해 TryAdd를 사용
_gameObjectDic.TryAdd(child.name, child.gameObject);
}
Component[] components = GetComponentsInChildren<Component>(true);
_componentDic = new Dictionary<string, Component>(components.Length * 4);
foreach (Component child in components)
{
_componentDic.TryAdd($"{child.gameObject.name}_{child.GetType().Name}", child);
}
}
public GameObject GetUI(in string name)
{
_gameObjectDic.TryGetValue(name, out GameObject gameObject);
return gameObject;
}
public T GetUI<T>(in string name) where T : Component
{
_componentDic.TryGetValue($"{name}_{typeof(T).Name}", out Component component);
if(component != null)
return component as T;
GameObject gameObject = GetUI(name);
if (GetUI(name) == null)
return null;
component = gameObject.GetComponent<T>();
if (component == null)
return null;
_componentDic.TryAdd($"{name}_{typeof(T).Name}", component);
return component as T;
}
}
이렇게 작성한 BaseUI의 GetUI 메서드를 활용하여 UI요소들을 각각 관리할 수 있다.
public class WinPresenter : BaseUI
{
private GameObject _nextBtn;
private Text _coinText;
private Button _nextBtnButton;
private void Awake()
{
Init();
}
private void Start()
{
_nextBtn.SetActive(false);
_coinText.text = "100";
_nextBtnButton.interactable = false;
}
private void Init()
{
_nextBtn = GetUI("NextButton");
_coinText = GetUI<Text>("CoinText");
_nextBtnButton = GetUI<Button>("NextButton");
}
}
PointerHandler
지금까지 클릭이나 드래그와 같은 반응형 UI를 구현하기 위해서 Button을 추가한 뒤 Onclick에 이벤트를 추가했을 것이다. 하지만 이를 스크립트에서 인터페이스를 통해 구현하는 것이 가능한데, 바로 PointerHandler 인터페이스이다.
public class BaseUI : MonoBehaviour
{
public PointerHandler GetEvent(in string name)
{
GameObject gameObject = GetUI(name)
return gameObject.GetOrAddComponent<PointerHandler>();
}
}
public class WinPresenter : BaseUI
{
private void OnEnable()
{
// GetEvent("NextButton").Down += (data) => Debug.Log("누름");
GetEvent("NextButton").Down += Down;
GetEvent("NextButton").Up += Up;
}
private void OnDisable()
{
GetEvent("NextButton").Down -= Down;
GetEvent("NextButton").Up -= Up;
}
public void Down(PointerEventData data)
{
Debug.Log("누름");
}
public void Up(PointerEventData data)
{
Debug.Log("뗌");
}
}
Click
public class PointerHandler : MonoBehaviour
, IPointerUpHandler
, IPointerDownHandler
, IPointerClickHandler
{
public event Action<PointerEventData> Up;
public event Action<PointerEventData> Down;
public event Action<PointerEventData> Click;
public void OnPointerDown(PointerEventData eventData) => Down?.Invoke(eventData);
public void OnPointerUp(PointerEventData eventData) => Up?.Invoke(eventData);
public void OnPointerClick(PointerEventData eventData) => Click?.Invoke(eventData);
}
Drag
public class PointerHandler : MonoBehaviour
, IPointerUpHandler
, IPointerDownHandler
, IPointerClickHandler
{
public event Action<PointerEventData> BeginDrag;
public event Action<PointerEventData> EndDrag;
public event Action<PointerEventData> Drag;
public void OnBeginDrag(PointerEventData eventData) => BeginDrag?.Invoke(eventData);
public void OnDrag(PointerEventData eventData) => Drag?.Invoke(eventData);
public void OnEndDrag(PointerEventData eventData) => EndDrag?.Invoke(eventData);
}
Pointer
public class PointerHandler : MonoBehaviour
, IPointerEnterHandler
, IPointerMoveHandler
, IPointerExitHandler
{
public event Action<PointerEventData> Enter;
public event Action<PointerEventData> Move;
public event Action<PointerEventData> Exit;
public void OnPointerEnter(PointerEventData eventData) => Enter?.Invoke(eventData);
public void OnPointerMove(PointerEventData eventData) => Move?.Invoke(eventData);
public void OnPointerExit(PointerEventData eventData) => Exit?.Invoke(eventData);
}
PointerHandler - Object(3D)
pointerHandler를 3D 오브젝트에도 사용이 가능한데, 이를 위해서는 2가지의 사전 작업이 필요하다.
이러한 작업을 마치고 오브젝트에 PointerHandler 스크립트를 추가하여 마우스로 오브젝트를 이동시키는 것을 구현해보자
public class Pointer3DObject : MonoBehaviour
,IDragHandler
,IPointerDownHandler
,IPointerUpHandler
{
public void OnPointerDown(PointerEventData eventData)
{
transform.Translate(Vector3.up * 1f, Space.World);
}
public void OnDrag(PointerEventData eventData)
{
Plane plane = new Plane(Vector3.up, Vector3.zero);
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (plane.Raycast(ray, out float distance))
{
transform.position = ray.GetPoint(distance) + Vector3.up * 1.5f;
}
// transform.position += (Vector3)eventData.delta;
}
public void OnPointerUp(PointerEventData eventData)
{
transform.Translate(Vector3.down * 1f, Space.World);
}
}
위의 스크립트를 3D 오브젝트에 붙이면 평면 위에서 해당 오브젝트를 마우스로 이동시킬 수 있게 된다.
하지만 주의할 점은 다음과 같이 plane을 생성하고 Ray를 쏘는 방식에서는 카메라를 생성시킨 plane의 법선 방향에서 내려다보도록 설정을 해야 자연스러운 이동 구현이 가능하다.
Q&A
UI는 싱글톤을 활용하여 구성하는가?
→ 싱글톤은 전역적인 접근이 가능하며 단 하나만 존재하는 특징을 가지고 있다. 따라서 이러한 특징을 갖는 UI의 경우 싱글톤을 활용하는 것이 가능할 것이다. 예를 들어 로딩씬, DialogUI 처럼 전역적으로 동일한 형태로 접근이 필요한 경우 싱글톤을 사용한다. 다만 반대로 체력바와 같이 특정 씬에서만 사용하는 UI는 싱글톤으로 구현하기에 적합하지 않을 것이다.
UI를 코드로 구성하는 것은 불가능한가?
→ 당연히 UI 관리가 Unity Editor로만으로는 한계가 있기에, UI Manager를 통해 Stack형으로 관리하는 것도 병행되어야한다.
UI 요소들을 Prefab화 하고자 하는데 이를 코드 상으로 관리하는 방법이 있는가?
→ 마찬가지로 UI Manager를 통해서 관리하는 것이 가능하지만, 배치에 신경 쓸 필요가 있다.
Scriptable Object로 UI를 구현하는 것이 가능한가?
→ Scriptable Object의 가능성은 무한하기에 당연히 가능하다. 다만 효율성의 문제가 있기 때문에 고민하며 사용할 필요가 있을 것이다.
UI 데이터를 서버로 관리하는 경우가 있는가?
→ 서버에 있는 데이터를 기반으로 UI를 구성하는 경우도 분명 있다. 이는 MVP 패턴을 적용했을 때 model만 ServerDB로 바꿔 구현이 가능하다.
게임을 만들 때 모바일 출시 여부까지 고려해서 UI를 구성해야하는가?
→ 처음 구상에서 정하고 가는게 베스트지만, 기존 계획과 틀어질 가능성이 높기 때문에 미리 대비하는게 좋다.