무엇이 다시 계산되는지가 네 가지로 나뉜다
내가 만든 요소도 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 와 제일 크게 갈리는 지점입니다.
표에 없는 것 둘만 더 적어 둡니다. 모서리를 둥글리는 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 |
“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입니다