다양한 실험, 실용적인 조언, 성능 테스트를 바탕으로 한 자세한 가이드를 통해 Unity에서 UI 성능을 최적화하는 방법을 알아보세요! 안녕하세요! 저는 Pixonic(MY.GAMES)의 클라이언트 개발자 Sergey Begichev입니다. 이 게시물에서는 Unity3D의 UI 최적화에 대해 논의하겠습니다. 텍스처 세트를 렌더링하는 것은 간단해 보일 수 있지만 상당한 성능 문제로 이어질 수 있습니다. 예를 들어, War Robots 프로젝트에서 최적화되지 않은 UI 버전은 총 CPU 부하의 최대 30%를 차지했습니다. 놀라운 수치입니다! 일반적으로 이 문제는 두 가지 조건에서 발생합니다. 하나는 동적 객체가 많을 때이고, 두 번째는 디자이너가 다양한 해상도에서 안정적인 크기 조정을 우선시하는 레이아웃을 만들 때입니다. 이러한 상황에서는 작은 UI조차도 눈에 띄는 부하를 생성할 수 있습니다. 이것이 어떻게 작동하는지 살펴보고, 부하의 원인을 식별하고, 잠재적 해결책을 논의해 보겠습니다. 유니티의 추천사항 먼저 검토해 보겠습니다. UI 최적화를 위해 6가지 핵심 포인트로 요약했습니다. 유니티의 추천사항 캔버스를 하위 캔버스로 분할하세요 불필요한 Raycast 대상 제거 비용이 많이 드는 요소(큰 목록, 그리드 보기 등) 사용을 피하세요. 레이아웃 그룹을 피하세요 게임 객체 대신 캔버스 숨기기(GO) 애니메이터를 최적으로 활용하세요 2번과 3번은 직관적으로 명확하지만 나머지 권장 사항은 실제로 상상하기 어려울 수 있습니다. 예를 들어, "캔버스를 하위 캔버스로 분할"하라는 조언은 확실히 가치가 있지만 Unity는 이 구분의 원칙에 대한 명확한 지침을 제공하지 않습니다. 제 개인적인 입장에서 말씀드리자면, 실제적으로 하위 캔버스를 구현하는 것이 가장 합리적인 곳을 알고 싶습니다. "레이아웃 그룹을 피하라"는 조언을 고려하세요. 레이아웃 그룹은 높은 UI 부하에 기여할 수 있지만, 많은 대형 UI에는 여러 레이아웃 그룹이 있으며, 모든 것을 다시 작업하는 데 시간이 많이 걸릴 수 있습니다. 게다가 레이아웃 그룹을 피하는 레이아웃 디자이너는 작업에 훨씬 더 많은 시간을 할애하게 될 수 있습니다. 따라서 이러한 그룹을 피해야 할 때, 그룹이 유익할 수 있는 때, 그룹을 제거할 수 없는 경우 취해야 할 조치를 이해하는 것이 도움이 될 것입니다. 유니티의 권장 사항에 대한 이러한 모호성은 핵심 문제입니다. 이러한 제안에 어떤 원칙을 적용해야 할지 종종 불분명하기 때문입니다. UI 구성 원칙 UI 성능을 최적화하려면 Unity가 UI를 구성하는 방식을 이해하는 것이 필수적입니다. 이러한 단계를 이해하는 것은 Unity에서 효과적인 UI 최적화에 필수적입니다. 이 프로세스에서 세 가지 핵심 단계를 광범위하게 식별할 수 있습니다. . 처음에 Unity는 모든 UI 요소를 크기와 지정된 위치에 따라 배열합니다. 이러한 위치는 화면 가장자리와 다른 요소와 관련하여 계산되어 종속성 체인을 형성합니다. 레이아웃 . 다음으로 Unity는 더 효율적인 렌더링을 위해 개별 요소를 배치로 그룹화합니다. 하나의 큰 요소를 그리는 것은 항상 여러 개의 작은 요소를 렌더링하는 것보다 효율적입니다. (배칭에 대한 자세한 내용은 다음을 참조하세요. ) 배칭 이 기사. . 마지막으로 Unity는 수집된 배치를 그립니다. 배치가 적을수록 렌더링 프로세스가 더 빨라집니다. 렌더링 이 과정에는 다른 요소도 있지만, 이 세 단계가 대부분의 문제를 설명하므로 지금은 이 세 단계 중 하나에 집중하겠습니다. 이상적으로는 UI가 정적일 때, 즉 아무것도 움직이거나 변경되지 않을 때 레이아웃을 한 번 빌드하고, 대규모 배치를 하나 만들고, 효율적으로 렌더링할 수 있습니다. 그러나 하나의 요소의 위치라도 수정하면 해당 요소의 위치를 다시 계산하고 영향을 받는 배치를 다시 빌드해야 합니다. 다른 요소가 이 위치에 의존하는 경우 해당 요소의 위치도 다시 계산해야 하므로 계층 구조 전체에 연쇄 효과가 발생합니다. 조정이 필요한 요소가 많을수록 배치 로드가 높아집니다. 따라서 레이아웃의 변경은 전체 UI에 파장 효과를 일으킬 수 있으며, 우리의 목표는 변경 횟수를 최소화하는 것입니다. (또는 체인 반응을 방지하기 위해 변경 사항을 격리하는 것을 목표로 할 수 있습니다.) 실제적인 예로, 이 문제는 레이아웃 그룹을 사용할 때 특히 두드러집니다. 레이아웃이 다시 빌드될 때마다 모든 LayoutElement가 GetComponent 작업을 수행하는데, 이는 매우 리소스 집약적일 수 있습니다. 다중 테스트 성능 결과를 비교하기 위해 일련의 예를 살펴보겠습니다. (모든 테스트는 Google Pixel 1 기기에서 Unity 버전 2022.3.24f1을 사용하여 수행되었습니다.) 이 테스트에서는 단일 요소를 특징으로 하는 레이아웃 그룹을 만들고 두 가지 시나리오를 분석하겠습니다. 하나는 요소의 크기를 변경하는 경우이고, 다른 하나는 FillAmount 속성을 활용하는 경우입니다. RectTransform 변경 사항: FlllAmount 변경 사항: 두 번째 예에서는 같은 작업을 시도하지만, 8개의 요소가 있는 레이아웃 그룹에서 시도합니다. 이 경우, 우리는 여전히 하나의 요소만 변경합니다. RectTransform 변경 사항: FlllAmount 변경 사항: 이전 예에서 RectTransform의 변경으로 인해 레이아웃에 0.2ms의 부하가 발생했다면 이번에는 부하가 0.7ms로 증가합니다. 마찬가지로 일괄 업데이트로 인한 부하는 0.65ms에서 1.10ms로 증가합니다. 아직 하나의 요소만 수정하고 있지만, 레이아웃의 크기가 커지면서 리빌드 중의 부하가 상당히 영향을 받습니다. 반면에 요소의 FillAmount를 조정하면 요소의 수가 많아도 부하가 증가하지 않습니다. 이는 FillAmount를 수정해도 레이아웃 재구축이 트리거되지 않아 배치 업데이트 부하가 약간만 증가하기 때문입니다. 분명히, 이 시나리오에서는 FillAmount를 사용하는 것이 더 효율적인 선택입니다. 그러나 요소의 크기나 위치를 변경하면 상황이 더 복잡해집니다. 이러한 경우 레이아웃 재구축을 트리거하지 않는 Unity의 기본 제공 메커니즘을 대체하는 것은 어렵습니다. 여기서 SubCanvases가 등장합니다. SubCanvas 내에 변경 가능한 요소를 캡슐화했을 때의 결과를 살펴보겠습니다. 8개의 요소로 구성된 레이아웃 그룹을 만들고, 그 중 하나는 SubCanvas에 보관한 다음 해당 그룹의 변형을 수정합니다. SubCanvas의 RectTransform 변경 사항: 결과가 보여주듯이, SubCanvas 내에 단일 요소를 캡슐화하면 레이아웃의 부하가 거의 없어집니다. SubCanvas가 모든 변경 사항을 격리하여 계층의 상위 수준에서 다시 빌드할 필요가 없기 때문입니다. 하지만 캔버스 내부의 변경 사항은 캔버스 외부의 요소 위치에 영향을 미치지 않는다는 점에 유의하는 것이 중요합니다. 따라서 요소를 너무 많이 확장하면 인접한 요소와 겹칠 위험이 있습니다. 8개의 레이아웃 요소를 SubCanvas에 래핑하여 진행해 보겠습니다. 이전 예는 레이아웃의 부하가 낮은 반면, 배치 업데이트가 두 배로 증가했음을 보여줍니다. 즉, 요소를 여러 개의 SubCanvases로 나누면 레이아웃 빌드의 부하를 줄이는 데 도움이 되지만 배치 어셈블리의 부하가 증가합니다. 결과적으로 전체적으로 순 부정적 효과가 발생할 수 있습니다. 이제 또 다른 실험을 해봅시다. 먼저 8개의 요소로 레이아웃 그룹을 만든 다음 애니메이터를 사용하여 레이아웃 요소 중 하나를 수정합니다. 애니메이터는 RectTransform을 새 값으로 조정합니다. 여기서 우리는 모든 것을 수동으로 변경한 두 번째 예와 동일한 결과를 봅니다. 이는 RectTransform을 변경하는 데 무엇을 사용하든 아무런 차이가 없기 때문에 논리적입니다. 애니메이터는 RectTransform을 비슷한 값으로 변경합니다. 애니메이터는 이전에 값이 변경되지 않았더라도 매 프레임마다 동일한 값을 계속 덮어쓰는 문제에 직면했습니다. 이는 실수로 레이아웃 재구축을 트리거했습니다. 다행히도 최신 버전의 Unity는 이 문제를 해결하여 대체 버전으로 전환할 필요가 없습니다. 성능 개선을 위한 방법만 사용합니다. 트위닝 이제 8개의 요소가 있는 레이아웃 그룹 내에서 텍스트 값을 변경하면 어떻게 동작하는지, 그리고 레이아웃 재구성이 트리거되는지 살펴보겠습니다. 우리는 또한 재구축이 시작되는 것을 봅니다. 이제 8개 요소의 레이아웃 그룹에서 TextMechPro의 값을 변경해 보겠습니다. TextMechPro는 레이아웃을 다시 빌드하고, 일반 Text보다 배칭과 렌더링에 더 많은 부하를 주는 것처럼 보입니다. 8개 요소의 레이아웃 그룹에서 SubCanvas의 TextMechPro 값 변경: SubCanvas는 효과적으로 변경 사항을 분리하여 레이아웃 재구축을 방지했습니다. 그러나 일괄 업데이트에 대한 부하가 감소했지만 여전히 비교적 높습니다. 이는 각 문자가 별도의 텍스처로 처리되므로 텍스트로 작업할 때 문제가 됩니다. 따라서 텍스트를 수정하면 여러 텍스처에 영향을 미칩니다. 이제 레이아웃 그룹 내에서 GameObject(GO)를 켜고 끌 때 발생하는 부하를 평가해 보겠습니다. 8개 요소의 레이아웃 그룹 내에서 GameObject를 켜고 끄기: 보시다시피 GO를 켜거나 끄면 레이아웃이 다시 빌드됩니다. 8개 요소의 레이아웃 그룹이 있는 SubCanvas 내부에서 GO 켜기: 이 경우에도 SubCanvas가 부하를 덜어주는 데 도움이 됩니다. 이제 레이아웃 그룹으로 전체 GO를 켜거나 끄면 부하가 어떻게 되는지 확인해 보겠습니다. 결과에 따르면, 부하가 지금까지 가장 높은 수준에 도달했습니다. 루트 요소를 활성화하면 자식 요소에 대한 레이아웃 재구축이 트리거되고, 이는 배칭과 렌더링 모두에 상당한 부하를 초래합니다. 그렇다면 과도한 부하를 생성하지 않고 전체 UI 요소를 활성화하거나 비활성화해야 하는 경우 어떻게 해야 할까요? GO 자체를 활성화하거나 비활성화하는 대신 Canvas 또는 Canvas Group 구성 요소를 비활성화하면 됩니다. 또한 Canvas Group의 알파 채널을 0으로 설정하면 성능 문제를 피하면서 동일한 효과를 얻을 수 있습니다. Canvas Group 구성 요소를 비활성화하면 로드에 어떤 일이 발생하는지 알려드리겠습니다. GO는 캔버스가 비활성화된 상태에서도 활성화되어 있으므로 레이아웃은 유지되지만 표시되지 않습니다. 이 접근 방식은 레이아웃 로드를 낮출 뿐만 아니라 배칭 및 렌더링 로드를 크게 줄입니다. 다음으로, 레이아웃 그룹 내에서 SiblingIndex를 변경하는 데 따른 영향을 살펴보겠습니다. 8개 요소의 레이아웃 그룹 내에서 SiblingIndex 변경: 관찰한 대로, 레이아웃을 업데이트하는 데 0.7ms로 로드가 여전히 상당합니다. 이는 SiblingIndex에 대한 수정도 레이아웃 재구축을 트리거한다는 것을 분명히 나타냅니다. 이제 다른 접근 방식을 실험해 보겠습니다. SiblingIndex를 변경하는 대신 레이아웃 그룹 내의 두 요소의 텍스처를 바꿔 보겠습니다. 8개 요소의 레이아웃 그룹에서 두 요소의 텍스처 바꾸기: 보시다시피, 상황은 개선되지 않았습니다. 사실, 더 악화되었습니다. 텍스처를 교체하면 재구축도 트리거됩니다. 이제 사용자 지정 레이아웃 그룹을 만들어 보겠습니다. 8개의 요소를 구성하고 그 중 두 개의 위치를 간단히 바꿉니다. 8개의 요소가 있는 사용자 정의 레이아웃 그룹: 실제로 부하가 상당히 감소했습니다. 이는 예상된 일입니다. 이 예에서 스크립트는 단순히 두 요소의 위치를 바꿔서 무거운 GetComponent 작업과 모든 요소의 위치를 다시 계산할 필요성을 제거합니다. 결과적으로 배칭에 필요한 업데이트가 줄어듭니다. 이 방법이 만병통치약처럼 보이지만 스크립트에서 계산을 수행하면 전체 부하에도 영향을 미친다는 점에 유의하는 것이 중요합니다. 레이아웃 그룹에 더 많은 복잡성을 도입함에 따라 부하는 불가피하게 증가하지만, 계산이 스크립트에서 이루어지기 때문에 레이아웃 섹션에 반드시 반영되지는 않습니다. 따라서 코드의 효율성을 직접 모니터링하는 것이 중요합니다. 그러나 간단한 레이아웃 그룹의 경우 사용자 정의 솔루션이 훌륭한 옵션이 될 수 있습니다. 결론 레이아웃을 재구축하는 것은 상당한 과제입니다. 이 문제를 해결하려면 근본 원인을 파악해야 하는데, 근본 원인은 다양할 수 있습니다. 레이아웃 재구축으로 이어지는 주요 요인은 다음과 같습니다. 요소의 애니메이션: 이동, 크기 조절, 회전(변환의 모든 변경) 스프라이트 교체 텍스트 다시 쓰기 GO 켜기/끄기, GO 추가/제거 형제 인덱스 변경 새로운 버전의 Unity에서는 더 이상 문제가 되지 않지만 이전 버전에서는 문제가 되었던 몇 가지 측면을 강조하는 것이 중요합니다. 즉, 같은 텍스트를 덮어쓰고 애니메이터로 같은 값을 반복해서 설정하는 것입니다. 이제 레이아웃 재구축을 유발하는 요소를 파악했으므로 솔루션 옵션을 요약해 보겠습니다. 이 접근 방식은 변경 사항을 격리하여 계층 구조의 다른 요소에 영향을 미치지 않도록 합니다. 그러나 조심하세요. SubCanvas가 너무 많으면 배칭 부하가 상당히 증가할 수 있습니다. SubCanvas에서 재구축을 트리거하는 GameObject(GO)를 래핑합니다. 새 GO를 만드는 대신 객체 풀을 사용합니다. 이 방법은 레이아웃을 메모리에 보존하여 다시 빌드할 필요 없이 요소를 빠르게 활성화할 수 있습니다. GO 대신 SubCanvas 또는 Canvas Group을 켜고 끕니다. 셰이더를 사용하여 텍스처를 변경해도 레이아웃 재구축이 트리거되지 않습니다. 그러나 텍스처가 다른 요소와 겹칠 수 있다는 점을 명심하세요. 이 방법은 SubCanvases를 사용하는 것과 비슷한 목적을 효과적으로 제공하지만 셰이더를 작성해야 합니다. 셰이더 애니메이션을 활용합니다. Unity의 레이아웃 그룹의 주요 문제 중 하나는 각 LayoutElement가 재구축 중에 GetComponent를 호출한다는 것입니다. 이는 리소스를 많이 사용합니다. 사용자 지정 레이아웃 그룹을 만들면 이 문제를 해결할 수 있지만 고유한 과제가 있습니다. 사용자 지정 구성 요소에는 효과적인 사용을 위해 이해해야 하는 특정 운영 요구 사항이 있을 수 있습니다. 그럼에도 불구하고 이 방법은 특히 간단한 레이아웃 그룹 시나리오에서 더 효율적일 수 있습니다. Unity의 레이아웃 그룹을 사용자 지정 레이아웃 그룹으로 대체합니다.