이미 이전에 UI에 대해 배워본 바가 있지만, 리뷰하는 느낌으로 해당 내용을 다시 정리하고, 추가로 공부한 내용을 정리해보고자 한다.
UI는 User Interface의 줄임말로, 사용자에게 제공되는 프로그램으로의 가장 직관적인 접근 요소를 말한다. UI는 사용 환경에 따라 다양한 해상도에 유연하게 적용되어야 하며, 이는 UI를 구성함에 있어 가장 중요한 핵심 요소이다. 게임에서의 UI는 프로그램의 유지보수, 업데이트와 같은 확장성을 고려한 객체지향적 설계가 적용되어야 할 중요한 요소이다.
UI는 반드시 Canvas라는 오브젝트에 포함되어 있어야 한다. Canvas는 UI를 배치하는 평면의 도화지로, UI를 화면에 표기하기 위한 2차원 사각형 내의 위치를 제어하는 RectTransform 컴포넌트가 포함되어 있다.
RectTransform은 해상도 변경시에도 부모 오브젝트의 위치에 대상 오브젝트를 고정하고, 위치를 정렬하기 위한 앵커와 회전의 기준점이 되는 피벗을 가지고 있다.
캔버스 모드
- Screen Space - Overlay - 3D 게임 제작 시 주로 사용
- Screen Space - Camera - 2d 게임에서 주로 씀
- WorldSpace - 화면에 고정적인 UI에서 많이 씀 - hp바 같은 거
UI Scale Mode
- constant pixel size - 픽셀 사이즈만큼의 UI 크기를 가짐 (해상도가 달라지면 위치가 달라지고 크기가 고정-절대사이즈)
- scale with screen size - 화면의 비율만큼의 UI 크기를 가짐 (해상도가 달라져도 위치와 크기를 유지함-상대사이즈)
- constant physical size - cm 사이즈만큼의 UI 크기를 가짐 (해상도가 달라지면 위치가 달라지고 크기가 고정-절대사이즈)
RectTransform
- 앵커 : UI 요소의 부모 UI오브젝트에 대한 상대좌표. 이를 사용하면 UI 요소가 화면 크기가 변해도 같은 위치에 있도록 설정할 수 있다.
- 피봇 : 자신 UI 오브젝트의 회전이나 크기 조정시 로컬 공간 내의 하나의 기준점. (0, 0)은 왼쪽 하단, (1, 1)은 오른쪽 상단을 의미한다.
1) NGUI
NGUI는 스크립트로만 UI를 컨트롤하는 방식을 말한다. Unity 기술이 발달하기 이전 많이 쓰던 방식이나, 지금에 와서는 UGUI의 형태로 작업하는 경우가 많다. 하지만 예전 기술의 회사의 경우 아직 NGUI를 쓰는 경우도 있다.
2) UGUI
UI가 유니티의 렌더링 파이프라인에 통합되어 있으며, 구성요소도 게임오브젝트와 같은 방식으로 처리하는 방식을 말한다. 별도의 컨트롤창이 아닌 하이어라키에서 직관적으로 편집할 수 있다.
Image : 말 그대로 이미지를 가져올 수 있다. 특이사항으로는, 외부에서 온 이미지를 사용해야 할 때, 해당 이미지를 Default->Sprite로 변환해서 사용해야 한다. Image는 Sprite와 Color 데이터를 통해 이미지를 출력하기 때문에 렌더링 전 발생하는 전처리 과정으로 상황에 따라 처리 비용이 높을 수 있다.
Raw Image : Raw Image는 원본 이미지를 그대로 출력하기 때문에 동영상 같은 것도 출력이 가능하다. 또한 이미지에 대한 추가 처리가 필요하지 않아 처리 속도가 빠르고 주로 동영상 재생에 적합하다.
마우스 클릭/화면 터치를 위한 UI 구성요소이다. 내부적으로 클릭 시 동작을 정의할 수 있도록 OnClick 이벤트를 내장하고 있으며, 이벤트 트리거를 사용해 다양한 구현이 가능하다.
Slider를 이용하면 HP바를 만들 수도 있다. 방법은 아래와 같다.
Slider를 생성하고, Handle Slide Area를 제거한다.
Fill을 빨간색, Background를 검정색으로 설정한다.
using UnityEngine;
using UnityEngine.UI;
public class PlayerHpBar : MonoBehaviour
{
[SerializeField] Slider slider;
public void SetHP(int curHP)
{
slider.value = curHP;
}
public void SetMaxHP(int maxHP)
{
slider.maxValue = maxHP;
}
}
구성요소로는 아래처럼 Vieport - Content 내부 공간이 있고, 가로와 세로 스크롤바가 각각 있다.
몇몇 옵선에 대해 알아보자.
가로, 세로 바를 전부 사용할 건지 체크박스로 조절가능하다.
Movement Type은 세 종류가 있다.
말 그대로 텍스트를 지원해주는 UI 기능이다. 텍스트는 이미지의 형태로 저장되며 폰트의 별도 변환이 필요하다.
여기서 만약 한글 지원 폰트를 사용하고 싶을 경우 세팅하는 방법에 대해 알아보고자 한다.
사용한 폰트는 무료 폰트인 빛의 계승자 폰트로 진행했다.
Text Asset 생성
책임을 쪼개고 기능을 분리하는 데 있어 사용할 수 있는 패턴이다. 모델과 View를 나누고 연동하는 방식으로 기능을 구분하여, 관리에 있어 용이하게 하는 기법이다.
사용자 인터페이스를 개발할 때 흔히 사용되는 일련의 디자인 패턴이다.
소프트웨어의 논리적 부분, 데이터 부분, 프레젠테이션 부분을 분리하는 것이다.
MVC를 이용한 플레이어 HP UI 표기를 살펴보자.
using UnityEngine;
public class PlayerModel : MonoBehaviour
{
[SerializeField] PlayerHpText hpText;
[SerializeField] PlayerHpBar hpBar;
[SerializeField] int hp;
[SerializeField] int maxHP;
public int MaxHP { set { maxHP = value; } get { return maxHP; } }
public int HP
{
set
{
hp = value;
hpText.SetText(hp, maxHP);
hpBar.SetHP(hp);
}
get
{
return hp;
}
}
private void Start()
{
hpText.SetText(hp, MaxHP);
hpBar.SetMaxHP(maxHP);
hpBar.SetHP(hp);
}
}
// 플레이어 HP 바
using UnityEngine;
using UnityEngine.UI;
public class PlayerHpBar : MonoBehaviour
{
[SerializeField] Slider slider;
public void SetHP(int curHP)
{
slider.value = curHP;
}
public void SetMaxHP(int maxHP)
{
slider.maxValue = maxHP;
}
}
// 플레이어 HP 텍스트
using TMPro;
using UnityEngine;
public class PlayerHpText : MonoBehaviour
{
[SerializeField] TMP_Text textUI;
public void SetText(int hp, int maxHP)
{
textUI.text = $"HP {hp} / {maxHP}";
}
}
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField] PlayerModel model;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
model.HP -= 1;
}
}
}
MVC 디자인은 각 부분이 하나의 작업을 수행하여 기능을 분리할 수 있고, 단일 책임 원칙을 따르는 연장선상에 놓이는 데에 장점이 있다.
유니티의 경우에서는 이미 UI 기능을 구현해놓았기 때문에 View와 관련된 부분을 직접 개발할 필요가 없어졌고, 오히려 View에 별도의 코드를 만들어야 하는 불편함이 생겼다.
따라서 View가 계속해서 모델을 관찰하는 형태가 아닌 Presenter가 중개자 역할을 하는 형태로 처리하는 것이유니티 상황에선 관리하기 용이하다.
* 유니티에서는 그러므로 MVP 패턴을 사용하는 것을 권장한다.
MVP를 이용한 UI 구현의 과정을 알아보자.
using UnityEngine;
public class PlayerCtrl : MonoBehaviour
{
[SerializeField] PlayerMod model;
void Update()
{
HPUp();
HPDown();
Jump();
}
private void HPUp()
{
if(Input.GetKeyDown(KeyCode.D))
{
model.HP += 1;
}
}
private void HPDown()
{
if (Input.GetKeyDown(KeyCode.A))
{
model.HP -= 1;
}
}
private void Jump()
{
if(Input.GetKeyDown(KeyCode.Space))
{
model.Rigid.AddForce(Vector3.up * 10, ForceMode.Impulse);
model.JumpCount += 1;
}
}
}
using UnityEngine;
public class PlayerMod : MonoBehaviour
{
[SerializeField] int hp;
public int HP { set { hp = value; OnHPChanged?.Invoke(hp); } get { return hp; } }
public event Action<int> OnHPChanged;
[SerializeField] int maxHp;
public int MaxHP { get { return maxHp; } }
[SerializeField] Rigidbody rigid;
[SerializeField] int jumpCount;
public Rigidbody Rigid { get { return rigid; } }
public int JumpCount { set { jumpCount = value; OnJumpCountChanged?.Invoke(jumpCount); } get { return jumpCount; } }
public event Action<int> OnJumpCountChanged;
}
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class PlayerPresent : MonoBehaviour
{
[Header("Model")]
[SerializeField] PlayerMod model;
[Header("View")]
[SerializeField] TMP_Text playerHPText;
[SerializeField] TMP_Text playerMaxHpText;
[SerializeField] Slider playerHPSlider;
[SerializeField] TMP_Text playerJumpText;
private void OnEnable()
{
model.OnHPChanged += SetHp;
model.OnJumpCountChanged += SetJumpCount;
SetMaxHp(model.MaxHP);
SetHp(model.HP);
}
private void OnDisable()
{
model.OnHPChanged -= SetHp;
model.OnJumpCountChanged -= SetJumpCount;
}
private void Update()
{
}
public void SetHp(int hp)
{
StringBuilder HP = new StringBuilder();
HP.Append($"HP : {hp} / ");
playerHPText.text = HP.ToString();
playerHPSlider.value = hp;
}
public void SetMaxHp(int maxHp)
{
StringBuilder MaxHp = new StringBuilder();
MaxHp.Append($"{maxHp}");
playerMaxHpText.text = maxHp.ToString();
playerHPSlider.maxValue = maxHp;
}
public void SetJumpCount(int jumpCount)
{
StringBuilder JumpCount = new StringBuilder();
JumpCount.Append($"JumpCount : {jumpCount}");
playerJumpText.text = JumpCount.ToString() ;
}
}
해당 내용은 깊게 다루지 않아 이론적인 내용만 정리하려 한다.
Model
데이터를 다루는 부분. 비즈니스 로직을 포함한다.
View
레이아웃과 화면을 보여주는 역할
ViewModel
View와 Model 사이에서 중재자 역할을 수행한다.
View에서 발생하는 이벤트를 감지하고, 해당 이벤트에 맞는 비즈니스 로직을 수행합니다.
Model과 상호작용하여 데이터를 가져오거나 업데이트하고, View에 데이터를 업데이트하는 역할을 합니다.
View에 표시할 데이터를 가공하여 제공하는 역할을 합니다.
사용자의 Action들은 View를 통해 들어온다.
View에 Action이 들어오면 ViewModel에 Action을 전달한다.
ViewModel은 Model에게 데이터를 요청한다.
Model은 ViewModel에게 요청받은 데이터를 응답한다.
ViewModel은 응답 받은 데이터를 가공하여 저장한다.
View는 Data Binding을 이용해 UI를 갱신한다.
언뜻 보기에는 MVP와 비슷하나, MVP는 View와 Presenter 사이의 의존관계가 1:1로 형성되어있다면, MVVM은 View와 ViewModel사이의 관계가 1대n으로 되어있다. 또한 데이터 바인딩을 이용한다면 View와 ViewModel 사이의 의존성을 없앨 수 있다.
장점
뷰 로직과 비지니스 로직을 분리하여 생산성을 높힐 수 있다. (UI가 나오지 않아도 개발 가능)
테스트가 수월해진다. (의존성이 없기 때문)
뷰와 뷰모델이 1:n 관계이기 때문에 중복되는 로직을 모듈화 해서 여러 뷰에 적용할 수 있다. (코드 재사용 가능)
많은 기업들이 애용하는 디자인 패턴이다.
단점
설계하기가 복잡하다. (Rx,데이터 바인딩에 대한 지식 필요)
뷰모델이 비대해질 수 있다.
데이터 바인딩으로 인한 메모리 소모가 심하다.
ViewModel 설계가 복잡하다는 단점이 있다.