XR 플밍 - 9. UnityEngine3D 응용 프로그래밍 - UI 복습, UI 연동과 MVC, MVP, MVVM 패턴 (5/12)

이형원·2025년 5월 12일
0

XR플밍

목록 보기
71/215

1. UI

이미 이전에 UI에 대해 배워본 바가 있지만, 리뷰하는 느낌으로 해당 내용을 다시 정리하고, 추가로 공부한 내용을 정리해보고자 한다.

1.1 UI란?

UI는 User Interface의 줄임말로, 사용자에게 제공되는 프로그램으로의 가장 직관적인 접근 요소를 말한다. UI는 사용 환경에 따라 다양한 해상도에 유연하게 적용되어야 하며, 이는 UI를 구성함에 있어 가장 중요한 핵심 요소이다. 게임에서의 UI는 프로그램의 유지보수, 업데이트와 같은 확장성을 고려한 객체지향적 설계가 적용되어야 할 중요한 요소이다.

1.2 UI의 구성 요소 - 캔버스와 RectTransform

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.3 NGUI/UGUI

1) NGUI
NGUI는 스크립트로만 UI를 컨트롤하는 방식을 말한다. Unity 기술이 발달하기 이전 많이 쓰던 방식이나, 지금에 와서는 UGUI의 형태로 작업하는 경우가 많다. 하지만 예전 기술의 회사의 경우 아직 NGUI를 쓰는 경우도 있다.

2) UGUI
UI가 유니티의 렌더링 파이프라인에 통합되어 있으며, 구성요소도 게임오브젝트와 같은 방식으로 처리하는 방식을 말한다. 별도의 컨트롤창이 아닌 하이어라키에서 직관적으로 편집할 수 있다.

2. 기본 지원 UI

2.1 Image/Raw Image

  • Image : 말 그대로 이미지를 가져올 수 있다. 특이사항으로는, 외부에서 온 이미지를 사용해야 할 때, 해당 이미지를 Default->Sprite로 변환해서 사용해야 한다. Image는 Sprite와 Color 데이터를 통해 이미지를 출력하기 때문에 렌더링 전 발생하는 전처리 과정으로 상황에 따라 처리 비용이 높을 수 있다.

  • Raw Image : Raw Image는 원본 이미지를 그대로 출력하기 때문에 동영상 같은 것도 출력이 가능하다. 또한 이미지에 대한 추가 처리가 필요하지 않아 처리 속도가 빠르고 주로 동영상 재생에 적합하다.

2.2 버튼

마우스 클릭/화면 터치를 위한 UI 구성요소이다. 내부적으로 클릭 시 동작을 정의할 수 있도록 OnClick 이벤트를 내장하고 있으며, 이벤트 트리거를 사용해 다양한 구현이 가능하다.

2.3 Slider

  • Slider : 볼륨키처럼 좌우로 왔다갔다 할 수 있다.

Slider를 이용하면 HP바를 만들 수도 있다. 방법은 아래와 같다.

  1. Slider를 생성하고, Handle Slide Area를 제거한다.

  2. Fill을 빨간색, Background를 검정색으로 설정한다.

  1. 스크립트를 HpBar에 삽입한다.
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;
    }
}

2.4 Scrollbar

  • Scrollbar : hp바처럼 크기를 늘렸다 줄였다 할 수 있다.

2.5 Scroll View

  • Scroll View : 가로, 세로 스크롤 바를 만들 수 있다.

구성요소로는 아래처럼 Vieport - Content 내부 공간이 있고, 가로와 세로 스크롤바가 각각 있다.

몇몇 옵선에 대해 알아보자.

  • 가로, 세로 바를 전부 사용할 건지 체크박스로 조절가능하다.

  • Movement Type은 세 종류가 있다.

    • Unrestricted : 화면에 제한이 없다. 마음대로 드래그 할 수 있다.
    • Elastic : 탄성이 있는 것처럼 화면을 드래그하면 돌아온다.
    • Clamped : 내용이 늘어나도 화면이 고정된다

2.6 TextMeshPro

말 그대로 텍스트를 지원해주는 UI 기능이다. 텍스트는 이미지의 형태로 저장되며 폰트의 별도 변환이 필요하다.

여기서 만약 한글 지원 폰트를 사용하고 싶을 경우 세팅하는 방법에 대해 알아보고자 한다.
사용한 폰트는 무료 폰트인 빛의 계승자 폰트로 진행했다.

Text Asset 생성

  • Source Font File : 폰트 원본 파일 삽입
  • Sampling Point Size : 텍스쳐화 될 폰트 이미지에 삽입될 글자의 크기, 일반적으로 Auto Sizing을 선택한다.
  • Padding : 글자 이미지 간의 여백, 보통 5를 선택한다.
  • Packing Method : 폰트들을 한 장의 이미지로 합쳐 아틀라스를 생성한다. 두 옵션의 차이가 크지 않으므로 'Fast'를 선택한다
  • Atlas Resolution : 글자로 구성된 한 장의 이미지의 최대 크기, 너무 작다면 폰트가 제대로 출력되지 않을 수 있고 너무 크다면 성능적으로 좋지 않다. 한글이라면 일반적으로 2048 ~ 4096을 추천한다.
  • Character Set : 폰트에셋으로 지정해 둘 폰트를 지정하는 단계. 한글폰트가 포함되어야 하므로 Custom Range로 설정한다.
  • Character Sequence : 유니코드 단위의 10진수로 범위를 지정한다. 32-126,44032-55203,12593-12643,8200-9900를 입력하며. 이는 각각 영문자, 한글, 한글 자모음, 특수문자의 범위이다.
  • RenderMode : 텍스트를 이미지화 할 때 사용되는 렌더링 모드. 거리에 따라 선명도를 계산해 렌더링 하는 방식인 SDF를 사용하고, 그 중에서도 SDF16 을 선택하지만 이 수치는 디바이스 환경이나 해상도 등에 차이가 있을 수 있으며 숫자가 높을수록 더 많은 연산이 발생하므로 많은 테스트를 통해 결정되어야 한다.

