무엇이 다시 계산되는지가 네 가지로 나뉜다
Esc

    무엇이 다시 계산되는지가 네 가지로 나뉜다

    –UnityUIUI Toolkit최적화

    내가 만든 요소도 UXML 에 태그로 들어간다까지가 만드는 이야기였습니다. 이제 돌아가는 이야기입니다.

    한 칸이 바뀌면 그 캔버스는 통째로 다시 계산된다에서 uGUI 의 비용 구조를 봤습니다. 캔버스 안의 한 칸이 바뀌면 그 캔버스 전체가 다시 묶였습니다. 단위가 캔버스 하나였고, 그래서 캔버스를 어떻게 자르느냐가 최적화의 대부분이었습니다.

    UI Toolkit 은 단위가 다릅니다. 무엇이 다시 도는지가 넷으로 갈려 있습니다.

    다시 도는 것이 넷입니다

    문서가 추적되는 연산을 넷으로 적어 뒀습니다.

    단계 하는 일 언제
    스타일 해석 “Determines the final appearance of elements by applying USS selectors and styles” 클래스나 스타일이 바뀔 때
    레이아웃 재계산 “Adjusts the size and position of elements to fit correctly within the UI hierarchy” 크기, 위치, 정렬이 바뀔 때
    정점 버퍼 갱신 “Updates geometric shapes used to render UI elements” 모서리 둥글리기처럼 기하가 바뀔 때
    렌더 상태 변경 “Changes rendering states like textures and blending modes” 마스킹이나 고유 텍스처

    넷이 늘 같이 돌지 않습니다. 무엇을 바꿨느냐에 따라 어디까지 번지는지가 달라집니다. 이게 uGUI 와 제일 크게 갈리는 지점입니다.

    다시 도는 단위가 다릅니다 uGUI · 칸 하나를 바꿨을 때 바뀐 칸 안 바뀐 칸 안 바뀐 칸 안 바뀐 칸 안 바뀐 칸 안 바뀐 칸 캔버스가 통째로 다시 묶입니다 UI Toolkit · width 를 바꿨을 때 스타일 해석 안 돕니다 레이아웃 재계산 돕니다 정점 버퍼 갱신 돕니다 렌더 상태 변경 안 돕니다 바꾼 값이 닿는 단계만 돕니다
    uGUI 는 캔버스를 어떻게 자르냐가 문제였고, 여기서는 무엇을 바꾸냐가 문제입니다.

    표에 없는 것 둘만 더 적어 둡니다. 모서리를 둥글리는 border-radius 는 문서가 직접 든 정점 버퍼 갱신의 예시 입니다. 그리고 마스크를 켜고 끄면 렌더 상태가 바뀝니다. 그쪽 비용은 다음 편에서 봅니다.

    여기서 실무 규칙 하나가 바로 나옵니다. 크기를 건드리는 값만 형제와 부모까지 번집니다. translate 나 색은 그 요소에서 끝나는데, width 나 margin 은 배치가 다시 잡히면서 옆 칸들도 같이 움직입니다. 움직임은 클립이 아니라 두 값 사이에서 만들어진다에서 transform 계열에 전환을 걸라고 한 이유가 이것입니다.

    숨기는 방법 셋을 비용으로 다시 봅니다

    칸은 남는 자리를 나눠 갖는 방식으로 정해진다에서 display: none 과 visibility: hidden 을 자리 기준 으로 갈랐습니다. 칸이 없어지느냐 남느냐였습니다. 비용 기준으로 보면 셋이 이렇게 갈립니다.

    방법 문서 표현 비용
    opacity: 0 “Still renders them as transparent” 계속 그립니다. 투명하게 그릴 뿐입니다
    visibility: hidden “Prevents rendering but keeps it as part of the layout” 안 그리지만 배치에는 남아 있습니다
    display: none “Stops rendering and layout updates entirely” 둘 다 멈춥니다

    display: none 이 제일 싸 보이는데 따라오는 것이 있습니다. 다시 켤 때 레이아웃 재계산이 옵니다. 칸이 없어졌다가 생기니 배치를 다시 잡아야 합니다.

    그래서 고르는 기준이 이렇게 됩니다.

    • 오래 안 보일 것, 목록에서 빠질 것이면 display: none
    • 자주 껐다 켤 것이고 자리가 고정이면 visibility: hidden 쪽이 다시 켤 때 싸게 붙습니다
    • opacity: 0 은 숨기는 용도가 아닙니다. 페이드처럼 값이 이어져야 할 때만 씁니다. 안 보이는데 계속 그리고 있습니다

    마지막 줄이 14편에서 본 uGUI 쪽 결론과 정확히 반대라는 점이 재미있습니다. uGUI 에서는 CanvasGroup.alpha = 0 이 제일 싼 축이었습니다. 체계가 다르면 같은 방법의 값이 달라집니다.

    자주 움직이는 것에는 표식을 답니다

    위에서 translate 하나가 정점 버퍼를 다시 만들게 한다고 했습니다. 그걸 막는 장치가 따로 있습니다. usageHints 입니다.

    <ui:VisualElement usage-hints="DynamicTransform" />
    visualElement.usageHints = UsageHints.DynamicTransform;

    문서 표현으로 “pushes transform updates directly to the GPU, bypassing CPU mesh updates and improving performance” 입니다. 자리 갱신을 CPU 가 아니라 GPU 가 합니다.

    창 하나를 통째로 밀어 넣을 때 자식마다 표식 DynamicTransform DynamicTransform DynamicTransform DynamicTransform 변환이 넷으로 따로 갑니다 부모에 표식 하나 GroupTransform 표식 없음 표식 없음 표식 없음 표식 없음 한 번 걸면 GPU 가 아래로 퍼뜨립니다
    "applies a single transform to the parent, which the GPU efficiently propagates to all child elements"

    값이 넷입니다.

    값 문서 설명
    DynamicTransform “Optimizes rendering of a VisualElement for frequent position and transformation changes”
    GroupTransform “Optimizes rendering of a VisualElement that changes its transformation and position frequently, and has many descendants that have their hints set to DynamicTransform”
    MaskContainer “Optimizes rendering of a VisualElement that has multiple descendants with nested masks”
    DynamicColor “Optimizes rendering of a VisualElement for frequent color changes, such as a built-in style color being animated”

    GroupTransform 의 쓰임이 특히 분명합니다. 창 하나를 통째로 밀어 넣는 연출이라면 창 안의 요소 하나하나에 DynamicTransform 을 달 게 아니라, 부모에 GroupTransform 을 답니다. 문서 표현으로 “applies a single transform to the parent, which the GPU efficiently propagates to all child elements” 입니다.

    붙이는 기준은 자주 바뀌는가 하나입니다. 한 번 놓고 안 움직이는 요소에 달아 두면 얻는 것 없이 GPU 쪽 자원만 잡아 둡니다.

    바인딩은 리플렉션을 미리 없앱니다

    앞 편에서 데이터 바인딩을 봤는데, 비용 이야기를 안 했습니다. 바인딩이 값을 읽으려면 그 타입의 속성 목록이 필요하고, 그걸 런타임에 리플렉션으로 뒤지면 느립니다.

    문서가 말하는 property bag 이 그 목록입니다. “A property bag is a companion object that enables efficient traversal and manipulation of a type’s data.” 이걸 컴파일 때 만들어 두라는 것이 권고입니다.

    using Unity.Properties;
    
    [GeneratePropertyBag]
    public partial class PlayerData
    {
        [CreateProperty]
        public float hp { get; set; }
    }

    [CreateProperty] 는 앞 편에서 이미 썼습니다. 바인딩하려면 필요한 표식이었습니다. 여기에 [GeneratePropertyBag] 을 더하면 그 타입의 속성 목록까지 컴파일 때 생성됩니다. 런타임에 뒤질 일이 없어집니다.

    갱신 시점을 줄이는 것도 같은 줄기입니다. IDataSourceViewHashProvider 와 INotifyBindablePropertyChanged 를 구현하면 “trigger updates only when specific property values change” 입니다. 안 구현하면 변화를 감지할 방법이 없어서 매 프레임 비교하는 쪽으로 떨어집니다.

    값이 자주 안 바뀌는데 바인딩이 여럿이면 여기부터 봅니다.

    무엇이 돌았는지 보는 법

    지금까지가 무엇이 비싼지였고, 이건 무엇이 실제로 돌았는지 확인하는 쪽입니다. 문서가 네 가지를 듭니다.

    • Unity Profiler. 프레임마다 어느 구간이 오래 걸렸는지
    • UI Toolkit Debugger. 요소 트리와 걸린 스타일을 직접 보는 창
    • Frame Debugger. 드로우 콜이 몇 개로 나갔고 무엇이 배치를 깼는지
    • SetPanelChangeReceiver. Panel Settings 에 붙이는 것으로 “allows you to listen for changes to your UI and track their source” 입니다

    마지막 것이 이 편의 내용과 제일 가깝습니다. 무엇이 UI 를 바꿨는지 그 출처를 알려 줍니다. 에디터와 개발 빌드에서만 동작합니다.

    유니티 6 에서 이 쪽이 꽤 손봐졌다는 것도 적어 둘 만합니다. 문서가 드는 것 중에 “Event dispatching rules have been simplified, making them easier to understand and twice as fast” 가 있고, 메시 생성이 잡으로 돌게 바뀌었고, 깊은 계층의 레이아웃 캐싱이 개선됐습니다.

    정리

    • uGUI 는 캔버스 하나가 단위였고, UI Toolkit 은 스타일 해석 / 레이아웃 재계산 / 정점 버퍼 갱신 / 렌더 상태 변경 넷으로 갈립니다
    • 바꾼 값마다 어디까지 번지는지가 다릅니다. 크기를 건드리는 값만 형제와 부모까지 갑니다
    • opacity: 0 은 안 보일 뿐 계속 그립니다. 숨기는 용도가 아닙니다. uGUI 에서의 결론과 반대입니다
    • 자주 껐다 켤 것은 visibility: hidden, 오래 안 보일 것은 display: none 입니다
    • 자주 움직이는 요소에는 usageHints 를 답니다. 묶음째 움직이면 부모에 GroupTransform 하나면 됩니다
    • 바인딩은 [GeneratePropertyBag] 으로 속성 목록을 컴파일 때 만들어 두고, INotifyBindablePropertyChanged 로 바뀔 때만 갱신하게 합니다
    • 확인은 Profiler, UI Toolkit Debugger, Frame Debugger, 그리고 SetPanelChangeReceiver 입니다