Optimizing UI Controls – 번역
Optimizing UI Controls
– 원문 링크 –
이전글 – Fill-rate, Canvases and input
Optimizing UI Controls
확인 완료한 버전: 5.3 – 난이도: 고급
유니티 UI 최적화 가이드의 이번 섹션에서는 특정 UI 컨트롤과 관련된 문제를 중점적으로 살펴보겠습니다. 상대적으로 대부분의 UI 컨트롤은 성능면에서 비슷하지만, 2가지 UI는 출시가능한 상태에 임박한 게임에서 성능 문제를 발생시키는 원인이 되는 경우가 많습니다.
UI text
유니티의 내장된 Text 컴포넌트는 UI에서 레스터화된(rasterized) 텍스트 메쉬를 화면에 표시하는데 편리한 수단입니다. 하지만, 일반적으로 알려지지 않은 몇가지 동작이 있는데, 이 동작으로 인해서 성능 문제가 발생하는 경우가 있습니다. 텍스트를 UI에 추가할 떄, 텍스트 문자 메쉬는 문자마다 개별 쿼드(quad)로 렌더링된다는 점을 명심해야합니다. 이 쿼드는 문자의 모양에 따라서, 상당한 양의 텍스트 메쉬 주변의 빈 공간을 차지하는 경향이 있기 때문에, 의도하지 않게 다른 UI 요소의 배칭(batching)을 깨는 위치에 텍스트를 위치시키는 경우가 쉽게 발생할 수 있습니다.
텍스트 메쉬 리빌드 (Text mesh rebuilds)
UI 텍스트 메쉬의 리빌드는 주요 문제 중 하나입니다. UI Text 컴포넌트가 변경될때마다, Text 컴포넌트는 실제 텍스트를 화면에 표시하는데 사용되는 폴리곤을 다시 계산해야합니다. 텍스트 컴포넌트 또는 컴포넌트의 부모 게임오브젝트가 단순히 비활성화된 다음 다시 활성화된 경우에, 텍스트가 변경되지 않더라도 이 폴리곤의 재계산이 발생합니다.
이 동작은 리더보드나 통계수치를 보여주는 화면과 같이, 다 수의 텍스트 레이블을 화면에 표시하는 UI에서 문제가 됩니다. UI를 표시하고 감추는데 사용되는 가장 일반적인 방법은 UI 요소를 포함하고있는 게임오브젝트를 활성화/비활성화시키는 방법인데, 다수의 텍스트 컴포넌트를 가진 UI는 종종 활성화될때마다 프레임이 갑자기 뚝 끊기는 딸꾹질현상을 발생시킵니다.
이 문제에 대한 해결방법은 다음 챕터의 캔버스 렌더러 비활성화하기 (Disabling Canvas Renderers) 섹션을 참고하시기 바랍니다.
다이나믹 폰트 및 폰트 아틀라스 (Dynamic fonts and font atlases)
다이나믹 폰트는 화면에 표시해야하는 글자 세트가 매우 크거나 실행 전에는 어떤 글자가 화면에 나타날지 모르는 경우에 텍스트를 표시하기 편리한 방법입니다. 유니티에서 구현된 다이나믹 폰트는 UI Text 컴포넌트에서 사용되는 글자를 기반으로 텍스트 아틀라스를 런타임에서 제작합니다.
로드된 각 개별 폰트 오브젝트는 다른 폰트와 동일한 폰트 집합(font family) 내에 있더라도, 자신의 텍스쳐 아틀라스를 관리합니다. 예를 들어, 한 UI 컨트롤에서 굵은 문자(bolded text)가 적용된 Arial 폰트를 사용하고, 다른 UI 컨트롤에서는 Arial Bold 폰트를 사용하는 경우, 출력되는 결과는 동일하지만 유니티는 Arial과 Arial Bold의 두 가지 텍스쳐를 구분해서 관리합니다.
성능 측면에서볼 때 이해해야하는 가장 중요한 것은, 유니티 UI의 다이나믹 폰트는 폰트 텍스쳐에서 글자의 스타일, 크기, 특징에 따라서 글자를 관리한다는 점입니다. 즉, 두 개의 Text 컴포넌트를 가진 UI에서 모두 글자 ‘A’를 표시하는 경우 다음과 같이 동작합니다:
- 두 Text 컴포넌트가 동일한 크기를 공유하면 폰트 아틀라스에는 하나의 글자만 생성됩니다.
- 두 Text 컴포넌트가 동일한 크기를 공유하지 않으면(예: 하나는 16 포인트 크기, 다른 하나는 24 포인트 크기), 폰트 아틀라스에는 크기가 다른 글자 ’A’의 사본이 두 개 포함됩니다.
- 한 Text 컴포넌트는 굷은 글꼴을 사용하고 다른 컴포넌트는 굵은 글꼴을 사용하지 않는 경우, 폰트 아틀라스에는 굵은 글꼴의 글자 ‘A’와 일반 글꼴의 글자 ‘A’가 포함됩니다.
다이나믹 폰트를 사용하는 UI Text 오브젝트에서 폰트 텍스쳐 아틀라스에 아직 저장되어있지 않은 글자가 입력되면, 폰트 텍스쳐 아틀라스를 다시 빌드해야합니다. 다행히 새로 추가되는 글자가 현재 아틀라스의 크기에 맞으면, 해당 글자는 아틀라스에 추가된 다음 그래픽 장치에 다시 업로드됩니다. 하지만, 현재 사용하는 아틀라스의 크기가 작아서 추가되는 글자를 담지 못하는 경우, 시스템에서는 아틀라스를 다시 제작합니다. 이 과정은 두 단계로 처리됩니다.
먼저 해당 아틀라스는 현재 활성화된 UI Text(1) 컴포넌트에 의해서 화면에 표시되는 글자를 사용해서 동일한 크기로 다시 빌드됩니다. 시스템에서 현재 사용중인 모든 글자를 새 아틀라스 크기에 맞추는데 성공한 경우, 이 아틀라스를 래스터화하고(rasterized) 두 번째 단계는 수행하지 않습니다.
둘째, 사용 중인 글자 세트가 현재 아틀라스와 동일한 크기를 갖는 아틀라스 크기에 맞지 않는 경우, 아틀라스의 가로, 세로 크기 중 짧은 크기를 두배로 늘려서 아틀라스를 새로 생성합니다. 예를 들어, 512×512 아틀라스의 경우, 512×1024 크기의 아틀라스로 확장됩니다.
위의 알고리즘으로 인해서, 다이나믹 폰트의 아틀라스는 아틀라스가 생성될 때만 그 크기가 증가할 수 있습니다. 텍스쳐 아틀라스를 다시 빌드하는 비용을 감안하면, 다시 빌드하는 과정을 최소화하는 것이 좋습니다. 이는 다음의 두 가지 방식으로 구현가능합니다.
가능하다면, 다이나믹 폰트를 사용하지 않고, 사전에 필요한 글자들을 모두 넣어둔 아틀라스를 제작해서 사용하는 것이 좋습니다. 이 방법은 일반적으로, 글자 크기가 작고, Latin/ASCII와 같이 제한된 글자 집합을 사용하는 UI에서 잘 동작합니다.
유니 코드 세트와 같이 범위가 매우 넓은 글자를 지원해야하는 경우에는 다이나믹 폰트를 사용해야 합니다. 살펴본대로 예측가능한 성능 문제를 피하기 위해서는 시작할 때 Font.RequestCharactersInTexture를 통해서 폰트 아틀라스를 적합한 글자 세트로 설정해서 준비하는 것이 좋습니다.
폰트 아틀라스 리빌드(rebuild)는 각 UI Text 컴포넌트가 변경될 때마다 개별적으로 실행된다는 점에 주의해야합니다. 다수의 Text 컴포넌트의 내용을 채울때, Text 컴포넌트의 내용에서 고유한 글자들을 모아서 폰트 아틀라스에 미리 저장해두는 것이 좋습니다. 이렇게하면 이 아틀라스는 새로운 글자가 작성될 때마다 리빌드되는 대신, 처음 한번만 리빌드하면 됩니다.
또한, 폰트 아틀라스 리빌드가 실행될때, 활성화된 UI Text 컴포넌트에 현재 포함되지 않은 글자는 새 아틀라스에 포함되지 않는다는 점을 주의해야합니다. 처음에 Font.RequestCharactersInTexture를 통해서 아틀라스에 추가된 글자라 하더라도 포함되지 않습니다. 이런 제약을 피하려면, Font.textureRebuilt 딜리게이트(Delegate)에 등록하고, Font.characterInfo를 쿼리(query)해서 원하는 모든 문자가 아틀라스에 준비되어 있는지 확인해야 합니다.
Font.textureRebuilt 딜리게이트는 현재 문서화 되어있지 않습니다. 이 딜리게이트는 파라미터 하나를 취하는 유니티 이벤트 입니다. 리빌드 처리된 폰트를 파라미터로 전달합니다. 이 이벤트에 등록하려면 다음을 따라야 합니다.
public void TextureRebuiltCallback(Font rebuiltFont) { /* … */ }
특수화된 Glyph 렌더러 (Specialized glyph renderers)
사용되는 글자를 잘 알고있고, 각 글자 간에 상대적으로 고정된 위치를 사용하는 경우, 해당 글자를 화면에 나타내기위한 스프라이트를 위한 커스텀 컴포넌트를 작성하는 것이 좋습니다. 점수를 표시하는 화면이 이 경우의 예가될 수 있습니다.
점수의 경우, 화면에 표시되는 문자는, 잘 알고있는 글자 세트(숫자 0~9)에서 가져오고, 언어에 따라서 변경되지 않으며, 각 자리마다 고정된 위치에 표시됩니다. 정수를 각 자릿수로 나눠서 적절하게 표시하는 것은 비교적 쉬운 작업입니다. 이런 종류의 특수화된 숫자-표시 시스템은 캔버스 기반의 UI Text 컴포넌트 보다 상당히 계산에 빠르고 할당량이 적은 방식으로 제작할 수 있습니다.
대체 폰트 및 메모리 사용량 (Fallback fonts and memory usage)
아주 많은 수의 글자-세트를 지원해야하는 프로그램의 경우, 폰트 임포터의 “Font Names” 필드에 다수의 폰트를 나열하는 경우가 많습니다. “Font Names”에 나열된 폰트는 모두 기본 폰트에 특정 글자를 위치시킬 수 없는 경우, 대체 폰트(폴백 폰트)로 사용됩니다. 대체 폰트의 순서는 “Font Names”필드에 나열된 순서에 따라서 결정됩니다.
하지만, 이 동작을 지원하기 위해서 유니티는 “Font Names” 필드에 나열된 모든 폰트를 메모리에 로드합니다. 특정 폰트의 글자-세트가 매우 큰 경우, 대체 폰트에 의해서 차지되는 메모리의 양이 과도하게 커질 수 있습니다. 이는 일본 한자 또는 중국어 글자 등 그림 문자 폰트를 포함시키는 경우에 자주볼 수 있습니다.
Best Fit기능과 성능 (Best Fit and performance)
일반적으로, UI Text 컴포넌트의 Best Fit 설정은 사용하지 않는 것이 좋습니다.
“Best Fit”은 설정된 최소/최대 크기로 고정된 상태에서 텍스트 컴포넌트의 바운딩 박스(Bounding Box)에서 오버플로우가 발생하지 않는 최대 정수 크기로 폰트 크기를 동적으로 조절합니다. 하지만, 유니티는 화면에 표시되는 고유한 크기의 글자마다 아틀라스에 저장하기 때문에, Best Fit을 사용하면 다른 크기의 글자를 저장하기 위해서 아틀라스의 수가 매우 급격하게 증가할 수 있습니다.
유니티 5.3까지, Best Fit에서 사용되는 크기 감지 기능은 최적화된 기능이 아닙니다. 글자 크기를 증가시키면서 테스트를 할 때마다 아틀라스에 글자를 생성하기 때문에, 이로 인해서 폰트 아틀라스를 생성하기 위한 시간이 더 필요하게 됩니다. 또한 아틀라스 오버플로우로 인해서, 사용한지 오래된 글자가 아틀라스에서 제외되는 경우가 발생합니다. Best Fit을 계산하는데 필요한 다수의 테스트로 인해서, 적절한 폰트 크기가 계산된 후에 Text 컴포넌트에서 사용중인 글자를 아틀라스에서 제거하고 해당 아틀라스를 적어도 한번 재생성하도록 설계되어 있습니다. 이 문제는 유니티 5.4에서 수정되었고, Best Fit 동작은 불필요하게 폰트의 텍스쳐 아틀라스를 확장하지 않습니다. 하지만, 고정 크기의 폰트를 사용하는 것보다 속도면에서 상당히 느립니다.
폰트 아틀라스가 너무 빈번하게 리빌드(rebuild)되면, 런타임 성능을 급격하게 저하시킬뿐만 아니라 메모리 파편화(fragmentation)를 유발할 수 있습니다. Best Fit으로 설정된 텍스트 컴포넌트의 수가 많을수록, 이 문제는 더욱 심각해 집니다.
스크롤 뷰 (Scroll Views)
유니티 UI의 스크롤 뷰는 런타임 성능 문제를 일으키는 원인 중 fill-rate 문제 다음으로 두번째로 흔한 원인입니다. 스크롤 뷰는 일반적으로 스크롤 뷰의 콘텐츠를 보여주기 위해서 상당한 수의 UI 요소를 필요로합니다. 스크롤 뷰를 채우는 두 가지 기본 방식은 다음과 같습니다:
- 스크롤 뷰의 콘텐츠를 표시하기 위해서 필요한 모든 UI 요소를 스크롤 뷰에 채웁니다.
- UI 요소를 풀링(pooling) 합니다. UI 요소를 화면에 표시하기 위해서 필요에 따라 위치를 변경합니다.
이 두가지 방법은 모두 문제를 가지고 있습니다.
첫번째 방법은 표시해야하는 아이템의 수가 증가하면 이를 위한 UI 요소를 생성하는데 필요한 시간이 증가하고, 스크롤 뷰를 리빌드(rebuild)하는데 필요한 시간 역시 증가하게 됩니다. 적은 수의 Text 컴포넌트를 표시하는 경우와 같이, 스크롤 뷰에서 화면에 표시해야하는 UI 요소의 수가 적은 경우에는, 이 방법이 간단하기 때문에 사용하기에 좋습니다.
두번째 방법은 현재 UI 및 레이아웃 시스템에서 적절하게 구현하기 위해서 상당한 양의 코드가 필요합니다. 이를 구현하기 위해서 가능한 두 가지 방법에 대해서 뒤에 더 자세히 살펴보겠습니다. 상당히 복잡하게 스크롤되는 UI의 경우, 성능 문제를 해결하기 위해서 일반적으로 일종의 풀링(pooling)방식의 접근이 필요합니다.
이러한 이슈에도 불구하고, 두 가지 접근 방법 모두 RectMask2D 컴포넌트를 스크롤 뷰에 추가해서 성능 향상을 시킬 수 있습니다. 이 컴포넌트(RectMask2D)는 스크롤 뷰의 뷰포트 바깥에 있는 UI 요소가 Drawable 목록에 포함되지 않도록 합니다. Drawable 목록에 포함된 UI 요소는 캔버스를 리빌드할 때 해당 UI 요소의 UI 메쉬를 생성한 다음, 정렬 및 분석이 필요하기 때문에 성능에 부담을 줄 수 있습니다.
스크롤 뷰 항목의 단순 풀링 (Simple Scroll View element pooling)
유니티에 내장된 Scroll View 컴포넌트를 사용하는 편리함을 그대로 유지하면서 스크롤 뷰를 사용해서 오브젝트 풀링을 구현하는 가장 간단한 방법은 하이브리드(hybrid) 접근 방식을 사용하는 것입니다:
레이아웃 시스템에서 스크롤 뷰 콘텐츠의 크기를 적절하게 계산할 수 있도록 하고, 스크롤 바가 제대로 기능할 수 있도록 UI 요소를 유니티 UI에 배치하려면, Layout Element 컴포넌트를 추가한 게임오브젝트를 화면에 보이는 UI 요소의 “placeholders”로 사용합니다.
그런 다음, Scroll View의 보이는 영역을 채우기 위해서 충분한 양의 UI 요소 풀(pool)을 생성하고 이를 placeholders의 부모로 지정합니다. 스크롤 뷰가 스크롤 되면, UI 요소를 재사용해서 스크롤 뷰의 내용을 표시합니다.
이 방법은 배치(batch)처리해야하는 UI 요소의 수를 줄여줍니다. 배치처리하는데 드는 비용은 Canvas 안의 Canvas Renderer의 수에 의해서만 증가하고, RectTransform의 수에는 영향을 받지 않습니다.
간단한 접근방법의 문제점 (Problems with the simple approach)
현재 UI 요소의 계층구조가 변경되거나 하위 요소의 순서가 변경될 때마다 해당 UI 요소와 하위 요소는 모두 “dirty”로 표시되어 이 요소들이 포함된 캔버스의 리빌드를 강요하게 됩니다.
이렇게 동작하는 이유는 유니티에서 트랜스폼의 계층을 변경하는 콜백과 하위 계층의 순서를 변경하는 콜백을 서로 분리하지 않았기 때문입니다. 이 두 이벤트는 모두 OnTransformParentChanged 콜백을 호출합니다. 유니티 UI의 Graphic 클래스(Grahpic.cs 소스 참고)의 소스에 OnTransformParentChanged 콜백이 구현되어 있고 이 콜백은 SetAllDirty 메소드를 호출합니다. Graphic 컴포넌트를 dirty 상태로 표시하면, 다음 프레임이 렌더링되기 전에, 시스템에서 해당 Graphic 컴포넌트의 레이아웃 및 정점을 리빌드 합니다.
Scroll View를 구성하는 각 UI 요소의 루트 RectTransform에 캔버스(Canvas)를 할당하면, 스크롤 뷰 전체를 리빌드하지 않고, 계층 구조가 변경된 UI 요소만 리빌드하도록 만들 수 있습니다. 하지만, 이 방법은 스크롤 뷰를 렌더링하는데 필요한 드로우 콜의 수를 증가시키는 경향이 있습니다. 또한, 스크롤 뷰를 구성하는 개별 UI 요소들이 복잡하고 다수의(10개 이상) Graphic 컴포넌트를 포함하는 경우(특히, 각 UI 요소마다 다 수의 Layout 컴포넌트를 포함하는 경우), 이 UI 요소를 리빌드하는 비용이 상당히 크기 때문에 저사양 장치에서 프레임 속도를 현저하게 떨어뜨리는 원인이 될 수 있습니다.
Scroll View UI 요소의 크기가 변경되지 않는 경우에는 레이아웃 및 정점을 전체적으로 다시 계산할 필요가 없습니다. 하지만, 위에서 설명한 문제를 피하려면, 부모 계층 또는 하위 계층의 순서를 변경하는 방법 대신 위치 변경을 기반으로 하는 오브젝트 풀링을 구현해야 합니다.
위치 기반 스크롤 뷰 풀링 (Position-based Scroll View pools)
위에서 설명한 문제를 해결하기 위한 방법으로, 스크롤 뷰에 포함되는 UI의 RectTransform을 이동시키는 풀링 시스템을 사용하는 스크롤 뷰를 만들 수 있습니다. 이 방법을 사용하면, UI 요소의 크기가 변경이 되지 않는한, 이동 처리된 RectTransform의 리빌드가 필요하지 않기 때문에 스크롤 뷰의 성능이 크게 향상될 수 있습니다.
이를 구현하기 위해서는, 일반적으로 Scroll View의 하위 클래스를 작성하거나 커스텀 Layout Group 컴포넌트를 작성하는 것이 가장 좋은 방법입니다. 두 번째 방법이 일반적으로 더 간단한 방법이며, 유니티 UI의 LayoutGroup 추상 기본 클래스의 하위 클래스를 구현해서 커스텀 Layout Group 컴포넌트를 작성할 수 있습니다.
커스텀 Layout Group 컴포넌트는 기본 소스 데이터를 분석해서 화면에 표시해야하는 UI 요소의 수를 검사하고 스크롤 뷰 콘텐츠의 RectTransform 크기를 적절하게 조절할 수 있습니다. 그런 다음 Scroll View Change 이벤트에 등록하고 화면에 표시할 UI 요소의 위치를 재조정하는데 이 정보를 사용할 수 있습니다.
각주 (Footnotes)
- 부모 캔버스(Canvas)가 활성화 상태지만, Canvas Renderer가 비활성화된 UI Text 컴포넌트가 포함됩니다.
내용 끝까지 읽어주셔서 감사합니다.
배너 클릭은 저에게 많은 힘이 됩니다.
감사합니다 🙂