3. UI 연동과 MVC, MVP, MVVM 패턴

책임을 쪼개고 기능을 분리하는 데 있어 사용할 수 있는 패턴이다. 모델과 View를 나누고 연동하는 방식으로 기능을 구분하여, 관리에 있어 용이하게 하는 기법이다.

3.1 MVC(Model-View-Controller) 패턴

사용자 인터페이스를 개발할 때 흔히 사용되는 일련의 디자인 패턴이다.

소프트웨어의 논리적 부분, 데이터 부분, 프레젠테이션 부분을 분리하는 것이다.

  • 모델은 데이터를 저장한다.
  • 뷰는 인터페이스로, 프레젠테이션 부분을 담당한다.
  • 컨트롤러는 로직 - 논리적 처리 부분을 담당한다.

MVC를 이용한 플레이어 HP UI 표기를 살펴보자.

  • 모델은 데이터를 저장한다. 따라서 HP를 세팅하고, HP를 불러오는 역할만 수행한다.
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);
    }
}
  • 뷰는 인터페이스로 텍스트로 표시된 UI와 Bar로 표시된 UI로 분리하였다.
// 플레이어 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}";
    }
}
  • 컨트롤러는 로직만을 처리한다. HP가 감소하는 로직만이 컨트롤러에 담겨 있다.
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] PlayerModel model;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            model.HP -= 1;
        }
    }
}

MVC 디자인은 각 부분이 하나의 작업을 수행하여 기능을 분리할 수 있고, 단일 책임 원칙을 따르는 연장선상에 놓이는 데에 장점이 있다.

3.2 MVP(Model-View-Presenter) 패턴

유니티의 경우에서는 이미 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;
        }
    }
}
  • 이에 대한 데이터는 Model에 저장된다.
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;
}
  • 이에 대해서 Presenter가 데이터를 받아와서 View로서 UI의 시각적 처리에 대해 진행한다. 관리하는 UI가 3개나 있음에도 하나의 클래스로 처리한 것을 알 수 있다.
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() ;
    }
}

3.3 MVVM(Model-View-View-Model) 패턴

해당 내용은 깊게 다루지 않아 이론적인 내용만 정리하려 한다.

3.3.1구성요소

  • Model
    데이터를 다루는 부분. 비즈니스 로직을 포함한다.

  • View
    레이아웃과 화면을 보여주는 역할

  • ViewModel
    View와 Model 사이에서 중재자 역할을 수행한다.
    View에서 발생하는 이벤트를 감지하고, 해당 이벤트에 맞는 비즈니스 로직을 수행합니다.
    Model과 상호작용하여 데이터를 가져오거나 업데이트하고, View에 데이터를 업데이트하는 역할을 합니다.
    View에 표시할 데이터를 가공하여 제공하는 역할을 합니다.

3.3.2 MVVM 동작 과정

사용자의 Action들은 View를 통해 들어온다.
View에 Action이 들어오면 ViewModel에 Action을 전달한다.
ViewModel은 Model에게 데이터를 요청한다.
Model은 ViewModel에게 요청받은 데이터를 응답한다.
ViewModel은 응답 받은 데이터를 가공하여 저장한다.
View는 Data Binding을 이용해 UI를 갱신한다.

3.3.3 MVVM 특징

언뜻 보기에는 MVP와 비슷하나, MVP는 View와 Presenter 사이의 의존관계가 1:1로 형성되어있다면, MVVM은 View와 ViewModel사이의 관계가 1대n으로 되어있다. 또한 데이터 바인딩을 이용한다면 View와 ViewModel 사이의 의존성을 없앨 수 있다.

3.3.4 MVVM 장단점

장점
뷰 로직과 비지니스 로직을 분리하여 생산성을 높힐 수 있다. (UI가 나오지 않아도 개발 가능)
테스트가 수월해진다. (의존성이 없기 때문)
뷰와 뷰모델이 1:n 관계이기 때문에 중복되는 로직을 모듈화 해서 여러 뷰에 적용할 수 있다. (코드 재사용 가능)
많은 기업들이 애용하는 디자인 패턴이다.

단점
설계하기가 복잡하다. (Rx,데이터 바인딩에 대한 지식 필요)
뷰모델이 비대해질 수 있다.
데이터 바인딩으로 인한 메모리 소모가 심하다.
ViewModel 설계가 복잡하다는 단점이 있다.

profile
게임 만들러 코딩 공부중

0개의 댓글