Unity UI

정선호·2023년 7월 11일
0

Unity Features

목록 보기
14/28
post-thumbnail

재생목록

Making UI That Looks Good In Unity

강의 영상

개요

유니티에서 UI 레이아웃을 디자인할 때 유용한 팁들에 대한 설명

  • 게임 내내 UI가 균일하게 보이도록 하는 방법
  • 유니티 레이아웃 그룹과 레이아웃 엘리먼트 컴포넌트에 관한 거의 모든 것
  • grab pass shader를 이용해 블러 패널 컴포넌트를 디자인하는 방법

UI의 기본

UI의 기본

사용자들은 색상, 스타일, 일관성 등을 주로 보며 UI의 좋고 나쁨을 판단한다.
좋은 UI 디자인을 확인하는 방법은 다른 게임에서 얻는 것이다. 여러 게임의 UI를 분석해 어떤 점이 좋고, 어떤 점이 부족한지를 파악한다.

UI는 두 가지 디자인으로 나눌 수 있다.

  • Flat(평면) : 그림이 주를 이루는 디자인
  • Diagetic(설명) : 문자가 주를 이루는 디자인

Flat형 UI가 만들기도 쉽고, 심미적으로 더 좋아보이기 쉽다.

컬러 팔레트

UI를 제작하기 위해서는 색에 대한 기본적인 이론과 지식을 알아야 한다.
참고 영상을 이용해 색에 대한 기본 지식을 공부하기를 추천한다.

UI에 사용할 색을 여러 조합으로만 제한하는 것은 인터페이스의 일관성과 편안함을 증대시켜 준다.
인터넷의 여러 컬러 팔레트 페이지에서 다양한 색조합을 확인할 수 있다.

Unity의 컬러 팔레트

컬러 프로퍼티 팝업의 메뉴 창에서 Create New Library를 이용해 컬러 조합들을 라이브러리에 저장해 빠르게 재사용할 수 있다.

Unity 레이아웃

UI를 디자인할 때 레이아웃 그룹의 컴포넌트들을 주로 사용하게 된다.
이들은 오브젝트들에 붙어서 Rect Transform을 기준으로 하여 해당 UI의 위치와 크기를 알맞게 맞춰주는 역할을 한다.
이 중에 자주 사용하는 것들은 다음과 같다.

레이아웃 컴포넌트들은 부모와 자식이 함께 적합해야 하는 방법을 정의하는 규칙의 중첩된 요소이다.

UI Blur

포스트 프로세싱을 이용한 URP UI Blur

쉐이더그래프를 이용한 URP UI Blur


How To Get A Better Grid Layout in Unity

강의 영상

개요

그리드 레이아웃은 인벤토리, 아이템 선택, 퍼즐 레이아웃 등 다양한 곳에서 사용된다.
요즈음은 그리드를 이용한 멀티플랫폼 UI가 대세이다.
이번 강의에서 공부할 내용은 다음과 같다.

  • 가독성 좋은 폰트
  • UI패널들을 스위칭하는 탭 컴포넌트 빌드
  • 플렉시블 그리드 레이아웃 시스템 빌드

가독성 좋은 폰트

게임의 UI를 분석해보면 가독성이 좋은 글자 폰트들을 파악할 수 있다.
좋은 폰트는 다음과 같이 선정한다.

  • 널리 사용되는 폰트를 사용한다.
  • 다른 사용처마다 단 하나의 폰트만 사용한다.
  • 폰트의 종류, 굵기, 크기 등을 항상 유지한다.

헤더에 사용되는 굵고 큰 폰트와 본문에 사용할 작고 가는 폰트의 조합을 추천한다.

탭 컴포넌트

하이어라키 레이아웃

  • 헤더를 Left, Tabs, Right로 나누어 좌우에 버튼을 추가해, 버튼으로 탭을 변경할 수 있게끔 스크립트 작성

실제 적용 사진

플렉시블 그리드 레이아웃

기존의 Grid Layout은 자식 오브젝트의 크기와 정렬 등을 유연하게 처리하지 못 하므로 커스텀레이아웃 스크립트를 작성해 사용한다.


using UnityEngine;
using UnityEngine.UI;

namespace Utils.UI
{
    public class FlexibleGridLayout : LayoutGroup
    {
        public enum FitType
        {
            Uniform,
            Width,
            Height,
            FixedRows,
            FixedColumns,
        }

        public FitType fitType;

        public int rows;
        public int columns;
        public Vector2 cellSize;
        public Vector2 spacing;

        public bool fitX;
        public bool fitY;


