[Unity/디자인 패턴] UI 패턴에 대해

Donghee·2024년 8월 10일
0

개요

최근 여러 프로젝트를 진행하며 손쉬운 UI 추가/삭제에 대해 어려움을 느꼈다. 때문에 유니티의 UI 패턴에 대해 찾아보았는데, Unity Korea 유튜브에 좋은 영상이 있어 이를 보고 정리했다. 이 글에 나오는 그림과 코드는 모두 이 영상에서 가져온 것임을 미리 알린다. 꽤 길이가 있는 내용이지만 레트로님이 설명을 잘해주셔서 직접 보는 것도 추천한다.

인게임 UI와 페이징 UI

HUD같은 인게임 UI는 별도의 UI 내비게이션을 생각할 필요가 없다. 인게임 UI들을 한 곳에 몰아놓고 그에 대한 레퍼런스에 접근하면 되기 때문이다. 하지만 이외의 페이징될 수 있는 UI(수집화면 UI, 설정 UI 등)는 A UI를 켰다가 B UI를 켜도 A UI의 상태를 기억해야 하는 등, 다양한 요구사항이 발생한다. 이를 우리는 UI 내비게이션을 통해 통제할 수 있다.

페이징 UI의 종류

먼저 페이징 UI의 종류를 알아보고 접근 방법을 생각해보자.

계층

계층 구조는 맨 위에 루트 페이지가 존재하는 구조이다. 가장 흔한 구조이고, 가장 구현하기 쉽다.

플랫

플랫 구조는 메인 페이지가 4~5개, 혹은 그 이상도 존재한다. 각 페이지별로 이동할때 이전 페이지의 스택이 날라가지 않는다. 즉, A 페이지에서 여러 행동을 하다가, B 페이지로 넘어가도 A 페이지의 행동이 날라가지 않는 것이다. 이때 다시 A 페이지로 돌아오면 먼저 하고 있었거나 보여지고 있었던 UI가 보여진다.

콘텐츠 주도

콘텐츠 주도 구조는 콘텐츠에 따라 UI가 유동적으로 움직이는 구조이다. 루트 페이지가 존재하지 않는 것이 특징이다.

UI 내비게이션의 구현

먼저 가장 기본 형태인 계층 구조를 구현해보자. 여기에서는 UI 각각의 객체를 담당하는 UIView, 그리고 이를 관리하는 내비게이션인 UINavigation 클래스가 등장한다.

UIView

UIView somePage = UIView.Get("PageName");
somePage.Show();

모든 UIView 객체들은 Show와 Hide 메서드를 구현했다. 이를 호출하면, 당장 화면에 보여지거나 사라지게 하는 작업을 할 수 있다.
위 코드에서 보여진 "PageName"은 로드가 가능한 프리팹(Resources 폴더, 어드레서블 에셋 등)을 의미한다.
그러나 UIView에게 직접 Show나 Hide를 요구하는 경우는 없다. 우리는 이 작업을 관리하는 UINavigation에게 맡긴다.

VisibleState는 현재 보여지고 있는지 아닌지를 나타낸다. 영상에서는 Appearing, Appeared, Disappearing, Disappeared로 4개의 상태를 Enum으로 관리한다고 한다. UI가 보여지고 있는 중이거나 사라지는 중에 다른 행동을 하는 것을 제어하기 위해 Appearing과 Disappearing을 추가했다고 한다.

UINavigation

MonoBehaviour를 상속함.

var newPage = UINavigation.Push("PageName");
var previousPage = UINavigation.Pop();

마치 Stack과 비슷한 구조라고 생각하면 더 이해가 편하다. Push를 진행하면 가장 최근에 본 UI로 해당 UIView가 저장된다. 그리고 UIView 객체가 Show를 진행한다. 이후 Pop을 하면 가장 최근에 본 UI가 Hide를 진행하고, 그 다음 UIView가 가장 최근에 본 UI로 저장된다.

그림으로 이해해보자. 왼쪽엔 Scene에서 보여지고 있는 상황이고, 오른쪽은 UINavigation 객체에 저장된 History(UIView가 어떤 순서로 실행되었는지)이다.

현재는 Home이 보여지고 있고, Home > Account > Home 순서로 실행이 되었다.
여기서 UINavigation.Push("Library")를 해보자.

Library라는 해당하는 UIView를 찾아서, 해당 UI를 Show()를 한다. 직전의 페이지는 Hide()한다. 또한 UINavigation엔 Library가 실행되었다는 기록이 추가되었다.

플랫 구조에서의 UINavigation

플랫 구조를 다시 보자. 플랫 구조는 계층 구조에서의 루트가 여러개 있는 구조이다. 위에서의 UINavigation으로는 이를 관리하기가 어렵다. 내비게이션이 하나인 형태라 각각의 진행 상황을 한번에 관리할 수 없기 때문이다. 따라서 UINavigation을 여러개 놓고, 이를 관리하는 UINavigationController를 배치하는 형태로 바꾸면, 쉽게 관리할 수 있다.

이러면 서로 다른 Navigation에서 각자의 History를 저장할 수 있다. UINavigationController가 각 Navigation만 옮겨다니며 페이지를 관리해주면 된다.

프리팹에서 인스턴스화 vs 씬에 UIView 미리 배치

UINavigation에서 각 UI 페이지들을 불러올 때, 두 방식 중 어떤 것이 효율적일까?

