유니티에서 UI 레이아웃을 디자인할 때 유용한 팁들에 대한 설명
grab pass shader
를 이용해 블러 패널 컴포넌트를 디자인하는 방법사용자들은 색상, 스타일, 일관성 등을 주로 보며 UI의 좋고 나쁨을 판단한다.
좋은 UI 디자인을 확인하는 방법은 다른 게임에서 얻는 것이다. 여러 게임의 UI를 분석해 어떤 점이 좋고, 어떤 점이 부족한지를 파악한다.
UI는 두 가지 디자인으로 나눌 수 있다.
Flat형 UI가 만들기도 쉽고, 심미적으로 더 좋아보이기 쉽다.
UI를 제작하기 위해서는 색에 대한 기본적인 이론과 지식을 알아야 한다.
참고 영상을 이용해 색에 대한 기본 지식을 공부하기를 추천한다.
UI에 사용할 색을 여러 조합으로만 제한하는 것은 인터페이스의 일관성과 편안함을 증대시켜 준다.
인터넷의 여러 컬러 팔레트 페이지에서 다양한 색조합을 확인할 수 있다.
컬러 프로퍼티 팝업의 메뉴 창에서 Create New Library
를 이용해 컬러 조합들을 라이브러리에 저장해 빠르게 재사용할 수 있다.
UI를 디자인할 때 레이아웃 그룹의 컴포넌트들을 주로 사용하게 된다.
이들은 오브젝트들에 붙어서 Rect Transform
을 기준으로 하여 해당 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()
{
}
}
}
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뿐 아니라 콜라이더를 갖고 있는 오브젝트에 툴팁 트리거를 추가하여 오브젝트에 대한 툴팁도 만들 수 있다.
로딩바, 체력바 등에 사용되는 진행도바는 플레이어에게 글을 읽히는 것보다 더 빠르고 직관적으로 정보를 전달해준다.
이번 강의에서는 다음과 같은 작업을 실시한다.
Progress Bar
, Mask
, Fill
셋 다 이미지이다. 자세한 구현은 유튜브 참조
위와 같이 스크립트를 작성한 후 아래와 같이 에디터 모드에서 추가할 수 있다.
원형 진행도바 또한 선형 진행도바와 같이 제작한 후 스크립트를 추가하여 만들 수 있다.
또한 다음과 같이 에디터 모드에서 추가시킬 수 있다.
UI에 애니메이션 효과를 주기 위해 Animation
을 사용하는 것은 리소스를 많이 잡아먹는 비효율적인 방법이다.
UI에 팝업 등의 애니메이션 효과를 트랜스폼 변경 등 스크립트로 구현하는 방법이 있는데 이를 UI Tweening
이라 한다.
트위닝은 다음과 같은 이유로 실시한다.
유니티 애셋스토어에 올라온 강력한 무료 트위닝 애셋이다.
트위닝의 시작 혹은 종료 등 특정 상황에서 이벤트를 델리게이트할 수 있어 간편하고 유용한 작업을 수행할 수 있다.
주의할 점으로는 gameObject
를 할당하는 것보다 게임오브젝트 내의 Transform
혹은 RectTransform
을 할당하는 것이 더욱 안전하다.
다음과 같이 스크립트를 작성해 오브젝트를 위아래로 움직이게 하는 등의 트위닝을 할 수 있다.
LeanTween
의 타입을 변경하거나, AnimationCurve
의 그래프를 변경하여 다양한 종류의 움직임을 나타낼 수 있다.
탭은 책의 중간중간에 끼워져 빠르게 특정 부분으로 넘길 수 있는 장치이다.
UI내에서는 대체적으로 옵션 메뉴, 세팅을 변경할 때 혹은 완전 새로운 유형의 프론트엔드로 넘어갈 때 등에 사용된다.
이번 강의에서는 다음 작업들을 수행한다.
탭 시스템은 비슷한 UI를 한 공간에 묶을 때 주로 사용되며, 화면 공간과 리소스의 낭비를 막을 수 있다.
Tap Area
에는 TapGroup
스크립트를 추가하고, Tab
에는 TabButton
스크립트를 추가한다.
로딩 스크린은 연극의 가림막 같은 것이다.
게임이 로딩 되는 도중에 플레이어에게 로딩 과정을 숨기기 위한 장치로, 어떠한 게임에든 포함되어 있다.
간단한 배경 이미지, 설명 텍스트, 진행도 바가 존재하는 로딩 스크린이다.
로딩 스크린을 세운 후 비동기 방식으로 씬을 로딩하고, 모든 씬의 로딩이 완성되면 로딩 스크린을 없애는 코드
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");
}
}
튜토리얼 다이얼로그, 이벤트 표시, 팝업 메뉴바 등등 팝업되는 모든 UI를 모달이라 한다.
매우 많은 곳에서 사용되므로 유연하고 재사용이 가능하게 만들어야 한다.
OS의 파일 탐색기 창처럼 떠 있으면서 이동할 수 있는 윈도우를 모달리스 윈도우라고 한다.
동영상 참조