        public override void CalculateLayoutInputHorizontal()
        {

            base.CalculateLayoutInputHorizontal();

            if (fitType == FitType.Width || fitType == FitType.Height || fitType == FitType.Uniform)
            {
                fitX = true;
                fitY = true;

                float sqrRt = Mathf.Sqrt(rectChildren.Count);
                rows = Mathf.CeilToInt(sqrRt);
                columns = Mathf.CeilToInt(sqrRt);
            }

            if (fitType == FitType.Width || fitType == FitType.FixedColumns)
            {
                fitY = true;
                rows = Mathf.CeilToInt(rectChildren.Count / (float)columns);
            }

            if (fitType == FitType.Height || fitType == FitType.FixedRows)
            {
                fitX = true;
                columns = Mathf.CeilToInt(rectChildren.Count / (float)rows);
            }

            rows = rows <= 0 ? 1 : rows;
            columns = columns <= 0 ? 1 : columns;


            Rect r = rectTransform.rect;
            float parentWidth = r.width;
            float parentHeight = r.height;

            float cellWidth = (parentWidth / columns) - ((spacing.x / (columns)) * (columns - 1)) -
                              (padding.left / (float)columns) - (padding.right / (float)columns);
            float cellHeight = (parentHeight / rows) - ((spacing.y / (rows)) * (rows - 1)) -
                               (padding.top / (float)rows) - (padding.bottom / (float)rows);

            cellSize.x = fitX ? cellWidth : cellSize.x;
            cellSize.y = fitY ? cellHeight : cellSize.y;

            for (int i = 0; i < rectChildren.Count; i++)
            {

                int rowCount = i / columns;
                int columnCount = i % columns;

                var item = rectChildren[i];

                var xPos = (cellSize.x * columnCount) + (spacing.x * columnCount) + padding.left;
                var yPos = (cellSize.y * rowCount) + (spacing.y * rowCount) + padding.top;

                SetChildAlongAxis(item, 0, xPos, cellSize.x);
                SetChildAlongAxis(item, 1, yPos, cellSize.y);
            }

        }

        public override void CalculateLayoutInputVertical()
        {
        }

        public override void SetLayoutHorizontal()
        {
        }

        public override void SetLayoutVertical()
        {
        }
    }
}



Designing A Responsive Tooltip System in Unity

강의 영상

개요

Flat형식의 UI를 개발할 시 주로 그림 위주의 UI를 작성하다 보니 게임, 혹은 해당 UI의 용도 등에 대한 설명이 부족한 경우가 생긴다.

이를 해소하기 위해서는 플레이어에게 텍스트로 이루어진 부가설명을 제공해 주어야 하는데, 이 때 툴팁 시스템이 매우 유용하게 사용된다.

툴팁 시스템은 플레이어에게 자세한 정보를 준다는 목적과 UI를 깔끔하게 유지해야 한다는 목적을 동시에 만족시킬 수 있다.

이번 강의에서는 유연한 툴팁 시스템을 만들기 위해 다음을 학습힌다.

  • 툴팁을 그리고 표시해주는 툴팁 시스템을 디자인한다.
  • 상호작용을 다루기 위한 툴팁 트리거 컴포넌트를 구현한다.
  • 콘텐츠 양에 따른 동적 사이즈의 툴팁 만들기를 해본다.
  • 툴팁 포지셔닝과 앵커링을 실시한다.

툴팁 만들기

툴팁 전용 캔버스를 따로 만든 후 정렬 순서값을 매우 높여 항상 화면의 최상단에 위치하도록 한다.
툴팁은 레이캐스팅이 필요 없으므로 Graphic Raycasting컴포넌트를 삭제한다.

툴팁 오브젝트를 만들고, 그 자식으로 헤더와 콘텐츠 텍스트를 추가한다.
툴팁 오브젝트에 다음과 같은 컴포넌트들을 적용한다.

  • Vertical Layout Group을 추가하고 Control Child Size만 활성화시킨다.
  • Content Size Fitter를 추가해 모든 Fit을 Preffered Size로 변경한다.
  • Layout Element를 추가해 Preffered size를 적절히 제한한다.
  • 다음 스크립트를 추가해 사이즈가 내용에 따라 동적으로 변화하게 해준다.


툴팁 시스템 및 트리거 만들기

다음과 같이 툴팁을 켜고 끌 수 있는 시스템을 만든다.

다음과 같이 툴팁 트리거를 만든다.