프리팹에서 인스턴스화

이는 Resources 폴더 또는 어드레서블 에셋을 통해 프리팹을 불러오고, 그것을 인스턴스화하는 방식이다.

  • UINavigation.Push("Library") 실행시
    - 마지막 UIView의 Hide 실행
    - Library에 해당하는 UIView 프리팹을 가져와 인스턴스화 > Show 실행
    - 생성된 UIView를 History 스택에 Push
  • UINavigation.Pop() 실행시
    - 마지막 UIView의 Hide 실행
    • History에서 마지막 UIView 삭제
    • History의 마지막 UIView를 다음 UIView로 변경

하지만 여러 단점이 존재한다.

  • 동일한 UIView이지만 서로 다른 내용인 것이 여러개 띄워져 있으면, 같은 페이지에 대해서 여러개의 게임 오브젝트가 생성됨
  • 플레이 시작시에는 UIView가 하나도 없어야 하기 때문에, 플레이 버튼을 누르기 전까지 UI를 확인할 수가 없음

씬에 UIView가 미리 존재

이는 씬에 UIView들을 미리 배치시켜놓고, 플레이가 시작되면 전부 Hide되고 위치가 리셋되는 방식이다.

이 방식은 씬에 UIView가 무한히 생길 일이 없으므로 상대적으로 부담이 적고, 플레이에 들어가기 전에도 미리 볼 수 있어 작업이 효율적이다.

UIView 작업 플로우

버전 관리 시스템으로 작업을 진행할 때, 마스터 캔버스에 UIView 프리팹을 붙이는 식으로 작업한다. 씬에서 작업하는 것보다는 프리팹이 머지가 훨씬 쉽다. 따라서 씬 단위로 UI변경을 다루기 보다는, 프리팹으로 변경을 다루는 것이 유리하다. 프리팹을 변경하더라도 씬에서는 프리팹 레퍼런스를 잡지, 프리팹의 내용은 안 잡고 있기 때문에 변경의 영향이 없다.

UI 팝업

팝업은 똑같은 팝업이 여러개 뜰 수도 있다. 즉 겉은 비슷하지만 내용이 서로 다른 객체들이 여러개 나올 수도 있다는 말이다. 따라서 프리팹에서 인스턴스화를 진행한다.

var popup = PopupManager.Enqueue(Popup.GetPrefab("Default"));
popup.SetText("messageField", message);

팝업을 2개 이상 띄울 때, 하나의 팝업이 종료되어야 대기하던 그 다음 팝업이 호출되는 방식을 위해, 큐 자료구조를 채택했다. 여기서는 messageField라는 텍스트를 담는 게임오브젝트에 message라는 텍스트를 넣는 코드를 보여주고 있다.

MVVM 패턴

먼저 여기서 설명하는 것은 MVVM 패턴을 참고한 패턴이지, 완전한 MVVM 패턴은 아니다.

유니티 UI의 한계

평범한 유니티 UI엔 다음과 같은 한계가 존재한다.

  • 아티스트가 UI 프리팹 수정 > 레퍼런스가 망가짐
  • 뷰 셋업용 데이터 구조가 변경 > 뷰 코드가 망가짐
  • 형태는 같은데, 일부만 다른 UI로 변형 > 프로그래머가 아티스트 대신 레이아웃 구성

원하는 것

  • 새로운 UI 요소를 부담 없이 추가
  • 버튼 등의 유니티 이벤트에 할당한 리스너를 잃어버리지 않기
  • 디자이너가 직접 작업 가능하게 하기

MVVM 패턴의 특징

가장 직관적으로 보여주는 그림이다. 뷰가 실제로 보여지는 화면이고, 모델이 프로그램 안에서 데이터를 다루는 부분이다. 뷰모델은 그 사이에서 중재를 담당한다.
여기서 뷰와 뷰모델은 데이터 바인딩을 진행한다. 대신 서로 레퍼런스가 걸려있지 않고, 간접적으로 엮여있다. 뷰모델과 모델은 직접 연결된다. 뷰모델은 모델에게서 데이터를 전달받아, UI에 표현하기 위한 형태로 데이터를 변경한다. 결론적으로는 뷰와 모델이 동기화가 안되도 망가지지 않는다.

장점

  • 플레이 로직과 뷰 사이의 결합이 약해짐
  • 프로그래머와 아티스트가 비동기로 작업 가능
  • 포맷팅을 뷰모델에서 처리할 수 있음

위 사진은 뷰모델의 예시이다. 모델에서 받은 데이터들을 MasterContext > HomePageContext > FeaturedHeaderText와 같은 형태로 저장하고 있다. 이제 뷰에서는 이 컨텍스트 경로를 지정해주기만 하면, 알아서 모델의 데이터가 전달이 되는 것이다.
즉, 모델에서 뷰 모델에게 데이터를 전달해주고. 뷰는 알아서 원하는 것만 그때그때 빼가면 된다.
영상의 강의자분께서는 Data Bind for Unity라는 에셋을 통해 뷰모델과 뷰 사이의 데이터 바인딩을 진행했다고 한다. 직접 구현할 수도 있으나, 에셋이나 깃허브 등에 좋은 라이브러리가 많으므로 활용하는 것도 방법이겠다.

참고

profile
마포고개발짱

0개의 댓글