툴팁 시스템을 빈 오브젝트에 생성하여 전역 참조할 수 있게 하고, 툴팁 트리거를 툴팁이 필요한 UI에 추가하면 툴팁이 팝업되고 사라지는 것을 볼 수 있다.

툴팁 포지셔닝 및 앵커링

툴팁 스크립트에 RectTransform을 추가하여 화면 위치에 따라 트랜스폼을 변환하는 스크립트를 추가해준다.

LeanTween 등으로 툴팁이 딜레이되어 나오게끔 하는 등의 변화를 줄 수도 있다.
또한 UI뿐 아니라 콜라이더를 갖고 있는 오브젝트에 툴팁 트리거를 추가하여 오브젝트에 대한 툴팁도 만들 수 있다.


How to create Progress Bars in Unity

강의 영상

개요

로딩바, 체력바 등에 사용되는 진행도바는 플레이어에게 글을 읽히는 것보다 더 빠르고 직관적으로 정보를 전달해준다.

이번 강의에서는 다음과 같은 작업을 실시한다.

  • 유니티 UI 툴들을 이용해 진행도바를 만든다.
  • 진행도바 스크립트를 작성한다.
  • 원형 진행도바를 디자인하고 스크립트를 작성한다.
  • 프리팹화 및 메뉴 추가로 하이어라키에 빠르게 추가할 수 있게끔 한다.

진행도바 구현

Progress Bar, Mask, Fill 셋 다 이미지이다. 자세한 구현은 유튜브 참조

스크립트 작성

프리팹화 및 메뉴 추가

위와 같이 스크립트를 작성한 후 아래와 같이 에디터 모드에서 추가할 수 있다.

원형 진행도바

원형 진행도바 또한 선형 진행도바와 같이 제작한 후 스크립트를 추가하여 만들 수 있다.

또한 다음과 같이 에디터 모드에서 추가시킬 수 있다.


Why you should use code to animate your UI in Unity.

강의 영상

개요

UI에 애니메이션 효과를 주기 위해 Animation을 사용하는 것은 리소스를 많이 잡아먹는 비효율적인 방법이다.

UI에 팝업 등의 애니메이션 효과를 트랜스폼 변경 등 스크립트로 구현하는 방법이 있는데 이를 UI Tweening이라 한다.

트위닝은 다음과 같은 이유로 실시한다.

  • 간단한 애니메이션 구현에 고효율적이다.
  • UI 퍼포먼스를 더욱 높여준다.
  • 스크립트를 이용해 컴포넌트 값들을 애니메이팅할 수 있다.

LeanTween

개요

유니티 애셋스토어에 올라온 강력한 무료 트위닝 애셋이다.

트위닝의 시작 혹은 종료 등 특정 상황에서 이벤트를 델리게이트할 수 있어 간편하고 유용한 작업을 수행할 수 있다.

주의할 점으로는 gameObject를 할당하는 것보다 게임오브젝트 내의 Transform 혹은 RectTransform을 할당하는 것이 더욱 안전하다.

오브젝트 트위닝

다음과 같이 스크립트를 작성해 오브젝트를 위아래로 움직이게 하는 등의 트위닝을 할 수 있다.

LeanTween의 타입을 변경하거나, AnimationCurve의 그래프를 변경하여 다양한 종류의 움직임을 나타낼 수 있다.


Creating a Custom Tab System in Unity

강의 영상

개요

탭은 책의 중간중간에 끼워져 빠르게 특정 부분으로 넘길 수 있는 장치이다.
UI내에서는 대체적으로 옵션 메뉴, 세팅을 변경할 때 혹은 완전 새로운 유형의 프론트엔드로 넘어갈 때 등에 사용된다.

이번 강의에서는 다음 작업들을 수행한다.

  • 유니티 UI 툴을 이용해 탭 시스템을 만든다.
  • 마우스 이벤트 핸들링을 포함한 탭 버튼 스크립트를 작성한다.
  • 여러 탭을 매니징하는 컨트롤 컴포넌트를 작성한다.
  • 활성화된 게임오브젝트들을 스왑하는 등의 기본 탭 행동을 정의한다.

탭 시스템

개요

탭 시스템은 비슷한 UI를 한 공간에 묶을 때 주로 사용되며, 화면 공간과 리소스의 낭비를 막을 수 있다.

  • 유니티 UI툴을 이용해 탭 시스템을 구현한다.
  • 마우스 이벤트 핸들링을 갖는 탭 버튼 스크립트를 작성한다.
  • 다수의 탭을 관리하는 컨트롤 컴포넌트를 작성한다.
  • 활성화된 게임오브젝트들을 변경하는 등 탭의 디폴트 행동을 정의한다.

하이어라키 구성

Tap Area에는 TapGroup스크립트를 추가하고, Tab에는 TabButton스크립트를 추가한다.

TapGroup 스크립트

TapButton 스크립트


Designing a Loading Screen in Unity

강의 영상

로딩 스크린 스크립트 예시

개요

로딩 스크린은 연극의 가림막 같은 것이다.
게임이 로딩 되는 도중에 플레이어에게 로딩 과정을 숨기기 위한 장치로, 어떠한 게임에든 포함되어 있다.

  • 유니티 에디터에서 로딩 스크린을 디자인한다.
  • 씬 로딩법과 씬 로딩 도중 로딩 스크린을 사용하게 한다.
  • 씬 초기화 상태를 추적하는 동적인 로딩 스크린을 만든다.

로딩 스크린 하이어라키

간단한 배경 이미지, 설명 텍스트, 진행도 바가 존재하는 로딩 스크린이다.

로딩 스크린 스크립트

씬 로딩만 담당하는 스크립트

로딩 스크린을 세운 후 비동기 방식으로 씬을 로딩하고, 모든 씬의 로딩이 완성되면 로딩 스크린을 없애는 코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;

namespace Utils.UI
{
    public class LoadingScreen : MonoBehaviour
    {
        public static LoadingScreen instance;
        public GameObject loadingScreen;

        private List<AsyncOperation> _scenesLoading;

        public ProgressBar bar;


        #region Unity Methods

        private void Awake()
        {
            instance = this;

            SceneManager.LoadSceneAsync("CurrentSceneName", LoadSceneMode.Additive);
        }

        #endregion


        #region Loading Methods

        public void LoadGame()
        {
            loadingScreen.gameObject.SetActive(true);
            
            _scenesLoading.Add(SceneManager.UnloadSceneAsync("CurrentSceneName"));
            _scenesLoading.Add(SceneManager.LoadSceneAsync("SceneToLoadName", LoadSceneMode.Additive));
            _scenesLoading.Add(SceneManager.LoadSceneAsync("SceneToLoadName2", LoadSceneMode.Additive));

            StartCoroutine(GetSceneLoadProgress());
        }

        // IMPROVEMENT : async/await를 이용해 코루틴을 대체할 수 있음
        private float _totalSceneProgress;
        private IEnumerator GetSceneLoadProgress()
        {
            foreach(var scene in _scenesLoading)
            {
                while (!scene.isDone)
                {
                    _totalSceneProgress = 0;

                    foreach (AsyncOperation op in _scenesLoading)
                    {
                        _totalSceneProgress += op.progress;
                    }

                    _totalSceneProgress = (_totalSceneProgress / _scenesLoading.Count) * 100f;

                    bar.value = Mathf.RoundToInt(_totalSceneProgress);
                    
                    yield return new WaitForEndOfFrame();
                }
            }
            
            loadingScreen.gameObject.SetActive(false);
        }

        #endregion
    }
}

씬 내 컴포넌트 로딩을 포함한 스크립트

씬 로드 후 씬 내에서 오브젝트 다수 생성 등 무거운 연산을 수행해야 할 때 해당 연산까지 로딩 스크린이 가리게 하는 스크립트

우선 무거운 연산을 수행하는 스크립트에 연산 수행정도와 연산 수행여부를 판단하는 변수와 함수를 추가한다.

이후 로딩 스크린 스크립트에 해당 값을 불러와 처리한다.

private float _totalSpawnProgress;
public IEnumerator GetTotalProgress()
{
    float totalProgress = 0;

    while ("SpawningScript".instance == null || "SpawningScript".instance.isDone)
    {
        if ("SpawningScript".instance == null)
        {
            totalProgress = 0;
        }
        else
        {
            _totalSpawnProgress = "SpawningScript".current.progress * 100f;
        }

        totalProgress = Mathf.Round(_totalSceneProgress + _totalSpawnProgress) / 2;
        bar.value = Mathf.RoundToInt(totalProgress);

        yield return null;
    }

    loadingScreen.gameObject.SetActive(false);
}

최종 스크립트

텍스트로 진행률을 명시적으로 표시하는 기능까지 추가한 최종 스크립트

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.UIElements;

namespace Utils.UI
{
    public class LoadingScreen : MonoBehaviour
    {
        public static LoadingScreen instance;
        public GameObject loadingScreen;

        private List<AsyncOperation> _scenesLoading;

        public ProgressBar bar;
        public Text text;


        #region Unity Methods

        private void Awake()
        {
            instance = this;

            SceneManager.LoadSceneAsync("CurrentSceneName", LoadSceneMode.Additive);
        }

        #endregion


        #region Loading Methods

        public void LoadGame()
        {
            loadingScreen.gameObject.SetActive(true);
            
            _scenesLoading.Add(SceneManager.UnloadSceneAsync("CurrentSceneName"));
            _scenesLoading.Add(SceneManager.LoadSceneAsync("SceneToLoadName", LoadSceneMode.Additive));
            _scenesLoading.Add(SceneManager.LoadSceneAsync("SceneToLoadName2", LoadSceneMode.Additive));

            StartCoroutine(GetSceneLoadProgress());
        }

        // IMPROVEMENT : async/await를 이용해 코루틴을 대체할 수 있음
        private float _totalSceneProgress;
        private IEnumerator GetSceneLoadProgress()
        {
            foreach(var scene in _scenesLoading)
            {
                while (!scene.isDone)
                {
                    _totalSceneProgress = 0;

                    foreach (AsyncOperation op in _scenesLoading)
                    {
                        _totalSceneProgress += op.progress;
                    }

                    _totalSceneProgress = (_totalSceneProgress / _scenesLoading.Count) * 100f;
                    bar.value = Mathf.RoundToInt(_totalSceneProgress);
                    text.text = "Instantiate Object: "+ _totalSceneProgress + "%";
                    
                    yield return new WaitForEndOfFrame();
                }
            }
            loadingScreen.gameObject.SetActive(false);
        }

        private float _totalSpawnProgress;
        public IEnumerator GetTotalProgress()
        {
            float totalProgress = 0;

            while ("SpawningScript".instance == null || "SpawningScript".instance.isDone)
            {
                if ("SpawningScript".instance == null)
                {
                    totalProgress = 0;
                }
                else
                {
                    _totalSpawnProgress = "SpawningScript".current.progress * 100f;
                    text.text = "Instantiate Object: "+ _totalSpawnProgress + "%";
                }

                totalProgress = Mathf.Round(_totalSceneProgress + _totalSpawnProgress) / 2;
                bar.value = Mathf.RoundToInt(totalProgress);

                yield return null;
            }

            loadingScreen.gameObject.SetActive(false);
        }


        #endregion
    }
}

기타 기능들

Enum을 활용해서 현재 스크립트의 상태까지 표현해줄 수 있다.

게임 로딩 중에 팁을 나열해주는 코루틴도 구현할 수 있다.

로딩 씬 코드 예시

두 무거운 씬 사이에 로딩 씬을 넣어 로딩 씬을 앞에 대기시키고 이후 로드할 씬이 완성되면 로딩 씬에서 로드된 씬으로 넘어가는 방법이다.

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class LoadingScene : MonoBehaviour 
{
    private static string sceneToLoad = "Atmospherics";

    [SerializeField]
    private Slider progressBarSlider;

    private void Start()
    {
        StartCoroutine(LoadSceneAsync(sceneToLoad));
    }

    private IEnumerator LoadSceneAsync(string sceneName)
    {
        yield return new WaitForSeconds(0.1f);

        var task = SceneManager.LoadSceneAsync(sceneToLoad, LoadSceneMode.Additive);
  
        while (!task.isDone)
        {
            progressBarSlider.value = task.progress;
            yield return null;
        }
        progressBarSlider.value = 1;
        yield return new WaitForSeconds(.2f);
        SceneManager.UnloadSceneAsync("Loading Scene");
    }

    public static void StartLoadingScene(string nameOfSceneToLoad)
    {
        sceneToLoad = nameOfSceneToLoad;
        SceneManager.LoadScene("Loading Scene");
    }
}

Making a Modal Window in Unity

강의 영상

개요

튜토리얼 다이얼로그, 이벤트 표시, 팝업 메뉴바 등등 팝업되는 모든 UI를 모달이라 한다.
매우 많은 곳에서 사용되므로 유연하고 재사용이 가능하게 만들어야 한다.
OS의 파일 탐색기 창처럼 떠 있으면서 이동할 수 있는 윈도우를 모달리스 윈도우라고 한다.

  • 유니티 UI 레이아웃으로 모달 윈도우를 디자인한다.
  • 모달 윈도우 컨트롤러 컴포넌트를 구현한다.
  • 외부 파라미터에 따라 윈도우의 레이아웃을 업데이트한다.

구현

동영상 참조

profile
학습한 내용을 빠르게 다시 찾기 위한 저장소

0개의 댓글