원문 : Other UI Optimization Techniques And Tips

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든지 내려갈 수 있습니다.



Other UI Optimization Techniques And Tips


확인 완료한 버전: 5.3 - 난이도: 고급


종종 UI 를 최적화할 명백한 방법이 없을 때도 있습니다. 이 섹션은 UI 성능을 개선하는데 도움을 줄 수도 있지만 구조적으로 "명백"하지는 않은, 유지보수하기는 어려울 수도 있거나 이상한 사이드 이펙트를 가질 수도 있는 몇 가지 제안을 하고자 합니다. 다른 것들은 초기 개발을 단순화하기 위한 UI 에서의 동작들을 위한 우회책들인데, 이는 상대적으로 성능 문제를 쉽게 발생시킬 수 있도록 해 줍니다.


RectTransform-Based Layouts


Layout 컴포넌트는 상대적으로 비쌉니다. 왜냐하면 그것들은 그것이 갱신될 때마다 자식 요소들의 크기와 위치를 재계산해야만 하기 때문입니다( 세부 사항을 원하면 Fundamentals 챕터의 Graphic rebuild 섹션을 참고하십시오 ). 만약 주어진 Layout 에 상대적으로 적고 고정된 개수의 요소들이 존재한다면, Layout 은 상대적으로 단순한 구조를 가지며, RectTransform 기반 레이아웃으로 대체하는 것이 가능할 것입니다.


RectTransform 에 대한 앵커를 할당함으로써, RectTrasnform 의 위치와 크기는 그것의 부모에 기반해 스케일링되도록 만들어질 수 있습니다. 예를 들어 단순한 두 개의 컬럼으로 구성된 레이아웃은 두 개의 RectTransform 들로 만들어질 수 있습니다:


    • 왼쪽 컬럼의 앵커는 X:(0, 0.5), Y(0,1) 이어야 합니다.
    • 오른쪽 컬럼의 앵커는 X(0.5,1), Y(0,1) 이어야 합니다.


RectTransform 의 크기와 위치에 대한 계산은 Transform 시스템 자체에 의해 네이티브 코드에서 수행될 것입니다. 이는 일반적으로 Layout 시스템에 의존하는 것 보다 더 좋은 성능을 냅니다. 또한 RectTransform 기반 레이아웃을 설정한 MonoBehaviour 를 작성하는 것도 가능합니다. 그러나 이는 상대적으로 복잡한 작업이며, 이 가이드가 다루는 영역에서 벗어납니다.


Disabling Canvas Renderers


UI 의 서로 다른 부분을 보여주거나 가릴 때, 보통 UI 의 루트에 있는 게임오브젝트를 enable 시키거나 disable 시킵니다. 이는 disable 된 UI 내의 컴포넌트가 input 이나 유니티 콜백을 받지 않도록 만듭니다.


그러나, 이는 Canvas 로 하여금 자신의 VBO 데이터를 버리도록 만듭니다. Canvas 를 다시 enabling 하는 것은 Canvas( 및 그것의 모든 Sub-Canvas ) 로 하여금 리빌드 절차와 리배칭 절차를 실행하도록 만듭니다. 만약 이러한 일이 자주 발생한다면, 증가된 CPU 사용량이 애플리에이션의 프레임 율을 뚝 떨어뜨릴 수 있습니다.


가능하다면, 이상하기는 하지만, UI 를 자신의 Canvas 나 Sub-canvas 상에 shown/hidden 되도록 배치하고 Canvas 나 Sub-canvas 에 붙어 있는 CanvasRenderer 컴포넌트를 enable/disable 시키는 것은 거의 하지 않는 우회책이 있습니다.


이는 UI 의 메쉬들이 그려지지 않도록 만들지만, 그것들은 메모리 상에 남아 있게 되며 원래의 배칭이 보존됩니다. 더우기 OnEnable 이나 OnDisable 콜백이 UI 계층상에서 호출되지 않을 것입니다.


그러나 이는 UI 의 Graphic 들을 GraphicRegistry 에서 제거하지 않으므로 그것들은 여전히 Graphic Raycast 를 검사하기 위해 컴포넌트의 리스트 상에 제출될 거라는 것에 주의하십시오. 이는 감춰진 UI 내의 어떠한 MonoBehaviour 들도 disable 시키지 않을 것이며, 이러한 MonoBehaviour 들은 여전히 Update 와 같은 유니티 생명주기 콜백들을 받을 것입니다.


이 문제를 피하기 위해서는, 이러한 방식으로 disable 될 UI 상의 MonoBehaviour 들이 유니티 생명주기 콜백들을 직접적으로 구현하지 않지만 UI 의 루트 게임오브젝트 상의 "Callback Manager" MonoBehaviour 로부터 자신들의 콜백을 받도록 해야 합니다. 이 "Callback Manager" 는 UI 가 shown/hidden 될 때마다 통지를 받아서 생명주기 이벤트들이 필요에 따라 전파되거나 전파되지 않도록 보장할 수 있습니다. 이 "Callback Manager" 패턴에 대한 더 많은 설명은 이 가이드의 영역을 벗어납니다.


Assigning Event Cameras


만약 가 월드 공간 카메라 모드나 스크린 공간 카메라 모드로 렌더링하도록 설정되어 있는 Canvas 들과 함께 유니티의 내장 Input Manager 를 사용하고 있다면, 항상 각각에 대해 Event Camera 나 Renderer Camera 속성을 설정하는 것이 중요합니다. 스크립트에서, 이는 항상 worldCamera 속성으로 노출됩니다.


만약 이 속성이 설정되지 않는다면, 유니티 UI 는 Main Camera 태그를 가진 게임 오브젝트에 붙어 있는 Camera 컴포넌트들을 검색함으로써 메인 카메라를 검색할 것입니다. 이 검색은 월드 공간 캔버스나 로컬 공간 캔버스당 적어도 한 번은 발생할 것입니다. GameObject.FindWithTag 는 느린 것으로 알려져 있기 때문에, 모든 월드 공간 캔버스와 카메라 공간 캔버스들은 디자인시나 초기화시에 할당된 Camera 속성들을 가질 것을 강력히 권합니다.


이 문제는 Overlay 캔바스에서는 발생하지 않습니다.

원문 : Optimizing UI Controls

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



Optimizing UI Controls


확인 완료한 버전: 5.3 - 난이도: 고급


이 섹션은 특정 유형의 UI 컨트롤들을 제출하는 것에 초점을 맞춘 최적화 가이드의 섹션입니다. 대부분의 컨트롤들은 성능 관점에서 상대적으로 유사하지만, 두 가지는 게임이 출시 가능한 상태에 가까워졌을 때 많은 성능 이슈를 일으키므로 특별합니다.


UI Text


유니티의 내장 Text 컴포넌트는 래스터화된 텍스트 글리프( glyph )를 UI 내부에 디스플레이하기 위한 편한 방법입니다. 그러나 일반적으로 알지 못하는 여러 가지 동작이 존재하며, 아직까지 주로 성능 핫스팟이 됩니다. UI 에 텍스트를 추가할 때, 텍스트 글리프는 실제로는 글자 하나당 개별적인 쿼드( quad )로 렌더링된다는 점을 기억해야 합니다. 이러한 쿼드들은 글리프를 둘러싼 빈 공간의 많은 부분을 차지하는 경향이 있습니다. 이는 그것의 모양에 의존하며, 원치않게 다른 UI 요소들의 배칭을 저해하는 텍스트가 배치되는 경우가 많습니다.


Text mesh rebuilds


한 가지 주요 이슈는 UI 텍스트 메쉬를 리빌드하는 것입니다. UI 텍스트 컴포넌트가 변경될 때마다, 그 텍스트 컴포넌트는 실제 텍스트를 디스플레이하기 위해서 사용되는 폴리곤들을 재계산해야 합니다. 이 재계산은 텍스트가 변하지 않더라도 텍스트 컴포넌트나 그것의 부모 게임 오브젝트들이 단순히 disable 되거나 다시 enable 될 때도 발생합니다.


이러한 동작은 점수판이나 통계를 위해 많은 개수의 레이블을 디스플레이하는 UI 에서 문제가 됩니다. 유니티 UI 를 가리거나 보여주는 가장 일반적인 방법은 UI 를 포함하는 게임오브젝트를 enable/disable 시키는 것이며, 많은 개수의 텍스트 컴포넌트를 가지고 있는 UI 는 그것들이 디스플레이될 때마다 원치않는 프레임율 스파이크를 일으키게 될 것입니다.


이 이슈에 대한 잠재적인 우회책을 원한다면, 다음 챕터의 Disabling Canvas Renderes 를 참고하시기 바랍니다.


Dynamic fonts and font atlases


동적 폰트는 전체 문자들의 집합이 매우 많거나 런타임에 앞서 알려지지 았을 때 텍스트를 렌더링하기 편한 방법을 제공합니다. 유니티 구현에서는, 이러한 폰트들은 UI Text 컴포넌트 내에 문자들에 기반해 런타임에 글리프 아틀라스를 빌드합니다.


로드된 각각의 개별 폰트 오브젝트는 자신만의 텍스쳐 아틀라스를 유지할 것입니다. 심지어는 그것이 다른 폰트들과 동일한 폰트 패밀리를 사용한다고 해도 말이죠. 예를 들어, 한 컨트롤 내에서 굵은( bold ) 텍스트를 가진 가진 Arial 을 사용하고, 다른 컨트롤에서 Arial Bold 를 사용하면 동일한 결과를 산출하지만, 유니티는 두 개를 개별적인 텍스쳐 아틀라스에 유지할 것입니다 - 하나는 Arial 을 위해 다른 하나는 Arial Bold 를 위해( 역주 : 하나는 Arial 폰트를 사용하고 텍스트에 굵기 옵션을 준 것이고, 다른 하나는 Arial Bold 폰트를 사용한 것을 의미하는 듯 ).


성능 관점에서 볼 때, 유니티 UI 의 동적 폰트가 개별 크기, 스타일, 문자를 위해 폰트 텍스쳐 아틀라스에 하나의 글리프를 유지한다는 것을 이해하는 것이 중요합니다. 즉, 만약 UI 가 두 개의 텍스트 컴포넌트를 가지고 있고, 둘다 'A' 라는 문자를 디스플레이한다고 하면:


    • 만약 두 Text 컴포넌트가 같은 크기를 공유한다면, 폰트 아틀라스는 그것 안에 하나의 글리프를 가지게 될 것입니다.
    • 만약 두 Text 컴포넌트가 같은 크기를 공유하지 않는다면( 예를 들어 하나는 16 포인트이고 다른 하나는 24 포인트라면 ), 폰트 아틀라스는 서로 다른 크기의 문자 'A' 를 위해 두 개의 복사본을 가지게 될 것입니다.
    • 만약 하나의 Text 컴포넌트는 bold 이고 다른 하나는 그렇지 않다면, 폰트 아틀라스는 굵은 'A' 와 보통 'A' 를 포함하게 될 것입니다.


동적 폰트를 가진 UI Text 오브젝트에 아직 폰트 텍스쳐로 래스터화되지 않은 글리프가 있다면, 폰트 텍스쳐 아틀라스는 반드시 리빌드되어야만 합니다. 새로운 글리프가 현재 아틀라스에 맞다면, 그것이 추가되고 그 아틀라스는 그래픽 디바이스에 다시 업로드될 것입니다. 그러나 현재 아틀라스가 너무 작다면, 시스템은 아틀라스를 리빌드할 것입니다. 이는 두 개의 단계로 수행됩니다.


첫째, 아틀라스는 같은 크기로 리빌드됩니다. 이 때 현재 활성화된 UI Text 컴포넌트에 의해 보이고 있는 글리프만이 사용됩니다. 만약 시스템이 현재 사용중인 글리프들을 새로운 아틀라스에 끼워 맞추는 것에 성공한다면, 그것은 그 아틀라스를 래스터화하고 다음 단계로 진행하지 않습니다.


둘째, 만약 현재 사용중인 글리프 집합이 현재 아틀라스와 같은 크기의 아틀라스에 끼워 맞춰질 수 없다면, 해상도가 더 낮은 쪽을 두 배로 늘린 더 큰 아틀라스가 생성됩니다. 예를 들어 512x512 아틀라스는 512x1024 아틀라스로 확장됩니다.


위의 알고리즘 때문에, 동적 폰트 아틀라스는 한 번 생성되고 나면 크기가 늘어나기만 합니다. Given the cost of rebuilding the texture atlases, 리빌드 동안 반드시 최소화해야 합니다. 이는 두 가지 방식으로 수행될 수 있습니다:


가능할 때마다, 원하는 글리프 셋을 위해서 비-동적 폰트들과 preconfigure support 를 사용하십시오. 이는 일반적으로 제약이 잘 된 Latin/ASCII 문자들과 같은 적은 범위의 크기를 가지는 문자 집합을 사용하는 UI 들을 위해 잘 동작합니다.


만약 전체 유니코드 셋과 같은 극단적으로 큰 범위를 가진 문자들이 지원되어야만 한다면, 폰트는 Dynamic 으로 설정되어야만 합니다. 예측할 수 있는 성능 문제들을 피하기 위해서는, 시작시에 폰트 글리프 아틀라스를 Font.RequestCharactersInTexture 를 통해 적절한 문자들의 집합을 가지도록 미리 준비시키기 바랍니다.


폰트 아틀라스 리빌드는 변경되는 각 UI Text 컴포넌트들을 위해서 개별적으로 발동된다는 것에 주의하십시오. 극단적으로 많은 개수의 Text 컴포넌트들을 띄울 때는, Text 컴포넌트의 내용에서 유일한 문자들을 수집하고 폰트 아틀라스를 미리 준비해야 이득입니다. 이는 글리프 아틀라스가 새로운 글리프가 나타날 때마다 한 번씩 리빌드되지 않고 한 번만 리빌드될 수 있도록 보장해 줄 것입니다.


폰트 아틀라스가 리빌드될 때, 활성화된 UI Text 컴포넌트에 현재 포함되어 있지 않은 모든 문자들은, 그것들이 Font.RequestCharactersInTexture 호출의 결과로서 원래부터 아틀라스에 추가되어 있는 경우라 할지라도, 새로운 아틀라스에 제출되지 않는다는 것에도 주의하시기 바랍니다. 이러한 제한을 우회하기 위해서는, 모든 원하는 문자들이 준비되어 있는 상태로 남아 있도록 하기 위해 Font.textureRebuilt 델리케이트를 구독하고 Font.characterInfo 를 질의하십시오.


Font.textureRebuilt 델리게이트는 현재 문서화되어 있지 않습니다. 그것은 단일 인자를 가진 유니티 이벤트입니다. 이 인자는 그것의 텍스쳐가 리빌드되어 있는 폰트입니다. 이 이벤트의 구독자는 다음 구문을 따라야 합니다:



Specialized glyph renderes


각 글리프 사이에 상대적으로 고정된 위치를 가지고 있는 잘 알려져 있는 글리프의 경우에는, 그들의 글리프를 디스플레이하는 커스텀 컴포넌트를 작성하는 것이 이득입니다. 이것의 예는 스코어 디스플레이가 있습니다.


점수를 위해, 디스플레이 가능한 문자들은 잘 알려진 글리프 셋( 숫자 0 ~ 9 )으로부터 그려지며, 지역에 따라 변하지 않고, 서로 고정된 거리로 보여집니다. 정수를 그것의 숫자로 분리하고 적절한 숫자 스프라이트를 렌더링하는 것은 상대적으로 단순합니다. This sort of specialized digit-display system can be built in a manner that is both allocationless and considerably faster to calculate, animate and display than the Canvas-driven UI Text component.


Fallback fonts and memory usage


큰 문자 셋을 지원해야만 하는 애플리케이션을 위해서는, 폰트 임포터의 "Font Names" 필드에서 많은 폰트들을 리스팅하고자 하는 마음이 듭니다. "Font Names" 필드에 리스팅된 모든 폰트들은 글리프가 주요 폰트 내에 배치될 수 없는 상황에서 대안으로 사용될 수 있을 것입니다. 폴백의 순서는 "Font Names" 필드에서 리스팅된 순서에 의해서 결정됩니다.


그러나 이러한 동작을 지원하기 위해서는 유니티는 "Font Names" 필드에서 리스팅된 모든 폰트들을 메모리에 로드해서 유지할 것입니다. 만약 폰트 문자 셋이 매우 크다면, 폴백 폰트에 의해 소비되는 메모리의 양이 매우 커질 것입니다. 이는 일본어 한자나 중국 문자들과 같은 그림 문자( pictographical ) 폰트를 포함할 때 자주 보이는 현상입니다.


Best Fit and performance


일반적으로, UI Text 컴포넌트의 Best Fit 설정은 절대 사용되어서는 안 됩니다.


"Best Fit" 는 동적으로, 이는 Text 컴포넌트의 바운딩 박스 내에 오우버플로우 없이 디스플레이될 수 있으며 구성 가능한 최소/최대 포인트 크기로 잘린, 가장 큰 정수 포인트 크기로 폰트의 크기를 조정합니다. 그러나 유니티는 디스플레이되고 있는 개별 문자의 크기별로 개별 글리프를 폰트 아틀라스에 렌더링하기 때문에, Best Fit 를 사용하면 다양한 크기의 글리프를 사용하는 아틀라스의 크기를 급격히 넘어서게 될 것입니다.


유니티 5.3 에서는, Best Fit 에 의해 사용되는 크기 검출이 선택적이지 않습니다. 그것은 It generates glyphs in the font atlas for each size increment tested, which further increases the amount of time required to generate font atlases. 이는 아틀라스 오우버플로우를 발생시키는 경향이 있는데, 이는 오래된 글리프가 아틀라스에서 제거되도록 만듭니다. Best Fit 계산을 위한 많은 횟수의 테스트 때문에, 이는 종종 다른 Text 컴포넌트에 의해 사용중인 글리프들을 퇴거시킬 것이고, 적절한 폰트 사이즈가 계산된 후에 적어도 한 번 더 폰트 아틀라스를 리빌드시킬 것입니다. 이러한 특정 이슈는 유니티 5.4 에서 교정되었습니다. 그리고 Best Fit 는 폰트 텍스쳐 아틀라스를 불필요하게 확장하지 않을 것이지만, 여전히 정적으로 크기가 정해진 텍스트보다는 훨씬 더 느립니다.


자주 사용하는 폰트 아틀라스의 리빌드는 런타임 성능을 급격히 떨어뜨릴 것이고 메모리 단편화를 일으킬 것입니다. Best Fit 로 설정된 Text 컴포넌트의 양이 많아질 수록, 이 문제가 악화될 것입니다.


Scroll Views


Fill-rate 문제 다음으로, 유니티 UI 의 스크롤 뷰는 일반적으로 두 번째 가는 런타임 성능 이슈입니다. 스크롤 뷰는 일반적으로 매우 많은 개수의 UI 요소들을 사용해 자신의 칸텐츠를 표현할 것을 요구합니다. 스크롤 뷰를 띄우는 기본적인 두 가지 접근법이 있습니다:


    • 모든 스크롤 뷰 칸텐츠를 표현하기 위해서 충분한 모든 요소들을 채웁니다.
    • 요소들을 풀링하여, 보여지는 칸텐츠를 표현하는 데 필요한 만큼 위치를 재조정합니다.


둘 다 문제를 가진 해결책들입니다.


첫 번째 해결책은 모든 UI 요소들을 인스턴스화기 위해서 필요한 시간을 증가시킵니다. 왜냐하면 표현해야 할 아이템이 늘어날 수록 스크롤 뷰를 리빌드하는 시간이 늘어나기 때문입니다. 한줌의 Text 컴포넌트만을 디스플레이할 필요가 있는 스크롤 뷰처럼, 스크롤 뷰 내에 요구되는 요소의 개수가 적다면, 단순함을 위해 이 기법이 선호됩니다.


두 번째 해결책은 현재 UI 와 레이아웃 시스템 하에서 올바르게 구현하기 위한 코드의 양이 너무 많습니다. 아래에서 나중에 두 가지 가능한 기법에 대해서 논의하도록 하겠습니다. 매우 복잡한 스크롤링 UI 에 대해서, 성능 문제를 피하기 위해서는 몇 가지 종류의 풀링 접근법이 필요합니다.


이러한 이슈들에도 불구하고, 모든 접근법들은 RectMask2D 컴포넌트를 스크롤 뷰에 추가함으로써 개선될 수 있습니다. 이 컴포넌트는 스크롤 뷰 뷰포트 외부에 존재하는 스크롤 뷰 요소들이, Canvas 가 리빌딩될 때 지오메트리가 생성되고 정렬되고 분석되어야 하는, drawable 요소의 리스트에 포함되지 않도록 보장해 줍니다.


Simple Scroll View element pooling


유니티 내장 Scroll View 컴포넌트를 사용하는 것의 자연스러운 이점의 대부분을 보존하면서 스크롤 뷰를 사용하는 오브젝트 풀링을 구현하기 위한 가장 단순한 방법은 하이브리드 접근법을 사용하는 것입니다:


UI 에 요소들을 놓기 위해서, 보이는 UI 요소들을 위한 "placeholder" 로서 Layout Element 컴포넌트를 게임오브젝트와 함께 사용하십시오. 이는 레이아웃 시스템이 스크롤 뷰의 칸텐트를 적절히 계산하도록 해 주고 스크롤바가 적절히 기능하게 해 줄 것입니다.


그리고 나서, 스크롤 뷰의 가시 영역의 보이는 부분에 충분히 맞게 보이는 UI 요소들에 대한 풀을 인스턴스화하고, 이것들을 위치 placeholder 들의 부모로 붙이십시오. 스크롤 뷰가 스크롤링되면, UI 요소들을 재사용해서 뷰에 스크롤된 칸텐트들을 디스플레이하십시오.


이는 배칭되어야 하는 UI 요소들의 개수를 지속적으로 줄이게 될 것입니다. 왜냐하면 배칭 비용은 RectTransform 의 개수가 아니라 Canvas 내의 CanvasRendere 의 개수에 기반해서만 증가할 것이기 때문입니다.


Problems with the simple approach


현재는, UI 요소의 부모가 변경되거나 이웃의 순서가 변경될 때마다, 그 요소와 그것의 하위 요소들은 갱신된 것으로 표시되며 그것들의 Canvas 가 강제로 리빌드됩니다.


그 이유는 유니티가 transform 의 부모를 변경하는 콜백과 이웃의 순서를 수정하기 위한 콜백을 분리하지 않았기 때문입니다. 이 이벤트들은 둘 다 OnTransformParentChanged 콜백을 부릅니다. 유니티 UI 의 Graphic 클래스의 소스( Graphics.cs )에서, 그 콜백이 구현되며 SetAllDirty 메서드를 호출합니다. Graphic 을 갱신함으로써, 시스템은 Graphic 이 다음 프레임에 렌더링되기 전에 그것의 레이아웃과 버텍스들을 리빌드할 수 있도록 만듭니다.


캔버스들에 스크롤 뷰 내의 각 요소에 대한 루트 RectTransform 을 할당하는 것이 가능합니다. 그러면 스크롤 뷰의 전체 칸텐츠가 아니라 부모가 변경된 요소들에 국한해서 리빌드를 할 것입니다. 그러나 이것은 스크롤 뷰를 렌더링하는 데 필요한 드로콜의 개수를 증가시키는 경향이 있습니다. 더우기, 스크롤 뷰 내부의 개별 요소들이 복잡하고 수십개의 Graphic 컴포넌트로 구성되어 있고, 특히 각 요소에 Layout 컴포넌트들이 매우 많다면, 그것들을 리빌드하는 비용은 저사양 디바이스에서 프레임율을 급격하게 떨어뜨리게 될 것입니다.


만약 스크롤 뷰 요소가 가변 크기를 가지지 않는다면, 레이아웃과 버텍스들에 대한 완전한 재계산은 불필요합니다. 그러나 이 동작을 피하는 것은 부모 변화나 이웃 순서 변화 대신에 위치 변화에 기반한 오브젝트 풀링 솔루션을 구현할 것을 요구합니다.


Position-based Scroll View Pools


위에서 언급한 문제들을 피하기 위해서, 오브젝트가 포함된 UI 요소들의 RectTransform 들을 단순하게 이동시킴으로써 오브젝트를 풀링하는 스크롤 뷰를 생성하는 것이 가능합니다. 움직여진 RectTransform 의 크기가 변경되지 않았다면 내용은 다시 빌드될 필요가 없으며, 이는 스크롤 뷰의 성능을 매우 개선하게 됩니다.


일반적으로는 이를 위해 ScrollView 의 커스텀 서브클래스를 작성하거나 커스텀 LayoutGroup 컴포넌트를 작성하는 것이 최선입니다. 후자는 일반적으로 더 단순한 솔루션이며, 유니티 UI 의 LayoutGroup 추상 기저 클래스의 서브클래스를 구현함으로써 성취할 수 있습니다.


커스텀 LayoutGroup 은 밑에 있는 소스 데이터들을 분석해 얼마나 많은 데이터 요소들이 디스플레이되어야 하고 스크롤 뷰의 Content RectTransform 이 적절히 리사이징될 수 있는지 판단합니다. 그리고 나서 그것은 ScrollView change event 들을 구독하고 이를 사용해 그것의 가시적 요소를 적절하게 재배치할 수 있습니다.

원문 : Fill-Rate, Canvases and Input

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



Fill-Rate, Canvases and Input


확인 완료한 버전: 5.3 - 난이도: 고급


이 챕터는 유니티 UI 를 구조화하는 것과 관련한 여러 이슈들에 대해 논의합니다.


Remediating Fill-Rate Issues


GPU 프래그먼트 파이프라인 상에서 부하를 제거하기 위해서 취해질 수 있는 두 가지 행동이 있습니다:


    • 프래그먼트 쉐이더의 복잡도를 줄이는 것입니다.
      • 더 많은 세부사항을 원한다면 "UI Shaders and low-spec devices" 섹션을 참고하십시오.
    • 샘플링되어야 하는 픽셀의 개수를 줄이는 것입니다.


UI 쉐이더는 일반적으로 표준화되어 있기 때문에, 가장 일반적인 문제는 단순히 fill-rate 용량을 초과하는 것입니다. 이는 대부분 UI 요소들이 매우 많이 겹쳐 그려지거나 화면의 많은 부분을 차지하는 UI 요소들이 많기 때문에 발생합니다. 둘다 높은 수준의 overdraw 를 유발할 수 있습니다.


fill-rate 에 대한 overutilization 을 줄이고 overdraw 를 줄이려면, 다음과 같은 해결책들을 고려해 보십시오.


Eliminating invisible UI


이 기법은, 플레이어에게 보이지 않는 비활성화된 요소들을 단순화하기 위해, 현존하는 UI 요소들을 최소로 재설계하는 것을 요구합니다. 가장 일반적인 경우는 불투명한 백그라운드를 가진 전체화면 UI 를 여는 것입니다. 이 경우 전체화면 UI 아래에 배치된 모든 UI 요소들은 비활성화될 수 있습니다.


이를 위한 가장 단순한 방법은 루트 게임오브젝트나 안 보이는 UI 요소를 포함하는 게임오브젝트들을 비활성화하는 것입니다. 대안적인 해결책을 원한다면 Disabling Canvas Renderer 섹션을 참고하십시오.


Disabling invisible camera output


불투명한 배경을 가진 전체 화면 UI 가 유니티 UI 에서 열려 있다면, 월드 공간 카메라는 여전히 표준 3D 씬을 UI 뒤에다가 렌더링하고 있을 것입니다. 이 렌더러는 전체화면 유니티 UI 가 전체 3D 씬을 가리고 있다는 사실을 인지하지 못합니다.


그러므로, 만약 완전한 전체화면 UI 가 열렸다면, 차폐된 모든 월드 공간 카메라를 비활성화하는 것은 GPU 부하를 줄여주며, 이는 단순히 불필요한 3D 공간 렌더링을 제거함으로써 수행됩니다.


노트: 만약 Canvas 가 "Screen Space - Overlay" 로 설정되어 있다면, 씬에서 활성하된 카메라의 개수를 무시하고 그려질 것입니다.


Majority-obscured cameras


많은 "전체화면" UI 들은 실제로는 전체 3D 월드를 차폐하지 않습니다. 그러나 월드의 일부분만을 보이게합니다. 이 경우에는, 보이는 월드 부분을 렌더 텍스쳐에 캡쳐하는 것이 더 효율적일 수 있습니다. 만약 월드의 보이는 부분이 렌더 텍스쳐에 "캐싱"되면, 실제 월드 공간 카메라가 비활성화될 수 있으며, 캐싱된 렌더 텍스쳐는 3D 월드에 대한 임포스터 버전을 제공하기 위해서 UI 스크린 뒤에 그려질 수 있습니다.


Composition-based UIs


컴포지션을 통해서 UI 를 생성하는 것은 매우 일반적입니다 - 최종 UI 를 생성하기 위해서 표준 백그라운드와 요소들을 합치고 레이어화하는 것입니다. 이 작업은 상대적으로 단순하고 반복적으로 수행하기 쉽지만, 그것은 유니티 UI 가 transparent 렌더링 큐를 사용하므로 성능에 좋지 않습니다.


백그라운드, 버튼, 그리고 버튼 위의 텍스트로 구성된 단순한 UI 를 생각해 봅시다. 텍스트 글리프( glyph ) 내에 포함된 픽셀의 경우, GPU 는 백그라운드 텍스쳐와 버튼 텍스쳐를 샘플링한 후에 최종적으로 텍스트 아틀라스 텍스쳐를 처리합니다. 결국 세 번의 샘플링이 필요합니다. UI 의 복잡도가 커질수록, 백그라운드 위쪽에 레이어화되어야 하는 요소들이 많아지며, 샘플링 개수는 급격하게 증가할 것입니다.


만약 큰 UI 가 fill-rate 한계( bound )에 도달했다는 것이 밝혀지면, UI 의 장식된( decorative )/불변하는( invariant ) 요소들을 그것의 백그라운드 텍스쳐와 머지하는 특별한 UI 스프라이트를 생성하는 것이 최선입니다. 이는 다른 요소 위에 레이어화되어야 하는 요소의 개수를 줄여주지만, 노가다가 필요하며 프로젝트의 텍스쳐 아틀라스의 크기가 증가합니다.


주어진 UI 를 특별한 UI 스프라이트 위에 생성하는 데 필요한 레이어화된 요소들을 압축하는데 있어서의 원칙은 sub-element 들을 위해서 사용될 수도 있습니다. 제품에 대한 스크롤링 팬을 가진 스토어 UI 를 생각해 봅시다. 각 제품의 UI 요소는 경계선, 배경, 가격을 표시하는 아이콘, 이름, 그리고 다른 정보들을 포함합니다.


스토어 UI 는 백그라운드를 필요로 하지만, 제품은 백그라운드 위에서 스크롤링되기 때문에 제품 요소는 스토어 UI 의 백그라운드 텍스쳐에 머지될 수 없습니다. 그러나 경계선, 가격, 이름, 그리고 다른 요소들은 제품의 백그라운드에 머지될 수 있습니다. 아이콘의 크기와 개수에 따라, fill-rate 절약을 고려해 볼 수 있습니다.


레이어화된 요소들을 합치는 것은 몇 가지 단점을 가지고 있습니다. 특별한 요소들을 더 이상 재사용할 수 없으며, 그것을 생성하기 위해서는 추가적인 아티스트 자원을 요구합니다. 새로운 큰 텍스쳐의 추가는 UI 텍스쳐를 저장하는데 필요한 메모리 총량을 증가시킬 것이며, 특히 UI 텍스쳐가 on-demand 로 로드되거나 언로드되지 않는다면 문제는 심각해질 것입니다.


UI Shaders and low-spec devices


유니티 UI 에 의해 사용되는 내장 쉐이더는 마스킹, 클리핑, 그리고 여러 가지 복잡한 연산에 대한 지원을 포함합니다. 이는 복잡도를 증가시키므로, UI 쉐이더는 iPhone 4 와 같은 low-end 장치 상에서의 더 단순한 유니티 2D 쉐이더와 비교했을 때 매우 느립니다.


만약 마스킹, 클리핑, 그리고 다른 "화려한" 기능들이 low-end 장치에서 필요하지 않다면, 최소 UI 쉐이더와 같은 불필요한 연산을 제거하는 커스텀 쉐이더를 만드는 것이 가능합니다.



UI Canvas Rebuilds


어떤 UI 를 디스플레이하기 위해서, UI 시스템은 스크린상에 표현된 각 UI 컴포넌트를 위한 지오메트리를 생성해야 합니다. 이는 동적 레이아웃 코드를 실행하거나 글자를 UI 텍스트 문자열로 표현하기 위해서 폴리곤을 생성한다거나 드로 콜을 최소화하기 위해서 가능한한 많은 지오메트리를 하나의 메쉬로 통합한다거나 하는 일들을 포함합니다. 이것은 여러 단계의 절차로 구성되며, 이 가이드의 시작부분의 Fandamentals 섹션에서 세부적으로 기술하고 있습니다.


두 가지 주요 원인 때문에 Canvas 를 리빌드하는 것은 성능 문제를 일으킬 수 있습니다:


    • 만약 Canvas 상에서 그려질 수 있는 UI 요소의 개수가 너무 많다면, 자체적으로 배치를 계산하는 비용이 매우 비싸집니다. 이는 요소들을 정렬하고 분석하는 비용 때문이며, 이 비용은 요소의 개수에 따라 비선형적으로 증가합니다.
    • 만약 Canvas 가 자주 갱신되면, 상대적으로 적은 변화를 가지는 Canvas 보다 리프레쉬하는데 더 많은 비용을 소비해야 합니다.


이러한 문제들은 모두 Canvas 상의 요소 개수가 증가하기 때문에 심해지는 경향이 있습니다.


중요하게 기억할 점: 그릴수 있는 UI 요소가 변경될 때마다, Canvas 는 배치 빌딩 절차를 재수행해야만 합니다. 이 절차는 그릴 수 있는 UI 요소 전체를 그것이 변경되었는지 여부와 관계없이 다시 분석합니다. UI 오브젝트의 외형에 영향을 주는 모든 변경을 "변경"이라고 하며, 이는 스프라이트 렌더러에 할당된 스프라이트, transform 위치와 스케일, 텍스트 메쉬에 포함된 텍스트 등을 포함합니다.


Child order


유니티 UI 들은 뒤에서 앞으로 생성되는데, 계층 구조 상에서 오브젝트의 순서가 그것의 정렬 순서를 결정합니다. 계층에서 먼저 등장하는 오브젝트는 나중에 등장하는 오브젝트보다 뒤에 있다고 간주됩니다. 배치는 계층을 위에서 아래로 돌면서 빌드되며, 같은 머티리얼과 텍스쳐를 사용하지만 중간 레이어( intermediate layer, 서로 다른 머티리얼을 가진 그래피컬 오브젝트인데, 그것의 바운딩 박스는 배치 가능한 다른 오브젝트와 겹치며 배치 가능한 두 오브젝트 사이의 계층에 배치되어 있습니다 )를 가지지 않는 모든 오브젝트들이 수집됩니다. 중간 레이어들은 배치를 깨게 됩니다.


Unity Frame Debugger 섹션에서 언급했듯이, 프레임 디버거는 중간 레이어들을 위한 UI 를 확인하기 위해 사용될 수 있습니다. 이는 한 오브젝트가 배치 가능한 다른 두 오브젝트 사이에 끼어들고 있는 상황입니다.


이 문제는 텍스트나 스프라이트가 다른 것들 근처에 위치할 때 자주 발생합니다: 텍스트의 바운딩 박스는 인접한 스프라이트와 보이지는 않지만 겹치게 됩니다. 왜냐하면 텍스트 글리프의 대부분의 폴리곤이 투명하기 때문입니다. 이는 두 가지 방법으로 해결될 수 있습니다:


    • 배칭될 수 없는 오브젝트에 의해서 배칭될 수 있는 오브젝트가 방해받지 않도록 요소를 재정렬합니다: 즉, 배칭될 수 없는 오브젝트를 배칭될 수 있는 오브젝트의 위쪽이나 아래쪽으로 움직입니다.
    • 오브젝트의 위치를 수정해서 보이지 않는 겹침을 제거합니다.


이 두 가지 방법은 모두 유니티 프레임 디버거를 활성화시켜서 수행될 수 있습니다. 유니티 프레임 디버거에서 보이는 드로콜의 개수를 관찰함으로써, UI 요소가 겹침으로써 낭비되는 드로콜의 개수를 최소화하는 순서와 위치를 찾는 것이 가능합니다.


Splitting Canvases


사소하기는 하지만, Canvas 를 나누는 것도 일반적으로 좋은 생각입니다. 요소들을 sub-canvas 나 인접한 Canvas 로 옮기는 것입니다.


( 예를 들어, 튜토리얼 화살표처럼 ) UI 의 특정 부분이 그 UI 의 나머지와는 다르게 깊이가 제어되어야 하는 경우에는 인접한 Canvas 로 옮기는 전략을 사용하는 것이 최선입니다.


다른 경우에는, sub-canvas 가 더욱 편리합니다. 왜냐하면 그것은 부모 Canvas 의 디스플레이 세팅을 상속하기 때문입니다.


일견하기에는 UI 를 sub-canvas 들에 하위분할하는 것이 최상이라 생각할 수 있지만, Canvas 시스템은 분리된 Canvas 들을 배치에 합치지 않는다는 점에 주의하십시오. 성능을 고려한 UI 설계는 리빌드 비용을 최소화하는 것과 드로콜을 낭비하는 것을 최소화하는 것 사이의 균형을 요구합니다.


General guidelines


Canvas  는 Canvas 내부의 변경된 요소들을 아무때나 리배칭하기 때문에, 단순하지 않은 Canvas 는 적어도 두 부분으로 분리하는 것이 일반적으로 최상입니다. 더우기 만약 요소들이 동시에 변경될 것을 기대하고 있다면 같은 Canvas 상에 요소들을 같이 배치하려고 시도하는 것이 최상입니다. 그 예로는 프로그레스 바와 카운트다운 타이머를 들 수 있습니다. 이것들은 모두 같은 데이터에 기반하고 있으므로, 동시에 갱신되는 것을 요구할 것이며, 그것들은 같은 Canvas 에 배치되어야 합니다.


정적이거나 변하지 않는 백그라운드나 레이블과 같은 모든 요소들을 한 Canvas 에 배치하십시오. 이것들은 Canvas 가 처음 디스플레이될 때 한 번에 배칭됩니다. 그리고 앞으로는 더 이상 리배칭이 필요하지 않을 것입니다.


"동적인" 요소들을 다른 캔버스에 배치하십시오 - 그것들은 자주 변경되는 것들입니다. 이는 이 Canvas 가 자주 갱신되는 요소들을 리배칭하는 것을 보장할 것입니다. 만약 동적 요소의 개수가 매우 많아진다면, ( 예를 들어 프로그레스 바, 타이머 표시, 움직이는 요소와 같은 ) 지속적으로 변화하는 요소 집합과 가끔 변화하는 요소 집합으로 분리하십시오.


경험적으로 볼 때 이는 좀 더 어렵습니다. 특히 UI 컨트롤들을 프리팹으로 캡슐화할 때 어렵습니다. 대신에 많은 UI 들은 비용이 많이 드는 컨트롤들을 Sub-Canvas 로 옮김으로써 Canvas 를 하위분할하는 정책을 씁니다.


Unity 5.2 and Optimized Bathcing


유니티 5.2 에서는 배칭 코드가 지속적으로 재작성되었으며, 4.6, 5.0, 5.1 과 비교했을 때 더 많은 성능 개선이 있었습니다. 더우기 1 개 이상의 코어를 가진 장치에서, 유니티 UI 시스템은 대부분의 처리를 워커 스레드에 넘길 것입니다. 일반적으로, 유니티 5.2 는 UI 를 수십개의 Sub-canvas 로 공격적으로 분할할 필요성을 제거했습니다. 모빌 장치 상의 많은 UI 들은 이제 2 ~ 3 개의 Canvas 들만 가지고도 좋은 성능을 낼 수 있습니다.


유니티 5.2 에서의 최적화에 대한 더 많은 정보를 원하신다면, 이 블로그 포스트를 참고하십시오.


Input and Raycasting In Unity UI


기본적으로, 유니티 UI 는 Graphic Raycast 컴포넌트를 사용해서 터치나 마우스 이벤트와 같은 입력 이벤트들을 다룹니다. 이는 일반적으로 Standalone Input Manager 컴포넌트에 의해 제어됩니다. 이름 대문에 Standalone Input Manager 가 "universal" 입력 관리 시스템으로 보이는데, 이는 마우스와 터치를 모두 다룰 것입니다.


Erroneous mouse input detection on mobile (5.3)


5.4 전에는 Graphic Raycaster 가 붙은 각각의 활성화된 Canvas 가 마치 터치 입력이 비활성화 되어 있는 것처럼 마우스의 위치를 검사하기 위해 매프레임 레이캐스트를 수행했습니다. 이는 플랫폼을 구분하지 않고 발생했습니다; iOS 와 안드로이드 장치에는 마우스가 없는데 여전히 마우스 위치를 질의하고 UI 요소들이 마우스 포인터 아래에 있는지 확인하려고 시도했습니다.


이는 CPU 시간을 낭비했으며, 유니티 애플리케이션 CPU 프레임 시간의 5% 이상을 소비하는 것이 밝혀졌습니다.


이 이슈는 5.4 에서 해결되었습니다: 5.4 부터는 마우스를 가지지 않은 장치는 마우스 위치를 질의하지 않으면 불필요한 레이케이트를 수행하지 않을 것입니다.


5.4 전 버전을 사용하고 있다면 모빌 개발자들은 자신만의 Input Manager 클래스를 작성할 것을 강력히 권합니다. 이는 유니티의 Standard Input Manager 를 유니티 UI 소스로부터 복사함으로써 단순화될 수 있으며, ProcessMouseEvent 메서드를 그 메서드에 대한 호출과 함께 주석처리해 버리면 됩니다.


Raycast optimization


Graphic Raycast 는 상대적으로 직관적인 구현이며, 이는 "Raycast Target" 설정이 true 인 모든 Graphic 컴포넌트를 돕니다. 각 Raycast Target 에 대해, Raycaster 는 몇 개의 테스트를 수행합니다. 만약 Raycast Target 이 모든 테스트를 통과하면, 히트 리스트에 그것이 추가됩니다.


Raycast implementation details


테스트는 다음과 같은 시점에 수행됩니다:


    • Raycast Target 이 활성화되어 있고, 그려질 때( 예를 들어 지오메트리 )
    • 입력 포인트가 Raycast Target 이 붙어 있는 RectTransform 내에 존재할 때
    • Raycast Target 이 ICanvasRaycastFilter 컴포넌트를 가지고 있거나 ( 깊이 상관없이 ) 그것의 자식일 때, 그리고 그 Raycast Filter 컴포넌트가 레이캐스트를 허용할 때


히트된 Raycast Target 의 리스트는 깊이에 의해 정렬되며, 순서를 바꾸기 위해서 필터링되며, 카메라 뒤에 렌더링되는 ( 예를 들어 스크린에 보이지 않는 ) 요소들을 제거하기 위해서 필터링됩니다.


Graphic Raycaster 는 Graphic Raycast 의 "Blocking Objects" 속성에 각각의 플래그가 설정되어 있으면 3D 나 2D 물리 시스템으로 레이를 캐스트할 수도 있습니다( 스크립트에서 수행하려면, blockingObjects 라는 속성임 ).


만약 2D 혹은 3D 블락킹 오브젝트가 활성화되어 있다면, raycast-blocking 물리 레이어 상에서 2D 혹은 3D 오브젝트 아래에서 그려지는 모든 Raycast Target 은 히트 리스트로부터 제거될 것입니다.


그리고 나서 최종 리스트가 반환됩니다.


Raycasting optimization tips


모든 Raycast Target 들은 Graphic Raycaster 에 의해서 테스트되어야만 한다면, 포인터 이벤트를 받아야만 하는 IU 컴포넌트 상의 "Raycast Target" 만 활성화하는 것이 최상입니다. Raycast Target 의 리스트를 더 작게 만들고 더 얕은 계층이 순회될 수록, Raycast 테스트의 속도가 빨라질 것입니다.


백그라운드와 텍스트가 모두 색상을 변경할 필요가 있는 버튼과 같이, 포인터 이벤트에 응답해야만 하는 다중의 UI 오브젝트들을 포함하는 복합 UI 컨트롤의 경우에는, 복합 UI 컨트롤의 루트에 하나의 Raycast Target 만을 배치하는 것이 더 좋습니다. 그 단일 Raycast Target 이 포인터 이벤트를 받게 되면, 그 이벤트를 관심있는 컴포넌트들에게 전달할 수 있습니다.


Hierarchy depth and raycast filters


각각의 Graphic Raycast 는 raycast 필터를 찾을 때 루트까지 모든 Transform 계층을 순회합니다. 이 연산의 비용은 계층 내의 깊이에 따라 선형적으로 증가합니다. 계층 내의 각 Transform 에 붙어 있는 모든 컴포넌트들이 ICanvasRaycastFilter 를 구현했는지 확인하기 위해 테스트되어야만 하므로, 이는 싼 연산이 아닙니다.


ICanvasRaycastFilter 를 사용하는 CanvasGroup, Image, Mask, RectMask2D 와 같은 표준 유니티 UI 컴포넌트들이 몇 개 있습니다. 그러므로 이 순회 비용은 쉽게 제거되기 힘듭니다.


Sub-canvas and the OverridedSorting property


Sub-canvas 에 있는 overrideSorting 속성은 Graphic Raycast 테스트가 Transform 계층을 올라가는 것을 막습니다. 만약 정렬이나 레이캐스트 검출 이슈없이 이것이 활성화될 수 있다면, 레이캐스트 계층 순회의 비용을 감소시키기 위해서 이것을 사용하십시오.

원문 : Funadamentals of Unity UI

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



Fundametals of Unity UI


확인 완료한 버전: 5.3 - 난이도: 고급


유니티 UI 시스템을 구성하는 여러 부분을 이해하는 것은 중요합니다. 그 시스템을 구성하는 여러 개의 기본 클래스들과 컴포넌트들이 있습니다. 이 챕터는 먼저 이 기사의 시리즈 전반에서 사용될 몇 개의 개념들을 정의하고, 유니티 UI 의 몇 가지 핵심 시스템의 저수준 동작에 대해 논의할 것입니다.


Teminology


Canvas 네이티브 코드 유니티 컴포넌트인데, 이는 게임의 월드 공간의 상위에 그려질 레이어화된 지오메트리( geometry )를 제공하기 위해 유니티 렌더링 시스템에 의해 사용됩니다.


Canvas 들은 자신의 내부 지오메트리를 배칭( batching )할 책임이 있으며, 적절한 렌더링 커맨드들을 생성하고 이것들을 유니티 그래픽스 시스템에 전달합니다. 이것은 전부 네이티브 C++ 코드에서 수행되며, 이는 rebatch 라든가 batch build 라고 불립니다. 캔버스가 리배칭을 요구하는 지오메트리로 구성되어 있다고 표시되면, 그 캔버스는 갱신되었다( dirty )고 판단됩니다.


지오메트리는 Canvas Renderer 컴포넌트에 의해 Canvas 에 제공됩니다.


Sub-canvas 는 그냥 Canvas 컴포넌트인데, 이는 다른 Canvas  컴포넌트에 내포됩니다. Sub-canvas 들은 그들의 부모의 다른 자식들과는 독립적입니다: 한 자식이 갱신되었다고 해서 부모로 하여금 그것의 지오메트리를 리빌드하도록 강제하지는 않을 것이라는 것입니다.


Graphic 클래스는 유니티 UI C# 라이브러리에 의해 제공되는 기반 클래스입니다. 이것은 Canvas 시스템 상에서 그려질 수 있는 지오메트리를 제공하는 모든 유니티 UI C# 클래스들의 기반 클래스입니다. 대부분의 내장 유니티 UI Graphics 들은 MaskableGrphic 서브 클래스를 통해서 구현됩니다. 이는 IMaskable 인터페이스를 통해 그것들이 마스킹될 수 있도록 해 줍니다. Drawable 의 주요 서브클래스들은 Image 와 Text 입니다. 그것들은 자신의 이름과 동일한 컴포넌트들을 제공합니다.


Layout 컴포넌트는 RectTransform 의 위치와 크기를 제어하며, 일반적으로 자신의 칸텐츠에 대한 상대적인 크기와 위치를 요구하는 복잡한 레이아웃을 생성하기 위해서 사용됩니다. Layout 컴포넌트는 RectTransform 에 의존하며, 이는 그것들에 연관된 RectTransform 의 속성에 영향을 미칩니다. 그것들은 Graphic 클래스에 대해서 종속적이지 않으며, 유니티 UI 의 Graphic 컴포넌트들과는 독립적으로 사용될 수 있습니다.


Graphic 컴포넌트와 Layout 컴포넌트는 모두 CanvasUpdateRegistry 클래스에 의존하는데, 이는 유니티 에디터 인터페이스에는 노출되지 않습니다. 이 클래스는 갱신되어야만 하는 Layout 컴포넌트와 Graphic 컴포넌트의 집합을 추적하는 클래스이며, 그것과 연관된 Canvas 가 willRenderCanvases 이벤트를 호출할 때 필요에 의해 트리거들이 갱신됩니다.


Layout 컴포넌트와 Graphic 컴포넌트에 대한 갱신을 rebuild 라 부릅니다. 이 리빌드 절차에 대해서는 이 문서의 나중에 더 세부적으로 다루도록 하겠습니다.


Rendering Details


유저 인터페이스를 유니티 UI 로 만들 때, Canvas 에 의해 그려지는 모든 지오메트리들은 Transparent 큐에 그려질 것이라는 점을 기억하십시오. 즉, 유니티 UI 에 의해서 생성된 지오메트리는 항상 알파블렌딩을 사용해 뒤에서 앞 순서로 그려질 것입니다. 성능 관점에서 기억해야 할 중요한 것은, 폴리곤으로부터 래스터화된 각 픽셀은 마치 그것이 다른 불투명한 폴리곤에 의해 의해 완전히 가려져 있는 것처럼 샘플링될 것이라는 것입니다. 모바일 장치에서는, 고수준의 overdraw 는 GPU 의  fill-rate 수용량( capacity )를 급격하게 초과할 수 있습니다.


The Batch Building Process (Canvases)


배치 빌딩 절차는 Canvas 가 그것의 UI 요소들을 표현하는 메시들을 결합하고 유니티의 그래픽스 파이프라인에 보내기 위한 렌더링 커맨드들을 생성하는 절차입니다. 이 절차의 결과는 Canvas 가 갱신( dirty )되었다고 표시될 때까지 캐싱되고 재사용됩니다. 이러한 갱신은 그것을 구성하는 메시들 중의 하나가 변경될 때마다 발생합니다.


Canvas 에 의해 사용되는 메시는 Canvas 에 붙어 있는 Canvas Renderer 컴포넌트로부터 획득됩니다. 하지만 Sub-canvas 에 포함된 Canvas Renderer 에서 획득하지는 않습니다.


배치를 계산하는 것은 메시를 깊이값에 의해 정렬하고 겹치는지 혹은 머티리얼을 공유하는지 등을 검사하는 작업들을 요구합니다. 이 연산은 멀티-스레드에서 수행되기 때문에 그것의 성능은 일반적으로 CPU 아키텍쳐가 달라질 때마다 달라지며, 특히 모바일 ( 일반적으로 몇 안 되는 CPU 코어를 가진 ) SoC 와 현대 데스크탑의 ( 보통 4 개 이상의 코어를 가지고 있는 ) CPU 사이에서 큰 차이가 납니다.


The Rebuild Process ( Graphics )


리빌드 절차는 유니티 UI 의C# Graphic 컴포넌트의 레이아웃과 메시가 재계산되는 곳입니다. 이는 CanvasUpdateRegistry 클래스에 의해 수행됩니다. 이것은 C# 클래스이며, 그것의 소스는 Unity's Bitbucket 에서 찾아볼 수 있음을 기억하십시오.


CanvasUpdateRegistry 내에서, 흥미로운 메서드는 PerformUpdate 입니다. 이 메서드는 Canvas 컴포넌트가 WillRenderCanvases 이벤트를 호출할 때마다 호출됩니다. 이 이벤트는 프레임당 한 번 불립니다.


PerformUpdate 는 세 단계의 절차로 실행됩니다:


    • 레이아웃을 리빌드하기 위해서 갱신된 Layout 컴포넌트들이 요청됩니다. 이는 ICanvasElement.Rebuild 메서드를 통해 수행됩니다.
    • 컬링과 클리핑을 수행하기 위해서 등록된 모든 ( Mask 같은 ) Clipping 컴포넌트들이 요청됩니다. 이는 ClippingRegistry.Cull 을 통해 수행됩니다.
    • 그래피컬 요소들을 리빌드하기 위해서 갱신된 Graphic 컴포넌트들이 요청됩니다.


Layout 과 Graphic 을 리빌드하기 위해, 이 절차는 여러 개의 부분으로 나뉩니다. Layout 리빌드는 세 가지 부분으로 실행됩니다( PreLayout, Layout, PostLayout ). 그리고 Graphic 리빌드는 두 가지 부분으로 실행됩니다( PreRender, LatePreRender ).


Layout rebuilds


하나 이상의 Layout 컴포넌트에 포함된 컴포넌트의 적절한 위치를 계산하기 위해서는, 그것들의 적절한 계층적 순서를 적용할 필요가 있습니다. 게임 오브젝트 계층에서 루트와 가까운 Layout 들은 잠재적으로 그것들에 내포되어 있는 다른 Layout 들의 위치와 크기를 수정할 수 있습니다. 그러므로 가장 먼저 계산되어야만 합니다.


이를 위해, 유니티 UI 는 계층 내에서의 깊이를 중심으로 갱신된 Layout 컴포넌트들을 정렬합니다. 계층 구조에서 높은 곳에 있는 아이템( 예를 들어 부모 Transform 이 거의 없는 아이템 )들은 리스트의 앞쪽으로 이동됩니다.


그리고 나서 레이아웃을 리빌드하기 위해서 Layout 컴포넌트들의 정렬된 리스트가 요청됩니다: 여기에서 Layout 컴포넌트에 의해서 제어되는 UI 요소들의 위치와 크기가 실제로 수정됩니다. 개별 요소들의 위치가 Layout 의 영향을 받는 방식에 대한 더 세부적인 내용을 원한다면, 유니티 매뉴얼의 UI Auto Layout 섹션을 참고하십시오.


Graphics rebuilds


Graphic 컴포넌트들이 리빌드될 때, 유니티 UI 는 ICanvasElement 인터페이스의 Rebuild 메서드에 대한 제어를 넘깁니다. Graphic 은 이를 구현하며, 리빌드 절차의 PreRender 스테이지 동안 두 개의 리빌드 단계를 실행합니다.


    • 만약 버텍스 데이터가 갱신되었다고 표시되었다면( 예를 들어, 컴포넌트의 RectTransform 의 크기가 변했다면 ), 메시가 리빌드됩니다.
    • 만약 머티리얼 데이터가 갱신되었다고 표시되었다면( 예를 들어, 컴포넌트의 머티리얼이나 텍스쳐가 변경되었다면 ), 동봉된 Canvas Renderer 의 머티리얼이 갱신됩니다.


Graphic 리빌드는 특정 순서로 처리되지는 않습니다. 그리고 어떠한 정렬 연산도 요구하지 않습니다.

원문 : AssetBundle Usage Patterns

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.

주의 : 아래쪽의 일부 섹션들은 현재 제 관심사와 관계가 없어 보여서 번역하지 않았습니다.



AssetBundle Usage Patterns


확인 완료한 버전: 5.3 - 난이도: 고급


이 문서는 유니티 5 의 애셋, 리소스, 리소스 관리를 다루는 기사 시리즈의 다섯 번째 챕터입니다.


이 시리즈의 이전 챕터에서는 애셋번들의 기초에 대해 다뤘는데, 특히 다양한 로딩 API 의 저수준 동작에 대해 다뤘습니다. 이 챕터는 애셋번들을 실무에서 사용하는 것의 다양한 관점에 대한 문제들과 잠재적 해결책들에 대해 논의합니다.


4.1. Managing Loaded Assets


메모리에 민감한 환경에서 로드된 오브젝트의 크기와 개수를 조심스럽게 제어하는 것은 매우 중요합니다. 유니티는 활성화된 씬에서 오브제트가 제거될 때 자동으로 그것들을 언로드하지 않습니다. 애셋 정리는 특정 시점에 발동되며, 이것은 수동으로 발동될 수도 있습니다.


애셋번들 자체는 매우 주의깊게 관리되어야만 합니다. ( 유니티 캐시에 있거나 AssetBundle.LoadFromFile 에 의해 로드된 ) 로컬 저장소로부터 로드된 애셋번들은 최소한의 메모리 부하를 가지는데, 약 10 ~ 40 KB 를 잘 넘지 않습니다. 이 부하는 매우 많은 개수의 애셋번들이 제출될 때 여전히 문제가 될 수 있습니다.


대부분의 프로젝트들은 유저에게 ( 레벨을 리플레이하는 것과 같은 ) 재경험 칸텐츠를 허용하며, 애셋번들을 로드하거나 언로드해야 하는 시점을 아는 것은 중욯바니다. 만약 애셋번들이 부적절하게 언로드되면, 그것은 메모리상에서 오브젝트가 중복되도록 만들 수 있습니다. 애셋번들을 부적절하게 언로드하는 것은 특정 환경에서 원치않는 동작을 유발할 수도 있는데, 이는 텍스쳐가 없어진다던가 하는 상황을 만듭니다. 이런 일이 왜 일어나는지 이해하려면, Asset, Objects, and Serialization 챕터의 Inter-Object references 섹션을 참고하십시오.


애셋과 애셋번들을 관리할 때 가장 중요한 것은 AssetBundle.Unload 의 인자를 true 로 공급하느냐 false 로 공급하느냐에 따라 다른 동작을 한다는 것을 이해하는 것입니다.


이 API 는 호출되고 있는 애셋번들의 헤더 정보를 언로드합니다. 인자는 이 애셋번들로부터 인스턴스화된 모든 오브젝트를 언로드할지 여부를 지정합니다. 만약 true 라면, 애셋번들로부터 생성된 모든 오브젝트들은 즉시 언로드됩니다 - 심지어 활성화된 씬에서 현재 사용중일지라도 말이죠.


예를 들어, 머티리얼 M 이 애셋번들 AB 로부터 로드되고, M 이 현재 활성화된 씬에서 사용주이라고 가정해 보겠습니다.


만약 AB.Unload(true) 가 호출되면, M 은 씬에서 제거되며 파괴되고 언로드될 것입니다. 그러나 AB.Unload(false) 가 호출되면 AB 의 헤더 정보가 언로드되지만, M 은 씬에 남아 있게 되고 여전히 자신의 기능을 수행할 것입니다. AssetBundle.Unload(false) 호출은 M 과 AB 간의 링크를 끊습니다. 만약 AB 가 나중에 다시 로드된다면, AB 에 포함된 오브젝트들에 대한 새로운 복사본이 메모리에 로드될 것입니다.


만약 AB 가 나중에 다시 로드된다면, 애셋번들의 헤더 정보의 새로운 복사본이 다시 로드될 것입니다. 하지만 M 은 AB 의 새 복사본으로부터 로드되지 않습니다. 유니티는 AB 와 M 의 새로운 복사본 사이의 어떠한 링크도 만들어 주지 않습니다.


만약 AB.LoadAsset() 을 호출해 M 을 다시 로드한다면, 유니티는 M 의 예전 복사본을 AB 안의 데이터의 인스턴스인 것처럼 해석하지 않을 것입니다. 그러므로 유니티는 M 의 새로운 복사본을 로드하고 씬에는 M 에 대한 동일한 두 개의 복사본이 존재하게 될 것입니다.


대부분의 프로젝트의 경우에, 이러한 동작은 원치않는 동작입니다. 대부분의 프로젝트들은 AssetBundle.Unload(true)를 사용해야 하며 오브젝트들이 중복되지 않는다고 확신하기 위한 기법을 써야 합니다. 두 개의 기법이 있습니다:


    1. 애플리케이션 생명주기 동안에 일시적인 애셋번들이 언로드되는 잘 정의된 지점을 가지고 있어야 합니다. 이러한 예로는 레벨이 전환되는 시점이나 로딩 스크린이 나오고 있는 동안이 있습니다. 이는 단순하고 가장 흔한 선택입니다.
    2. 개별 오브젝트들에 대한 참조 카운트를 유지하고, 애셋번들에서 생성된 오브젝트가 더 이상 사용되지 않을 때 애셋번들을 언로드하는 것입니다.


먄약 애플리케이션이 AssetBundle.Unload(false) 를 사용해야만 한다면, 개별 오브젝트들은 두 가지 방식으로 언로드될 수 있습니다:


    1. 원치않는 오브젝트에 대한 모든 참조를 제거하는데, 이는 씬과 코드에서 모두 이루어져야 합니다. 이 작업이 끝나면 Resources.UnloadUnusedAllsets 를 호출합니다.
    2. 씬을 non-additively 로 로드합니다. 이는 현재 씬에 있는 모든 오브젝트들을 파괴하고 Resources.UnloadUnusedAssets 를 자동으로 호출할 것입니다.


만약 프로젝트가 오브젝트가 로드되거나 언로드되는 동안 사용자로 하여금 대기하게 만들 수 있는( 게임 모드나 레벨이 전환되는 사이와 같은 ) 잘 정의된 지점을 가지고 있다면, 이 지점들은 필요한 만큼 많은 오브젝트들을 언로드하고 새로운 오브젝트들을 로드하는데 사용되어야 합니다.


이를 위한 가장 단순한 방법은 분리된 청크의 프로젝트를 씬에 패키징하는 것입니다. 그리고 나서 씬의 모든 종속성과 함께 씬을 애셋번들로 빌드합니다. 그리고 나서 애플리케이션은 씬 "로딩"에 들어 가는데, 이전 씬에 포함된 모든 애셋번들을 완전히 언로드하고, 새로운 씬을 포함하는 애셋번들을 로드합니다.


이것이 가장 단순한 플로우이기는 하지만, 어떤 프로젝트들은 좀 더 복잡한 애셋번들 관리를 요구합니다. 보편적인 애셋번들 설계 패턴이라는 것은 존재하지 않습니다. 각 프로즈게트의 데이터는 서로 다릅니다. 오브젝트들을 애셋번들로 그룹화하는 방법을 결정할 때, 일반적으로 가장 좋은 것은 동시에 로드되거나 업데이트되어야만 하는 오브젝트들을 애셋번들로 묶는 것입니다.


예를 들어, 롤플레잉 게임을 생각해 봅시다. 개별 맵과 컷씬들은 씬에 의해 애셋번들로 그룹화될 수 있습니다. 하지만 어떤 오븢게트들은 대부분의 씬에서 필요할 것입니다. 애셋번들은 초상화, 인게임 UI, 서로 다른 캐릭터 모델 및 텍스쳐를 제공하기 위해서 빌드도리 수 있습니다. 나중에 언급한 오브젝트들과 애셋들은 애플리케이션 시작시에 로드되어 애플리케이션 생명주기 동안 로드된 상태를 유지하는 애셋번들의 2차 집합으로 그룹화될 수 있습니다.


애셋번들이 언로드된 다음에 유니티가 애셋번들로부터 오브젝트를 다시 로드해야 하는 경우에 다른 문제가 발생할 수 있습니다. 이 경우에, 리로드는 실패할 것이며, 유니티 에디터의 계층에서는 (Missing) 오브젝트로 나타날 것입니다.


이 상황은, 모바일 앱이 중단되었을 때나 사용자가 PC 를 lock 했을 때 같이, 유니티가 그것의 그래픽스 칸텍스트에 대한 제어를 잃거나 다시 획득할 때 주로 발생합니다. 이 경우, 유니티는 GPU 에 텍스쳐와 쉐이더를 다시 업로드해야 합니다. 만약 이 애셋들을 위한 소스 애셋번들이 이용할 수 없는 상태라면, 애플리케이션은 "missing shader" 마젠타색으로 씬의 오븢게트를 렌더링하게 될 것입니다.


4.2. Distribution


프로젝트의 애셋번들을 클라이언트에 배포하기 위한 두 가지 기본 방식이 있습니다: 프로젝트와 함께 설치하는 것과 설치 후에 다운로드하는 것. 어떤 방식을 선택하느냐는 프로젝트가 실행되어야 하는 플랫폼의 기능과 제약에 의해 결정됩니다. 모바일 프로젝트들은 보통 초기 설치 크기를 줄이고 무선 다운로드 크기 제한 아래로 파일 크기를 유지하기 위해서 post-install 옵션을 선택합니다. 콘솔이나 PC 프로젝트들은 일반적으로 애셋번들을 초기 설치에 포함시킵니다.


적절한 구조를 만들면 애셋번들이 처음에 어떤 방식으로 전달되었느냐의 여부와는 관계없이 프로젝트의 post-install 로 새로운 혹은 수정된 칸텐트를 패치할 수 있습니다. 이와 관련한 더 많은 정보를 원한다면 이 기사의 Patching with AssetBundles 섹션을 참고하십시오.


4.2.1. Shipped with project


프로젝트에 애셋번들을 포함시키는 것은 애셋번들을 배포하는 가장 단순한 방법입니다. 왜냐하면 그것은 추가적인 다운로드 관리 코드를 필요로하지 않기 때문입니다. 프로젝트가 애셋번들을 설치 파일에 포함시켜야 하는 이유는 크게 두 가지가 있습니다:


    • 프로젝트 빌드 시간을 줄이고 반복적인 개발을 더 단순하게 만들기 위해서입니다. 만약 이 애셋번들이 애플리케이션 자체로부터 개별적으로 갱신될 필요가 없다면, 애셋번들은, 스트리밍 애셋에 애셋번들을 저장함으로써, 애플리케이션에 포함될 수 있습니다. 아래의 Streaming Assets 섹션을 참고하십시오.
    • 업데이트가 가능한 칸텐트의 초기 리비전을 포함시키기 위해서입니다. 이는 보통 초기 설치 이후에 엔드유저의 시간을 절약하기 위함이거나 나중의 패칭을 위한 기반의 역할을 하기 위해서입니다. Streaming Assets 는 이 경우에 이상적입니다. 그러나 커스텀 다운로딩 및 캐싱 시스템을 선택할 수 없다면, 업데이트가능한 칸텐트의 초기 리비전은 스트리밍 애셋에서 유니티 캐시로 로드될 수 있습니다.


4.2.1.1. Streaming Assets


설치시에 유니티 애플리케이션에 모든 유형의 칸텐트를 포함하는 가장 쉬운 방법은 프로젝트를 빌드하기 전에 칸텐트를 /Assets/StreamingAssets/ 폴더에 빌드하는 것입니다. 빌드시에 StreamingAssets 폴더 내에 포함된 모든 애셋은 최종 애플리케이션으로 복사됩니다. 이 폴더는 애셋번들 뿐만 아니라 최종 애플리케이션 내의 모든 유형의 칸텐트를 저장하기 위해 사용될 수 있습니다.


로컬 저장소 상에서 StreamingAssets 폴더의 전체 경로는 런타임에 Application.streamingAssetsPath 프라퍼티를 통해서 접근할 수 있습니다. 대부분의 플랫폼에서 애셋번들들은 AssetBundle.LoadFromFile 을 통해 로드될수 있습니다.


안드로이드 개발자: 안드로이드에서 Application.streamingAssetsPath 는 압축된 .jar 파일을 가리킬 것입니다. 마치 애셋번들이 압축된 것처럼 말이죠. 이 경우, WWW.LoadFromCacheOrDownload 을 호출해서 개별 애셋번들을 로드해야 합니다. 또한 .jar 파일을 압축해제하기 위한 커스텀 코드를 작성하고 애셋번들을 로컬 저장소의 읽기가능 위치로 추출하는 것도 가능합니다.


노트: 스트리밍 애셋은 특정 플랫폼에서는 쓰기가능한 위치가 아닙니다. 만약 프로젝트의 애셋번들이 설치 후에 갱신될 필요가 있다면, WWW.LoadFromCacheOrDownload 를 사용하거나 커스텀 다운로더를 작성하십시오. 세부사항을 원한다면 Custom downloaders - storage 섹션을 참고하십시오.


4.2.2. Downloaded post-install


애셋번들을 모바일 디바이스로 전송하기 위해 선호되는 기법은 번들을 앱 설치 후에 다운로드하는 것입니다. 이는 사용자가 전체 애플리케이션을 다시 다운로드하게 하지 않고도 설치 후에 새로운 혹은 변경된 칸텐트들을 사용해 칸텐트들이 업데이트될 수 있도록 해 줍니다. 모바일 플랫폼에서, 애플리케이션 바이너리들은 비싸고 복잡한 재인증 절차를 밟아야만 합니다. 그러므로 post-install 다운로드를 위한 좋은 시스템을 개발하는 것이 중요합니다.


애셋번들을 전송하기 위한 가장 단순한 방법은 웹 서버에 그것을 배치하고 WWW.LoadFromCacheOrDownload 혹은 UnityWebRequest 를 통해 전송하는 것입니다. 유니티는 자동으로 다운로드된 애셋번들을 로컬 저장소에 캐싱합니다. 만약 다운로드된 애셋번들이 LZMA 로 압축이 되어 있다면, 그 애셋번들은 추후의 빠른 로딩을 위해 압축이 안 된 캐시로 저장될 것입니다. 만약 다운로드된 번들이 LZ4 로 압축이 되어 있다면, 애셋번들은 압축된 상태로 저장될 것입니다.


만약 캐시가 꽉 차면, 유니티는 가장 최근에 사용되지 않은 애셋번들을 캐시에서 제거합니다. 세부사항을 원하면 Built-in caching 을 참고하십시오.


WWW.LoadFromCacheOrDownload 는 결점을 가지고 있음에 주의하십시오. Loading AssetBundles 섹션에서 기술했듯이, WWW 오브젝트는 다운로드를 하는 동안 애셋번들의 데이터와 같은 크기의 메모리를 소비합니다. 이는 원치않는 메모리 스파이크를 유발할 수 있습니다. 이를 피하기 위한 세 가지 방법이 있습니다:


    • 애셋번들을 작은 크기로 만드십시오. 번들이 다운로드되고 있을 때 애셋번들의 최대 크기는 프로젝트의 메모리 예산에 의해 결정될 것입니다. 다운로딩 스크린을 가진 애플리케이션은 애셋번들을 백그라운드에서 스트리밍하는 애플리케이션보다 더 많은 메모리를 할당할 수 있습니다.
    • 만약 유니티 5.3 이상 버전을 사용하고 있다면, 새로운 UnityWebRequest API 의 DownloadHandlerAssetBundle 로 전환하시기 바랍니다. 이는 다운로드 동안 메모리 스파이크를 일으키지 않습니다.
    • 커스텀 다운로더를 작성하십시오. 더 많은 정보를 원한다면 Custom downloaders 섹션을 참고하십시오.


가능하면 UnityWebRequest 를 사용해서 시작하는 것을 권합니다. 만약 5.2 이전 버전을 사용한다면 WWW.LoadFromCacheOrDownload 를 사용하십시오. 만약 built-in API 의 메모리 소비, 캐싱, 동작, 성능이 특정 프로젝트에서 원치 않는 결과를 보여 준다면, 혹은 프로젝트가 플랫폼-특정 코드를 실행해서 그것의 요구를 만족시켜야만 한다면, 커스텀 다운로드 시스템에 투자하십시오.


UnityWebRequest 나 WWW.LoadFromCacheOrDownload 의 사용을 막아야 하는 경우는 다음과 같습니다:


    • 애셋번들 캐시에 대한 fine-grained 제어가 요구될 때
    • 프로젝트가 커스텀 압축 전략을 구현할 필요가 있을 때
    • 프로젝트가 플랫폼-특정 API 를 사용해서, 비활성화되어 있는 동안 데이터를 스트림하고자 하는 요구같은, 특정 요구를 만족시키고자 할 때 
      • 예: iOS 백그라운드 태스크 API 를 사용해 백그라운드에서 데이터를 다운로드하고자 할 때
    • ( PC 처럼 ) 적절한 SSL 지원을 가지고 있지 않은 플랫폼 상에서, SSL 을 통해 전송되어야만 할때


4.2.3. Built-in caching


유니티는 빌트인 애셋번들 캐싱 시스템을 가지고 있는데, 이는 WWW.LoadFromCacheOrDownload 나 UnityWebRequest API 를 통해 다운로드된 애셋번들을 캐싱하기 위해서 사용될 수 있습니다.


두 API 는 모두 애셋번들 버전 번호를 인자로 받는 오우버로드 메서드를 가지고 있습니다. 이 번호는 애셋번들 내에 저장되는 것이 아니며, 애셋번들 시스템에 의해서 생성되지도 않습니다.


캐싱 시스템은 WWW.LoadFromCacheOrDownload 나 UnityWebRequest 에 넘겨진 마지막 버전 번호의 기록을 유지합니다. 다른 API 가 버전 번호와 함께 호출될 때, 캐싱 시스템은 캐싱된 애셋번들이 존재하는지를 확인합니다. 만약 존재한다면, 그것은 애셋번들이 처음 캐싱되었을 때 넘겨진 버전 번호와 현재 호출에 넘겨진 버전 번호를 비교합니다. 만약 이 번호가 일치하면, 시스템은 캐싱된 애셋번들을 로드하게 됩니다. 만약 번호가 일치하지 않거나 캐싱된 애셋번들이 존재하지 않는다면, 유니티는 새로운 복사본을 다운로드합니다. 이 새로운 복사본은 새로운 버전 번호와 연관됩니다.


캐싱 시스템 내의 애셋번들은 그것들이 다운로드되었던 전체 URL 에 의해서가 아니라 파일 이름에 의해서만 식별됩니다. 이는 같은 이름을 가진 애셋번들이 서로 다른 위치에 저장될 수도 있다는 것을 의미합니다. 예를 들면 애셋번들은 CDN( Content Delivery Network )의 다중 서버 상에 배치될 수 있습니다. 파일 이름이 동일한 이상, 캐싱 시스템은 그것들을 같은 애셋번들로 인지합니다.


애셋번들에 버전 번호를 할당하고 이 번호를 WWW.LoadFromCacheOrDownload 에 넘기기 위한 적절한 전략을 세우는 것은 개별 애플리케이션에 달려 있습니다. 대부분의 애플리케이션들은 유니티 5 의 AssetBundleManifest API 를 사용할 수 있습니다. 이 API 는 애셋번들 칸텐트의 MD5 해시를 계산함으로써 각 애셋번들의 버전 번호를 생성합니다. 애셋번들이 변경될 때마다 그것의 해시는 변경되며, 이는 그 애셋번들이 다운로드되어야만 한다는 것을 지시합니다.


노트: 유니티 빌트인 캐시 구현의 특이점( quirk ) 때문에, 예전 애셋번들은 캐시가 꽉 차기 전까지는 삭제되지 않을 것입니다. 유니티는 앞으로의 릴리스에서 이 특이점에 대해 고심할 의도를 가지고 있습니다.


세부사항을 원한다면 Patching with AssetBundles 섹션을 참고하십시오.


유니티의 빌트인 캐싱은 Caching 오브젝트 상의 API 를 호출함으로써 제어될 수 있습니다. 유니티 캐시의 동작은 Caching.expirationDelay 와 Caching.maximumAvailableDiskSpace 를 변경함으로써 제어될 수 있습니다.


Caching.expirationDelay 는 애셋번들이 자동으로 제거되기 전에 기다려야 할 최소한의 시간( 초 )입니다. 만약 이 시간 동안 애셋번들에 대한 접근이 없으면, 그것은 자동으로 지워집니다.


Caching.maximumAvailableDiskSpace determines the amout of space on local storage that the cache may use before it begings deleting AssetBundles that have been used less recently than the expirationDelay. 그것은 바이트 단위로 카운팅됩니다. 제한에 도달하게 되면, 유니티는 가장 옛날에 열린( 혹은 Caching.MarkedAsUsed 로 마킹된 ) 캐시에서 애셋번들을 제거합니다. 유니티는 새로운 다운로드를 완료하기 위해 충분한 공간을 확보할 때까지 캐싱된 애셋번들을 제거할 것입니다.


노트: 유니티 5.3 에서는 빌트인 캐시에 대한 제어가 매우 거칩니다. 특정 애셋번들을 캐시에서 제거하는 것은 불가능합니다. 그것들은 expiration 이나 디스크 공간 부족, 혹은 Caching.CleanCache 호출을 통해서만 제거될 수 있습니다( Caching.CleanCache 는 현재 캐시에 있는 모든 애셋번들을 제거하게 됩니다 ). 이는 개발이나 라이브 연산 동안에는 문제가 될 수 있습니다. 왜냐하면 유니티가 애플리케이션에 의해서 더 이상 사용되지 않는 애셋번들을 자동으로 지워주지 않기 때문입니다.


4.2.3.1. Cache Priming


애셋번들은 파일 이름으로 식별되기 때문에, 애플리케이션에 포함된 애셋번들을 사용하는 캐시를 "미리 지정하는( prime )" 것이 불가능합니다. 이를 위해서는, 각 애셋번들의 초기 혹은 기본 버전을 /Assets/StreamingAssets/ 에 저장하십시오. 그 절차는 Shipped with project 섹션에서 세부적으로 설명한 것과 동일합니다.


애플리케이션이 처음 실행될 때 Application.streamingAssetsPath 로부터 애셋번들을 로딩함으로써 캐시가 생성될 수 있습니다. 그 때부터, 애플리케이션은 WWW.LoadFromCacheOrDownloads 나 UnityWebRequest 를 정상적으로 호출할 수 있습니다.


4.2.4. Custom downloaders


커스텀 다운로더를 작성하는 것은 애플리케이션으로 하여금 애셋번들을 다운로드하고, 압축해제하고, 저장하는 것과 관련한 모든 제어를 할 수 있게 해 줍니다. 커스텀 다운로더를 작성하는 것은 야심찬 애플리케이션을 작성하고 있는 큰 팀들에 대해서만 권장됩니다. 커스텀 다운로더를 작성하는 동안 생각해야 하는 네 가지 정도의 문제가 있습니다:


    • 애셋번들을 다운로드하는 방법.
    • 애셋번들을 저장할 위치.
    • 애셋번들을 압축하는 방법이나 압축할지 여부.
    • 애셋번들을 패치하는 방법.


애셋번들을 패치하는 것과 관련한 더 많은 정보를 원한다면, Patching with AssetBundles 섹션을 참고하십시오.


4.2.4.1. Downloading


대부분의 애플리케이션에서, HTTP 는 애셋번들을 다운로드하는 가장 단순한 방법입니다. 그러나 HTTP 기반 다운로더를 구현하는 것은 단순한 작업이 아닙니다. 커스텀 다운로더는 너무 많은 메모리를 할당하는 행위, 너무 많은 스레드를 사용하는 행위 등을 피해야만 합니다. 유니티의 WWW 클래스는 여기에서 속속들이 설명한 이유 때문에 부적절합니다. WWW 는 높은 메모리 비용을 가지고 있으므로, WWW.LoadFromCacheOrDownload 클래스를 사용하는 경우가 아니라면 유니티의 WWW 클래스를 사용하는 것은 피하는 것이 좋습니다.


커스텀 다운로더를 작성할 때, 세 가지 옵션이 있습니다:


    • C# 의 HttpWebRequest 와 WebClient 클래스.
    • 커스텀 네이티브 플러그인.
    • 애셋 스토어 패키지.


4.2.4.1.1. C# classes


애플리케이션이 HTTPS/SSL 지원을 요구하지 않는다면, C# 의 WebClient 클래스는 애셋번들을 다운로드하기 위한 가장 단순한 가능성있는 메커니즘을 제공합니다. 그것은 관리되는 메모리 할당을 초과하는 일이 없이 로컬 저장소에 파일을 바로 다운로드하는 기능을 제공합니다.


애셋번들을 WebClient 를 통해 다운로드하기 위해서는, 클래스의 인스턴스를 할당하고 그것에 다운로드할 애셋번들의 URL 과 대상 경로를 넘깁니다. 만약 요청 파라미터에 대한 더 많은 제어를 원한다면, HttpWebRequest 클래스를 사용해서 다운로더를 작성하는 것이 가능합니다.


    1. HttpWebResponse.GetResponseStream 으로부터 바이트 스트림을 획득합니다.
    2. 고정 크기 바이트 버퍼를 스택에 할당합니다.
    3. 응답 스트림으로부터 읽어들여 버퍼에 씁니다.
    4. C# File.IO API 를 사용하거나 다른 스트리밍 IO 시스템을 사용해서 버퍼를 디스크에 씁니다.


플랫폼 노트: iOS, 안드로이드, 윈도우즈 폰은 유니티 C# 런타임이 C# HTTP 클래스들을 위한 HTTPS/SSL 지원을 포함하고 있는 유일한 플랫폼들입니다. PC 에서는, C# 클래스를 통해 HTTPS 서버에 접근하는 시도가 인증서 검증 에러( certificate validation error )를 발생시킬 것입니다.


4.2.4.1.2. Asset Store Packages


몇 개의 애셋 스토어 패키지들은 HTTP, HTTPS, 그리고 다른 프로토콜들을 통해서 파일을 다운로드하기 위한 네이티브 코드 구현을 제공하고 있습니다. 커스텀 네이티브 코드 플러그인을 작성하기 전에, 이용가능한 애셋 스토어 패키지들을 평가해 보실 것을 권합니다.


4.2.4.1.3. Custom Native Plugins


커스텀 네이티브 플러그인을 작성하는 것은 매우 시간이 많이 걸리는 작업이며 유니티에서 데이터를 다운로딩하기 위한 더욱 유연한 기법입니다. 프로그래밍 시간이 오래 걸리고 기술적 위험도가 높으므로, 이 기법은 다른 기법들이 애플리케이션의 요구사항을 만족시켜주지 못했을 때만 사용할 것을 권합니다. 예를 들어, 커스텀 네이티브 플러그인은 유니티의 C# SSL 지원이 없는 플랫폼( 윈도우즈, OSX, Linux )에서 애플리케이션이 SSL 통신을 해야만 하는 경우가 있습니다.


커스텀 네이티브 플러그인은 일반적으로 대상 플랫폼의 네이티브 다운로딩 API 에 대한 래퍼일 것입니다. iOS 의 NSURLConnection 과 안드로이드의 java.net.HttpURLConnection 이라는 예제가 있습니다. 이러한 API 를 사용하는 데 있어서의 세부사항을 원한다면 각 플랫폼의 네이티브 문서를 참고하십시오.


4.2.4.2. Storage


모든 플랫폼에서, Application.persistentDataPath 는 애플리케이션을 여러 번 실행하는 동안 지속적으로 유지되어야 하는 데이터를 저장하기 위해서 사용되어야 하는 쓰기가능한 위치를 가리킵니다. 커스텀 다운로더를 사용할 때는, Application.persistentDataPath 의 하위 디렉토리에 다운로드된 데이터를 저장할 것을 강력히 권합니다.


Application.streamingAssetPath 는 쓰기가능한 위치가 아니며, 애셋번들 캐시를 위해서는 좋지 않은 선택입니다. StreamingAssetsPath 가 포함하는 위치의 예는 다음과 같습니다:


    • OSX: .app 패키지 내; 쓰기 불가능.
    • Windows: 설치 디렉토리 내( 예를 들어 program files ); 보통 쓰기 불가능.
    • iOS: ipa 패키지 내; 쓰기 불가능.
    • Android: 압축된 .jar 파일 내; 쓰기 불가능.


4.3. Asset Assignment Strategies


프로젝트의 애셋을 애셋번들로 어떻게 나눌지를 결정하는 것은 쉽지 않은 문제입니다. 모든 오브젝트에 대해 각각의 애셋번들을 만들거나 하나의 애셋번들만 사용하는 것과 같은 단순한 전략을 취하고 싶은 충동을 느끼게 될 것입니다. 하지만 이러한 해결책은 심각한 단점들을 가지고 있습니다:


    • 너무 적은 애셋번들을 가지고 있습니다...
      • 런타임 메모리 사용량이 증가합니다.
      • 로딩 시간이 증가합니다.
      • Requires larger downloads

    • 너무 많은 애셋번들을 가지고 있습니다...
      • 빌드 시간이 증가합니다.
      • 개발이 복잡해집니다.
      • 전체 다운로드 시간이 증가합니다.


핵심 결정 사항은 오브젝트를 애셋번들에 어떻게 그룹화하느냐입니다. 주요 전략은 다음과 같습니다:


    • 논리적 요소들.
    • 오브젝트 유형들.
    • 동시성 칸텐트.


단일 프로젝트는 서로 다른 칸텐트 카테고리들을 위해 이들 전략을 섞을 수 있고 섞어야만 합니다. 예를 들어, 프로젝트는 UI 요소들을 서로 다른 플랫폼을 위한 애셋번들로 그룹화할 수 있습니다. 그러나 그것의 인터랙티브 칸텐트는 레벨이나 씬 단위로 그룹화합니다. 적용한 전략과 관계없이, 다음은 좋은 가이드라인이 됩니다:


    • 자주 갱신되는 오브젝트는 보통 변경되지 않는 오브젝트와는 다른 애셋번들로 나눕니다.
    • 동시에 로드될 것 같은 오븢게트는 함께 그룹화합니다.


예: 모델, 그것의 애니메이션, 텍스쳐.


    • 만약 오브젝트가 서로 다른 애셋번들 내에 있는 다수 개의 오브젝트들에 대한 종속성을 가지고 있다면, 그 애셋을 개별 애셋번들로 이동시키십시오.
      • 이상적으로 볼 때, 자식 오브젝트들을 그것의 부모 오브젝트들과 함께 그룹화하는 것이 좋습니다.
    • 만약 ( 텍스쳐의 HD 및 SD 버전 처럼 ) 두 개의 오브젝트들이 동시에 로드되는 것을 원하지 않는다면, 그것들을 개별 애셋번들로 나누십시오.
    • 만약 오브젝트들이 서로 다른 임포터 세팅이나 데이터를 가지고 있기 때문에 같은 오브젝트에 대한 서로 다른 버전이 된 것이라면, 애셋번들 Variants 를 대신 사용하십시오.


Once the above guideline are followed, 애셋번들 칸텐트의 50% 보다 적은 부분이 주어진 시간에 로드된다면 애셋번들을 분리하는 것을 고려하시기 바랍니다. 또한 동시에 로드되는 작은 애셋번들( 5 ~ 10 개 보다 적은 애셋 )들을 합치는 것을 고려하시기 바랍니다.


4.3.1. Logical entity grouping


논리적 엔터니 그룹화 전략에서는 오브젝트들이 그것들이 제공하는 프로젝트의 기능적 부분에 기반해서 그룹화됩니다. 이 전략에 따르면, 애플리케이션의 서로 다른 부분들은 서로 다른 애셋번들로 분리됩니다.


예:


    • UI 스크린을 위한 모든 텍스쳐와 레이아웃 데이터를 함께 번들로 묶습니다.
    • 캐릭터 셋을 위한 텍스쳐, 모델, 애니메이션을 함께 번들로 묶습니다.
    • 많은 레벨에서 공유되는 배경 조각들을 위한 텍스쳐와 모델들을 함께 번들로 묶습니다.


논리적 엔터티 그룹화는 가장 일반적인 애셋번들 전략입니다. 그리고 이는 다음과 같은 상황에 특히 적합합니다:


    • DLC( 역주 : Downloadable Content ).
    • 애플리케이션 생명주기 전반에 걸쳐 많은 곳에서 나타나는 엔터티들.


예:


    • 공통 캐릭터나 기본 UI 요소들.
    • 플랫폼이나 성능 세팅에 기반해서 다양해지는 엔터티들.


애셋을 논리적 엔터티에 의해서 그룹화하는 것의 장점은 변경되지 않은 칸텐트들을 다시 다운로드하지 않고도 개별 엔터티들을 쉽게 갱신할 수 있게 한다는 것입니다. 이것이 이 전략이 DLC 를 위해 특히 적합한 이유입니다. 이 전략은 대부분 메모리에 대해 효율적입니다. 왜냐하면 애플리케이션은 현재 사용중인 엔터티를 제출하는 애셋번들만을 로드할 필요가 있기 때문입니다.


그러나 이 전략은 구현하기 애매한 전략입니다. 왜냐하면 개발자들이 개별 오브젝트가 프로젝트에 의해서 언제 어떻게 사용되는지를 정확하게 알아야만 오브젝트들을 애셋번들에 할당할 수 있기 때문입니다.


4.3.2. Type Grouping


타입 그룹화는 가장 단순한 전략입니다. 이 전략에서, 비슷하거나 동일한 타입을 가진 오브젝트들이 같은 애셋번들에 묶입니다. 예를 들어, 여러 개의 서로 다른 오디오 트랙들이 애셋번들에 배치되거나, 여러 개의 서로 다른 언어 파일들이 애셋번들에 배치될 수 있습니다.


이 전략은 단순하지만, 빌드 시간, 로딩 시간, 업데이트 시간의 관점에서 봤을 때는 가장 비효율적입니다. 이 전략은 지역화 파일들처럼 작고 동시에 갱신되는 파일들을 위해서 자주 사용됩니다.


4.3.3. Concurrent content grouping


동시 칸텐트 그룹화는 오브젝트들이 동시에 로드되고 사용될 때 하나의 애셋번들로 묶는 전략입니다. 이 전략은 칸텐트가 매우 지역적인 프로젝트에서 보통 사용됩니다: 여기에서 칸텐트는 애플리케이션의 특정 위치나 시점에서 벗어나서 나타나지 않습니다. 예를 들어, 개별 레벨마다 유일한 아트, 캐릭터, 사운드 이펙트가 나오는 레벨 기반 게임을 들 수 있습니다.


동시-칸텐트 그룹화를 수행하기 위한 가장 일반적인 기법은 씬에 기반해 애셋번들을 생성하는 것입니다. 이 때 씬의 종속성을 거의 혹은 모두 포함하는 씬 기반 애셋번들을 사용합니다.


칸텐트가 매우 지역적이지 않거나 칸텐트가 애플리케이션 생명주기 동안에 다양한 위치에서 나타나는 프로젝트의 경우에는, 동시 칸텐트 그룹화는 논리적 엔터티 그룹화와 결합됩니다. 둘다 주어진 애셋번들의 칸텐트의 유용성을 최대화하기 위해서 필수적인 전략들입니다.


이 시나리오의 예는 오픈월드 게임입니다. 여기에서는 캐릭터들이 랜덤하게 스폰되며 월드 공간에 펴져 있습니다. 이 경우, 캐릭터가 동시에 어디에서 나타날지 예측하는 것은 쉽지 않습니다. 그래서 그것들은 일반적으로 서로 다른 전략을 사용해서 그룹화되어야 합니다.


4.4. Patching with AssetBundles


애셋번들을 패치하는 것은 새로운 애셋번들을 다운로드하고 그것을 현존하는 것과 교체하면 되기 때문에 매우 단순합니다. 만약 애플리케이션의 캐싱된 애셋번들을 관리하기 위해서 WWW.LoadCacheOrDownloadUnityWebRequest 가 사용되었다면, 이는 선택된 API 에 다른 version 매개변수를 넘기기만하므로 단순합니다( 더 많은 세부사항을 원한다면 스크립팅 레퍼런에 대한 위의 링크를 참고하세요 ). 


패칭 시스템에 있어서 해결하기 어려운 문제는 어떠한 애셋번들이 대체되어야 하는지를 찾는 것입니다. 패칭 시스템은 두 개의 리스트를 요구합니다:


    • 현재 다운로드된 애셋번들과 그것들의 버전 정보 리스트.
    • 서버 상의 애셋번들과 그것들의 버전 정보 리스트.


패쳐는 서버측의 애셋번들 리스트를 다운로드하여 애셋번들 리스트와 비교해야 합니다. Missing 애셋번들이나 버전 정보가 변한 애셋번들은 다시 다운로드되어야 합니다.


유니티 5 의 애셋번들 시스템은 빌드가 완료되었을 때 새로운 부가적인 애셋번들을 생성합니다. 이 부가적인 애셋번들은 AssetBundleManifest 오브젝트를 포함합니다. 이 매니페스트 오브젝트는 애셋번들의 리스트와 그것의 해시를 포함하고 있으며, 그것은 이용가능한 애셋번들의 리스트와 버전 정보를 클라이언트에 전송하기 위해서 사용됩니다. 애셋번들 매니페스트 번들에 대한 더 많은 정보를 원한다면 유니티 매뉴얼을 참고하세요.


애셋번들의 변화를 검색하는 커스텀 시스템을 작성하는 것도 가능합니다. 자신만의 시스템을 작성하는 대부분의 개발자들은 애셋번들 파일 리스트를 위해 JSON 같은 업계-표준 데이터 포맷을 사용하며, MD5 와 같은 체크섬을 계산하기 위해서 표준 C# 클래스를 사용합니다.


4.4.1. Differential patching


유니티 5 에서, 유니티는 결정론적인 방식으로 순서화된 데이터를 사용해 애셋번들을 빌드할 수 있습니다. 이는 커스텀 다운로더를 가진 애플리케이션이 differential patching( 역주 : 미분 패칭? ) 을 구현할 수 있도록 해 줍니다. 결정론적 레이아웃을 사용해서 애셋번들을 빌드하기 위해서는, BuildAssetBundleOptions.DeterministicAssetBundle 플래그를 BuildAssetBundles API 를 호출할 때 넘깁니다( 더 많은 세부 사항을 원한다면 스크립팅 레퍼런스 링크를 참고하세요 ).


유니티는 differential patching 을 위한 내장 메커니즘을 제공하지 않습니다. 그리고 WWW.LoadFromCachedOrDownload 나 UnityWebRequest 는 내장 캐싱 시스템을 사용할 때 differential patching 을 수행하지 않습니다. 만약 differntial pathcing 이 요구되면, 커스텀 다운로더를 작성해야 합니다.


4.4.2. iOS On-Demand Resources


온디맨드 리소스는 iOS 와 TVOS 장치에 칸텐트를 제공하기 위한 애플 API 입니다. 이는 iOS 9 장치에서 이용할 수 있습니다. 이것은 앱스토어 상에 런칭하는 데는 요구되지 않습니다만, TVOS 앱을 위해서는 요구됩니다.


애플의 온디맨드 리소스 시스템의 일반적인 개요는 애플 개발자 사이트에서 찾아볼 수 있습니다.


As of Unity 5.2.1, support for App Slicing and On-Demand Resources are both built upon another Apple system, Asset Catalogs. After registering a callback in the Unity Editor, the build pipeline for an iOS applications can report a set of files which will be automatically placed into Asset Catalogs and assigned specified On-Demand Resources tags.


A new UnityEngine.iOS.OnDemandResources API provides runtime support for retrieving and caching On-Demand Resources files. Once resources have been retrieved via ODR, they can then be loaded into Unity via the standard AssetBundle.LoadFromFile API.


For more details and an example project, see this Unity forum post.


4.5. Common Pitfalls


이 섹션은 애셋번들을 사용할 때 프로젝트에서 일반적으로 나타날 수 있는 몇 가지 문제들에 대해서 기술합니다.


4.5.1. Asset duplication


유니티 5 의 애셋번들 시스템은 오브젝트가 애셋번들에서 빌드될 때 오브젝트의 모든 종속성을 고려하지 않습니다. 이는 애셋 데이터베이스를 사용해서 수행됩니다. 이 종속성 정보는 애셋번들에 포함될 오브젝트 집합을 결정하는데 사용하는 정보입니다.


애셋번들에 명시적으로 할당된 오브젝트들은 그 애셋번들로 빌드되어야 할것입니다. 오브젝트의 AssetImporter 가 그것의 assetBundleName 속성을 비어있지 않은 문자열로 설정할 때 오브젝트는 "명시적으로" 할당됩니다. 이는 유니티 에디터에서 수행되는데, 오브젝트의 인스펙터나 에디터 스크립트로부터 애셋번들을 선택함으로써 수행될 수 있습니다.


애셋번들에 명시적으로 할당되지 않은 모든 오브젝트는 표시되지 않은 오브젝트를 참조하는 하나 이상의 오브젝트를 포함하는 모든 애셋번들들에 포함될 것입니다.


만약 두 개의 오브젝트가 서로 다른 애셋번들에 할당되었지만 둘다 공통 종속성 오브젝트를 참조하고 있다면, 그 종속성 오브젝트는 두 애셋번들에 모두 복사됩니다. 중복된 종속성이 인스턴스화될 것입니다. 이는 종속성 오브젝트에 대한 두 개의 복사본이 서로 다른 식별자를 가진 서로 다른 오브젝트로 고려된다는 것을 의미합니다. 이는 애플리케이션의 애셋번들의 전체 크기를 증가시킵니다. 또한 애플리케이션이 그것의 부모를 모두 로드한다면, 오브젝트의 서로 다른 복사본이 메모리에 로드될 것입니다.


이 문제를 해결하기 위한 두 가지 방법이 있습니다:


  1. 서로 다른 애셋번들에 빌드된 오브젝트가 같은 종속성을 가지지 않도록 합니다. 종속성을 공유하는 모든 오브젝트들은 자신의 종속성들을 복사하지 않고 같은 애셋번들에 배치될 수 있습니다.
    • 이 기법은 많은 공유 종속성을 가진 프로젝트에 대해서는 항상 실행가능한 것이 아닙니다. 이는 모놀리식 애셋번들을 생성하며, 그 애셋번들은 편의성과 효율성을 위해서 너무 자주 리빌드되거나 다시 다운로드되어야만 합니다.
  2. 애셋번들을 세그먼트화하면 종속성을 공유하는 두 개의 애셋번들이 동시에 로드되지 않을 것입니다.
    • 이 기법은 레벨 기반 게임과 같은 특정 유형의 프로젝트에 대해서 작동할 것입니다. 그러나 여전히 프로젝트의 애셋번들의 크기, 빌드 시간, 로딩 시간이 불필요하게 늘어납니다.
  3. 종속성 애셋이 자신만의 애셋번들에 빌드되도록 합니다. 이는 전체적으로 중복 애셋의 위험성을 제거합니다만, 복잡도를 증가시킵니다. 애플리케이션은 애셋번들 간의 종속성을 추적해서 AssetBundle.LoadAsset API 호출 전에 올바른 애셋번들이 로드되어 있도록 해야만 합니다.


유니티 5 에서, 오브젝트 종속성은 AssetDatabase API 를 통해 추적될 수 있습니다. 이는 UnityEditor 네임스페이스에 존재합니다. 네임스페이스가 내포하고 있듯이, 이 API 는 유니티 에디터에서만 사용할 수 있으며 런타임에는 사용할 수 없습니다. AssetDatabase.GetDependencies 는 특정 오브젝트나 애셋의 즉각적인 종속성을 모두 찾아내기 위해서 사용됩니다. 이 종속성들은 자신만의 종속성들을 가질 수도 있다는 점에 주의하십시오. 부가적으로, AssetImporter API 는 특정 오브젝트가 할당된 애셋번들을 질의하는데 사용될 수 있습니다.


AssestDatabase 와 AssetImporter API 를 혼용함으로써, 애셋번들의 직접적인 혹은 간접적인 종속성들을 모두 애셋번들에 할당하거나 애셋번들에 할당되지 않은 종속성을 애셋번들이 공유하고 있는지를 확인하는 에디터 스크립트를 작성하는 것이 가능합니다. 애셋이 중복되는 것의 메모리 비용 때문에, 모든 프로젝트에서 그런 스크립트를 제작하기를 권장합니다.


4.5.2. Sprite atlas duplication


다음 섹션은 자동으로 생성된 스프라이트 아틀라스를 결합했을 때 유니티 5 애셋 종속성 계산 코드의 특이함에 대해 기술합니다. 유니티 5.2.2p4 와 유니티 5.3 은 이 동작을 해결하기 위해서 패치되었습니다.


Unity 5.2.2p4, 5.3 and newer


자동으로 생성된 모든 스프라이트 아틀라스들은 스프라이트 아틀라스가 생성된 스프라이트 오브젝트를 포함하는 애셋번들에 할당될 것입니다. 만약 스프라이트 오브젝트가 다수개의 애셋번들에 할당되었다면, 스프라이트 아틀라스는 애셋번들에 할당되지 않고 중복되게 될 것입니다. 또한 만약 스프라이트 오브젝트가 애셋번들에 할당되지 않았다면, 스프라이트 아틀라스는 애셋번들에 할당되지 않을 것입니다.


스프라이트 아틀라스가 중복되지 않도록 하기 위해서는 같은 스프라이트 아틀라스라고 표기된 모든 스프라이트들이 같은 애셋번들에 할당되었는지 확인하십시오.


Unity 5.2.2p3 and older


Automatically-generated sprite atlases will never be assigned to an AssetBundle. Because of this, they will be included in any AssetBundles containing their constituent sprites and also any AssetBundles referencing their constituent sprites.


Because of this problem, it is strongly recommended that all Unity 5 projects using Unity's sprite packer upgrade to Unity 5.2.2p4, 5.3 or any newer version of Unity.


For projects that cannot upgrade, there are two workarounds for this problem:


    1. Easy: Avoid using Unity's built-in sprite packer. Sprite atlases generated by external tools will be normal Assets, and can be properly assigned to an AssetBundle.
    2. Hard: Assign all Objects that use automatically atlased sprites to the same AssetBundle as the sprites.


    • This will ensure that the generated sprite atlas is not seen as the indirect dependency of any other AssetBundles and will not be duplicated.
    • This solution preserves the simple workflow of using Unity's sprite packer, but it degrades developers' ability to separate Assets into different AssetBundles, and forces the re-download of an entire sprite atlas when any data changes on any component referencing the atlas, even if the atlas itself is unchanged.


4.5.3. Android textures


Due to heavy device fragmentation in the Android ecosystem, it is often necessary to compress textures into several different formats. While all Android devices support ETC1, ETC1 does not support textures with alpha channels. Should an application not require OpenGL ES 2 support, the cleanest way to solve the problem is to use ETC2, which is supported by all Android OpenGL ES 3 devices.


Most applications need to ship on older devices where ETC2 support is unavailable. One way to solve this problem is with Unity 5's AssetBundle Variants. (Please see Unity's Android optimization guide for details on other options.)


To use AssetBundle Variants, all textures that cannot be cleanly compressed using ETC1 must be isolated into texture-only AssetBundles. Next, create sufficient variants of these AssetBundles to support the non-ETC2-capable slices of the Android ecosystem, using vendor-specific texture compression formats such as DXT5, PVRTC and ATITC. For each AssetBundle Variant, change the included textures' TextureImporter settings to the compression format appropriate to the Variant.


At runtime, support for the different texture compression formats can be detected using the SystemInfo.SupportsTextureFormat API. This information should be used to select and load the AssetBundle Variant containing textures compressed in a supported format.


More information on Android texture compression formats can be found here.


4.5.4. iOS file handle overuse


이 이슈는 5.3.2p2 에서 해결되었습니다. 현재 버전의 유니티는 이 이슈의 영향을 받지 않습니다.


In versions prior to Unity 5.3.2p2, Unity would hold an open file handle to an AssetBundle the entire time that the AssetBundle is loaded. This is not a problem on most platforms. However, iOS limits the number of file handles a process may simultaneously have open to 255. If loading an AssetBundle causes this limit to be exceeded, the loading call will fail with a "Too Many Open File Handles" error.


This was a common problem for projects trying to divide their content across many hundreds or thousands of AssetBundles.


For projects unable to upgrade to a patched version of Unity, temporary solutions are:


    • Reducing the number of AssetBundles in use by merging related AssetBundles
    • Using AssetBundle.Unload(false) to close an AssetBundle's file handle, and managing the loaded Objects' lifecycles manually


4.6. AssetBundle Variant


A key feature of Unity 5's AssetBundle system is the introduction of AssetBundle Variants. The purpose of Variants is to allow an application to adjust its content to better suit its runtime environment. Variants permit different UnityEngine.Objects in different AssetBundle files to appear as being the "same" Object when loading Objects and resolving Instance ID references. Conceptually, it permits two UnityEngine.Objects to appear to share the same File GUID & Local ID, and identifies the actual UnityEngine.Object to load by a string Variant ID.


There are two primary use cases for this system:


  1. Variants simplify the loading of AssetBundles appropriate for a given platform.
    • Example: A build system might create an AssetBundle containing high-resolution textures and complex shaders suitable for a standalone DirectX11 Windows build, and a second AssetBundle with lower-fidelity content intended for Android. At runtime, the project's resource loading code can then load the appropriate AssetBundle Variant for its platform, and the Object names passed into the AssetBundle.Load API do not need to change.
  2. Variants allow an application to load different content on the same platform, but with different hardware.
    • This is key for supporting a wide range of mobile devices. An iPhone 4 is incapable of displaying the same fidelity of content as an iPhone 6 in any real-world application.
    • On Android, AssetBundle Variants can be used to tackle the immense fragmentation of screen aspect ratios and DPIs between devices.


4.6.1. Limitations


A key limitation of the AssetBundle Variant system is that it requires Variants to be built from distinct Assets. This limitation applies even if the only variations between those Assets is their import settings. If the only distinction between a texture built into Variant A and Variant B is the specific texture compression algorithm selected in the Unity texture importer, Variant A and Variant B must still be entirely different Assets. This means that Variant A and Variant B must be separate files on disk.


This limitation complicates the management of large projects as multiple copies of a specific Asset must be kept in source control. All copies of an Asset must be updated when developers wish to change the content of the Asset.


There are no built-in workarounds for this problem.


Most teams implement their own form of AssetBundle Variants. This is done by building AssetBundles with well-defined suffixes appended to their filenames, in order to identify the specific variant a given AssetBundle represents. Custom code programmatically alters the importer settings of the included Assets when building these AssetBundles. Some developers have extended their custom systems to also be able to alter parameters on components attached to prefabs.


4.7. Compressed or Uncompressed?


애셋번들을 압축할 것이냐 말 것이냐는 주의깊게 생각해 볼 주제입니다. 중요한 질문들은 다음과 같습니다:


    • 애셋번들의 로딩 타임이 중요한 요소입니까? 압축 안 된 애셋번들이 압축된 애셋번들보다 로컬 저장소나 로컬 캐시에서 로드하는데 훨씬 빠릅니다. 압축된 애셋번들을 원격 서버에서 다운로드할 대는 압축 안 된 애셋번들을 다운로드할 때보다 빠릅니다.
    • 애셋번들의 빌드 타임이 중요한 요소입니까? LZMA 와 LZ4 는 파일을 압축할 때 매우 느립니다. 그리고 유니티 에디터는 애셋번들을 직렬적으로 처리합니다. 많은 개수의 애셋번들을 포함하는 프로젝트는 그것들을 압축하는데 많은 시간을 소비하게 될 것입니다.
    • 애플리케이션 크기가 중요한 요소입니까? 만약 애셋번들이 애플리케이션에 포함된다면, 그것들을 압축하는 것이 전체 애플리케이션의 크기를 줄이는데 도움이 될 것입니다. 대안적으로, 애셋번들을 post-install 에 다운로드할 수도 있습니다.
    • 메모리 사용량이 중요한 요소입니까? 5.3 전에는 유니티의 압축해제 메커니즘이 압축해제를 하기 전에 전체 압축된 애셋번들을 메모리에 로드할 것을 요구했습니다. 만약 메모리 사용량이 중요하다면, 압축하지 말든가 LZ4 로 압축된 애셋번들을 사용하십시오.
    • 다운로드 시간이 중요한 요소입니까? 애셋번들이 크거나 사용자가 모바일 3G 나 low-speed meterred connections 와 같은 대역폭이 제한된 환경에 있다면 압축이 필요할 것입니다. 만약 몇 십 메가바이트의 데이터를 high-speed connections 를 사용하는 PC 로 전송하고 있다면, 압축을 배제해도 될 것입니다.


4.8. AssetBundles and WebGL


유니티는 WebGL 프로젝트에서는 개발자들이 압축된 애셋번들을 사용하지 말도록 강력히 권하고 있습니다.


As of Unity 5.3, all AssetBundle decompression and loading in a WebGL project must occur on the main thread. This is because Unity 5.3's WebGL export option does not currently support worker threads. (The downloading of AssetBundles is delegated to the browser via the XMLHttpRequest Javascript API, and will occur off of Unity's main thread.) This means that compressed AssetBundles are extremely expensive to load on WebGL.


With this in mind, you may want to avoid using the default LZMA Format for your AssetBundles and compress using LZ4 instead, which is decompressed very efficiently on-demand. If you need smaller compression sizes then LZ4 delivers, you can configure your web server to gzip-compress the files on the http protocol level (on top of LZ4 compression).

원문 : AssetBundle Fundamentals

주의 : 번역이 개판이므로 이상하면 원문을 참조하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



AssetBundle Fundamentals


확인 완료한 버전: 5.3 - 난이도: 고급


이것은 유니티 5 의 애샛, 리소스, 그리고 리소스 관리를 다루는 기사 시리즈의 네 번째 챕터입니다.


이 챕터에서는 애셋번들에 대해서 이야기합니다. 여기에서는 애셋번들이 빌드될 때까지의 기본적인 이스템과 함께 애셋번들과 상호작용하기 위해서 사용하는 핵심 API 들에 대해서 소개합니다. 특히, 애셋번들 자체를 로드하거나 언로드하는 것과 애셋번들로부터 특정 애셋과 오브젝트를 언로드하는 것에 대해서도 논의합니다.


애셋번들을 사용함에 있어서, 더 많은 패턴 및 best practices 를 원한다면 이 시리즈의 다음 챕터를 참조하십시오.


3.1. Overview


애셋번들 시스템은 하나 이상의 파일들을 유니티가 인덱싱할 수 있는 압축( archival ) 포맷으로 저장하는 기법을 제공합니다. 이 시스템의 목적은 유니티의 직렬화 시스템과 호환되는 데이터 전송 기법을 제공하는데 있습니다. 애셋번들은 설치 후에 non-code content 를 전송하고 갱신하기 위한 유니티의 주요 도구입니다. 이는 개발자들로 하여금 shipped asset size 를 줄이고, 런타임 메모리 압력을 최소화하고, 엔드 유저의 디바이스를 위해서 최적화된 칸텐트를 선택적으로 로드할 수 있도록 해 줍니다.


애셋번들이 동작하는 방식에 대해서 이해하는 것은 모바일 디바이스를 위해 성공적인 유니티 프로젝트를 빌드하기 위한 기초입니다.


3.2. What's in an AssetBundle?


애셋번들은 두 개의 부분으로 구성됩니다: 헤더와 데이터 세그먼트.


헤더는 애셋번들이 빌드될 때 유니티에 의해서 생성됩니다. 그것은 애셋 번들에 대한 정보를 포함하는데요, 그 정보로는 애셋번들의 식별자, 압축 여부, 매니페이스( manifest ) 등이 있습니다.


매니페스트는 오브젝트의 이름을 키로 사용하는 검색 테이블입니다. 각 엔트리는 주어진 오브젝트를 애셋번들의 데이터 세그먼트의 어느 부분에서 찾을 수 있는지를 가르쳐 주는 바이트 인덱스를 제공합니다. 대부분의 플랫폼에서, 이 검색 테이블은 STL std::multimap 으로 구현됩니다. 주어진 플랫폼에서 STL 구현에 의해 사용되는 특정 알고리즘은 다양하지만, 대부분은 균형잡힌 검색 트리( balanced search tree )를 사용합니다. Windows 와 OSX 에서 파생된 ( iOS 를 포함한 ) 플랫폼들은 red-black tree 를 사용합니다. 그러므로 매니페스트를 생성하기 위해 필요한 시간은 애셋번들 내부의 애셋들이 많아질 수록 비선형적으로 증가합니다.


데이터 세그먼트는 애셋번들 내의 애셋들을 직렬화함으로써 생성된 raw data 를 포함합니다. 만약 데이터 세그먼트가 압축되어 있다면, LZMA 알고리즘이 collective sequence of serialized bytes 에 적용된 것입니다 - 즉, 모든 애셋들은 직렬화되어 있으며, 전체 바이트 배열이 압축된 것입니다.


유니티 5.3 전에는 오브젝트들이 애셋번들 내부에 개별적으로 압축될 수 없었습니다. 결과적으로 5.3 전 버전에서는 압축된 애셋번들로부터 하나 이상의 오브젝트를 읽을 때, 유니티는 전체 애셋번들을 압축해제해야 했습니다. 일반적으로 유니티는 같은 애셋번들에서 발생하는 연속적인 로딩 요청을 최적화하기 위해서 애셋번들에 대한 압축해제된 복사본을 캐싱합니다.


유니티 5.3 은 LZ4 압축 옵션을 추가했습니다. LZ4 압축 옵션으로 빌드된 애셋번들은 애셋번들 내의 오브젝트를 개별적으로 압축하며, 이는 유니티가 압축된 애셋번들을 디스크에 저장할 수 있도록 허용합니다. 이는 유니티가 전체 애셋번들을 압축해제하지 않고도 개별 오브젝트를 압축해제할 수 있도록 해 줍니다.


3.3. The AssetBundle Manager


유니티는 Bitbucket 에 애셋번들 관리자의 구현에 대한 레퍼런스를 개발하고 유지합니다. 이 관리자는 이 챕터에서 세부적으로 설명하는 많은 개념들과 API 들을 사용하며, 애셋번들을 리소스 관리 워크플로우에 통합해야 하는 프로젝트들을 위한 유용한 시작점을 제공합니다.


주목할 만한 특징은 "simulation mode" 를 포함합니다. 유니티 에디터가 활성화되면, 이 모드는 애셋번들로 표시된( tagged ) 애셋들에 대한 요청을 프로젝트의 /Assets/ 폴더 내에 있는 원래 애셋으로 리디렉트( redirect )합니다. 이는 개발자로 하여금 애셋번들을 리빌드하지 않고도 프로젝트에서 작업할 수 있도록 해 줍니다.


애셋번들 관리자는 오픈소스 프로젝트이며, 여기에서 찾아볼 수 있습니다.


3.4. Loading AssetBundles


유니티 5 에서, 애셋번들은 네 개의 API 를 통해서 로드될 수 있습니다. 이 네 개의 API 들의 동작은 두 개의 조건에 의존해 달라집니다:


    1. 애셋번들이 LZMA 압축인가, LZ4 압축인가.
    2. 애셋번들이 로드되고 있는 플랫폼이 어디인가.


다음과 같은 API 들이 있습니다:




3.4.1. AssetBundle.LoadFromMemroyAsync


유니티는 이 API 를 사용하는 것을 추천하지 않습니다.


유니티 5.3.3 업데이트: 이 API 는 유니티 5.3.3 에서 이름이 변경되었습니다. 유니티 5.3.2( 및 이전 )에는, 이 API 가 AssetBundle.CreateFromMemory 로 알려져 있었습니다. 그것의 기능이 변경되지는 않았습니다.


AssetBundle.LoadFromMemoryAsync 는 관리되는 코드의 바이트 배열( byte[] in C# )에서 애셋번들을 로드합니다. 그것은 항상 소스 데이터를 관리되는 코드 바이트 배열에서 새롭게 할당된 연속된 네이티브 메모리 블록의 배열로 복사합니다. 만약 애셋번들이 LZMA 로 압축되었다면, 그것은 복사하는 동안 애셋번들을 압축해제합니다. 압축이 안 되어 있거나 LZ4 로 압축된 애셋번들은 말 그대로 복사됩니다.


이 API 에 의해 소비되는 메모리의 최대량은 적어도 애셋번들의 크기의 두 배입니다: 한 복사본은 API 에 의해 생성된 네이티브 메모리에 있고, 한 복사본은 API 에 넘겨진 관리되는 바이트 배열에 있습니다. 그러므로 이 API 를 통해 애셋번들로부터 로드된 애셋들은 메모리에 세 번 복사됩니다: 하나는 관리되는 코드 바이트 배열, 하나는 네이티브 메모리 복사본, 하나는 애셋 자체를 위해서 GPU 나 시스템 메모리에.


3.4.2. AssetBundle.LoadFromFile


유니티 5.3 업데이트: 이 API 는 유니티 5.3 에서 이름이 변경되었습니다. 유니티 5.2( 및 이전 )에서는 AssetBundle.CreateFromFile 로 알려져 있었습니다. 그것의 기능은 변경되지 않았습니다.


AssetBundle.LoadFromFile 은 매우 효율적인 API 이며, SD 카드나 하드 디스크와 같은 로컬 저장소에서 압축되지 않은 애셋번들을 로드하기 위한 의도로 만들어졌습니다. 만약 애셋번들이 압축되지 않았거나 LZ4 로 압축되었다면, 이 API 의 동작은 다음과 같습니다:


모바일 디바이스: 이 API 는 애셋번들의 헤더만을 로드하며, 데이터는 디스크에 남겨 둡니다. 애셋번들의 오브젝트는 로딩 메서드들이 호출되거나 그것들의 InstanceID 가 역참조될 때 요청에 의해( on-demand ) 로드될 것입니다. 이 시나리오에서는 과도하게 사용되는 메모리가 없습니다.


유니티 에디터: 이 API 는 마치 AssetBundle.LoadFromMemoryAsync 가 사용되거나 바이트들이 디스크에서 일어들여 지는 것처럼 전체 애셋번들을 메모리로 로드할 것입니다. 이 API 는 프로젝트가 유니티 에디터에서 프로우파일링되고 있는 동안에는 메모리 스파이크를 발생시킬 수 있습니다. 이는 디바이스 상에서의 성능에 영향을 미치지 않으며, 개선책을 취하기 전에 이 스파이크를 디바이스에서 다시 테스트해 보아야 합니다.


노트: 유니티 5.3 이전의 안드로이드 디바이스에서, 이 API 는 Streaming Assets 경로에서 애셋번들을 로드하려고 시도할 때 실패할 것입니다. 이는 그 경로의 내용이 압축된 .jar 파일 내부에 존재하기 때문입니다. 더 많은 세부사항에 대해서 알고자 한다면, AssetBundle usage patterns 챕터의 Distribution - shipped with project 섹션을 참고하십시오. 이 이슈는 유니티 5.4 에서 해결되었습니다. 유니티 5.4 이상에서 빌드된 게임에서는 이제 이 API 를 사용해 Streaming Asset 으로부터 애셋번들을 로드할 수 있습니다.


노트: AssetBundle.LoadFromFile 에 대한 호출은 LZMA 압축 애셋번들에 대해서는 항상 실패합니다.


3.4.3. WWW.LoadFromCacheOrDownload


WWW.LoadFromCacheOrDownload 는 원격 서버와 로컬 저장소에서 오브젝트를 로딩하기 위해서 유용한 API 입니다. 파일들은 file:// URL 을 사용해서 로컬 저장소로부터 로드될 수 있습니다. 만약 애셋번들이 유니티 캐시에 제출되었다면, 이 API 는 AssetBundle.LoadFromFile 과 정확히 같은 동작을 수행하게 됩니다.


만약 애셋번들이 아직 캐싱되지 않았다면, WWW.LoadFromCacheOrDownload 는 애셋번들을 소스로부터 읽어들일 것입니다. 만약 애셋번들이 압축되어 있다면, 그것은 워커 스레드( worker thread )를 사용하여 압축을 해제하고 그것을 캐시에 쓰게 됩니다. 그렇지 않다면, 그것은 워커 스레드를 통해 캐시로 바로 쓰여집니다.


일단 애셋번들이 캐싱되면, WWW.LoadFromCacheOrDownload 는 헤더 정보를 캐싱되고 압축해제된 애셋번들에서 로드합니다. 그리고 나서 이 API 는 AssetBundle.LoadFromFile 을 사용해 애셋번들을 로드하는 것과 동일한 동작을 수행합니다.


노트: 고정크기 버퍼를 통해 데이터가 압축해제되고 캐시에 쓰이는 동안, WWW 오브젝트는 애셋번들의 바이트의 전체 복사본을 네이티브 메모리에 유지합니다. 이 부가적인 복사본은 WWW.bytes 속성에 의해서 지속적으로 지원됩니다.


WWW 오브젝트 내에 애셋번들의 바이트를 캐싱하기 위한 메모리 오우버헤드 때문에, WWW.LoadFromCacheOrDownload 를 사용하는 개발자들은 자신들의 애셋번들을 작게 유지할 필요가 있습니다 - 거의 몇 메가 바이트 정도로. 또한 모바일 디바이스와 같은 제한된 메모리를 가진 플랫폼에서 작업하는 개발자들은 메모리 스파이크를 피하기 위해서 자신들의 코드가 한 번에 하나의 애셋번들만을 다운로드하도록 해야 합니다. 애셋번들 크기와 관련한 더 많은 논의를 원한다면, AssetBundle usage Patterns 챕터의 Asset assignment strategies 섹션을 참고하십시오.


노트: 이 API 에 대한 각각의 호출은 새로운 워커 스레드를 생성할 것입니다. 이 API 를 여러 번 호출할 때는 스레드가 너무 많이 생성되지 않도록 주의해야만 합니다. 만약 5 ~ 10 개 이상의 애셋번들이 다운로드되어야 한다면, 적은 개수의 애셋번들이 동시에 다운로드되도록 코드를 작성하는 것을 권장합니다.


3.4.4. AssetBundleDownloadHandler


유니티 5.3 에서 모바일 플랫폼을 위해 소개된 UnityWebRequest API 는 WWW API 를 대체할 더욱 유연한 대안을 제공합니다. UnityWebRequest 는 개발자로 하여금 유니티가 다운로드된 데이터를 어떻게 다룰지 정확하게 지정할 수 있게 합니다. 또한 개발자로 하여금 불필요한 메모리 사용량을 줄일 수 있도록 해 줍니다. UnityWebRequest 를 통해 애셋번들을 다운로드하는 가장 단순한 방법은 UnityWebRequest.GetAssetBundle API 를 호출하는 것입니다.


이 가이드의 목적을 볼 때 흥미로운 클래스는 DownloadHandlerAssetBundle 입니다. 다운로드 핸들러는 WWW.LoadFromCacheOrDownload 와 유사한 동작을 수행합니다. 그것은 워커 스레드를 사용해서 다운로드된 데이터를 고정 크기 버퍼에 스트리밍하고, 다운로드 핸들러의 구성에 따라 버퍼링된 데이터를 임시 저장소나 애셋번들 캐시에 스풀링합니다. LZMA 압축된 애셋번들은 다운로드 동안 압축해제되고 압축되지 않은 캐시로 저장됩니다.


이러한 모든 연산들은 네이티브 코드 상에서 수행되며, 관리되는 힙이 확장될 위험성을 제거해 줍니다. 더우기 이 다운로드 핸들러는 다운로드된 모드 바이트들에 대한 네이티브 코드 복사본을 유지하지 않습니다. 게다가 애셋번들을 다운로드하는 데 필요한 메모리 오우버헤드를 줄여줍니다.


다운로드가 완료되면, 다운로드 핸들러의 assetBundle 프라퍼티는 다운로드된 애셋번들에 대한 접근을 제공합니다. 마치 AssetBundle.LoadFromFile 이 다운로드된 애셋번들상에서 호출되는 것처럼 말이죠.


UnityWebRequest API 는 WWW.LoadFromCacheOrDownload 와 동일한 방식으로 캐싱을 지원하기도 합니다. 만약 캐싱 정보가 UnityWebRequest 오브젝트에 제공되고 요청된 애셋번들이 이미 유니티 캐시에 존재한다면, 애셋번들은 즉시 이용가능해지며 이 API 는 AssetBundle.LoadFromFile 과 동일하게 동작합니다.


노트: 유니티 애셋번들 캐시는 WWW.LoadFromCacheOrDownload 와 UnityWebRequest 사이에서 공유됩니다. 한 API 에 의해서 다운로드된 모든 애셋번들은 다른 API 를 통해서 사용할 수 있습니다.


노트: WWW 와는 다르게, UnityWebRequest 시스템은 내부 워커 스레드 풀과 내부 잡( job ) 시스템을 가지고 있으며, 이는 개발자가 동시에 너무 많은 번들을 다운로드할 수 없게 해 줍니다. 스레드 풀의 크기는 현재 사용자가 설정할 수 없습니다.


3.4.5. Recommendations


일반적으로, 가능하다면 AssetBundle.LoadFromFile 을 사용하십시오. 이 API 는 속도, 디스크 사용량, 런타임 메모리 사용량의 관점에서 가장 효율적입니다.


애셋번들을 다운로드하고 패치해야 하는 프로젝트를 위해서는, 유니티 5.3 이상의 버전을 사용하고 있다면 UnityWebRequest 를 사용하고 5.2 이하의 버전을 사용하고 있다면 WWW.LoadFromCacheOrDownload 를 사용하는 것을 강력히 권합니다. 다음 챕터의 Distribution 섹션에서 설명하듯이, it is possible to prime the AssetBundle Cache with Bundles include within a project's installer.


WWW.LoadFromCacheOrDownload 를 사용할 때는 프로젝트의 애셋번들이 프로젝트의 최대 메모리 예산보다 2 ~ 3% 정도 작은 상태로 유지되도록 하는 것을 추천합니다. 그래야 메모리 사용량 스파이크 때문에 애플리케이션이 종료되는 것을 막을 수 있습니다. 대부분의 프로젝트에서 애셋번들은 5 MB 를 넘어서서는 안되며, 1 ~ 2 개 이상의 애셋번들이 동시에 다운로드되어서는 안 됩니다.


WWW.LoadFromCacheOrDownload 나 UnityWebRequest 를 사용할 때는 애셋번들을 로드한 후에 다운로드 코드가 적절히 Dispose 를 호출하도록 해야 합니다. C# 의 using 문은 WWW 와 UnityWebRequest 를 안전하게 dispose 할 수 있는 가장 편리한 방법입니다.


규모가 큰 엔지니어링 팀을 위한 프로젝트와 단일 캐싱 이나 다운로딩 요구들을 위해서는 커스텀 다운로드가 필요합니다. 커스텀 다운로더를 작성하는 것은 사소한 엔지니어링 작업이 아닙니다. 그리고 모든 커스텀 다운로더는 AssetBundle.LoadFromFile 과 호환되도록 만들어져야만 합니다. 세부사항을 원한다면 다음 챕터의 Distribution 섹션을 참고하십시오.


3.5. Loading Assets From AssetBundles


UnityEngine.Object 는 AssetBundle 오브젝트에 있는 여러 API 를 사용해 애셋번들로부터 로드될 수 있습니다 : LoadAsset, LoadAllAssets, LoadAssetWithSubAssets. 이러한 모든 API 들은 비동기 버전을 가지고 있습니다 - Async 라는 접미어가 있습니다: LoadAssetAsync, LoadAllAssetsAsync, LoadAssetWithSubAssetsAsync.


동기 API 는 항상 비동기 API 보다 적어도 한 프레임 단위에서는 빠릅니다. 이는 특히 유니티 5.1 이전 버전에서는 사실입니다. 유니티 5.2 전에는 모든 비동기 API 들이 한 프레임에 거의 하나의 오브젝트만을 로드했스니다. 이는 LoadAllAssetsAsync 와 LoadAssetWithSubAssetAsync 가 관련 동기 API 들보다 심각하게 느렸음을 의미합니다. 이 동작은 유니티 5.2 에서 교정되었습니다. 비동기 로딩은 이제 자신의 시간 분할( time-slice ) 제한이 걸릴 때까지 한 프레임에 여러 개의 오브젝트를 로드합니다. 그것의 기반이 되는 기술적 요인과 시간 분할과 관련한 세부사항을 살펴보고자 한다면 아래의 Low-level loading details 를 참고하시기 바랍니다.


LoadAllAssets 는 독립적인 다수개의 UnityEngine.Object 들을 로드할 때 사용됩니다. 그것은 애셋번들 내의 대부분의( 혹은 모든 ) 오브젝트들이 로드될 필요가 있을 경우에 사용되야 합니다. 다른 두 API 와 비교해 봤을 때, LoadAllAssets 는 LoadAssets 를 개별적으로 호출했을 때 보다는 약간 빠릅니다. 그러므로 로드될 애셋의 개수가 많지만 한 번에 로드될 필요가 있는 개수가 애셋번들의 내용은 2/3 보다 적을 때는 애셋번들을 다수 개의 작은 번들로 분리하고 LoadAllAssets 를 호출할 것을 권합니다.


LoadAssetWithSubAssets 는 여러 개의 내포된 오브젝트를 포함하고 있는 복합 애셋을 로드해야 할 때 사용해야 합니다. 그러한 예로는 FBX 모델이 있는데, 거기에는 내포된 애니메이션이나 내부에 여러 개의 내포된 스프라이트를 포함하는 스프라이트 아틀라스를 포함하고 있을 수 있습니다. 만약 오브젝트들이 같은 애셋으로부터 로드될 필요가 있지만 애셋번들 내에 관련없는 다른 오브젝트들이 존재한다면, 이 API 를 사용하십시오.


다른 경우에는 LoadAsset 이나 LoadAssetAsync 를 사용하십시오.


3.5.1. Low-loevle loading details


UnityEngine.Object 로딩은 메인 스레드에서 수행됩니다: 오브젝트의 데이터는 워커 스레드에서 저장소로부터 읽혀들여집니다. 유니티 시스템의 스레드에 민감하지 않은 부분들( 스크립팅, 그래픽스 )을 건드리는 것들은 워커 스레드에서 변환됩니다. 예를 들어 VBO 는 메쉬로부터 생성되며, 텍스쳐들은 압축해제됩니다.


5.3 버전 전에는 오브젝트 로딩이 직렬적이며 오브젝트 로딩의 일부분은 메인 스레드에서만 수행되었습니다. 이는 "Integration" 이라 불렸습니다. 워커 스레드가 오브젝트 데이터를 로딩하는 것을 마친 후에, 그것은 새롭게 로드된 오브젝트를 메인 스레드에서 통합하기 위해 중지되었으며, 메인 스레드 통합이 완료될 때까지 ( 다음 오브젝트를 로딩하지 않고 ) 중지된 상태로 남아 있었습니다.


5.3 버전부터는 오브젝트 로딩은 병행처리됩니다. 여러 개의 오브젝트들이 역직렬화되며, 워커 스레드 상에서 처리되고 통합됩니다. 오브젝트가 로딩을 마쳤을 때, 그것의 Awake 콜백이 호출되며 오브젝트는 다음 프레임 동안에 유니티 엔진의 나머지 부분에 대해 이용 가능하게 됩니다.


동기화된 AssetBundle.Load 메서드는 오브젝트 로딩이 완료될 때까지 메인 스레드를 중단시킬 것입니다. 5.3 버전 전에는 비동기 AssetBundle.LoadAsync 메서드는 오브젝트를 메인 스레드에서 통합하기 전까지 메인 스레드를 중단시키지 않을 것입니다. They will also time-slice Object loading so that Object integration does not occupy more than a certain number of milliseconds of frame time. 얼마만큼의 시간이 설정되느냐는 Application.backgroundLoadingProperty 라는 프라퍼티에 의해 설정됩니다.


    • ThreadProirity.High: 프레임당 최대 50 밀리세컨드.
    • ThreadPriority.Normal: 프레임당 최대 10 밀리세컨드.
    • ThreadPriotity.BelowNormal: 프레임당 최대 4 밀리세컨드.
    • ThreadPriority.Low: 프레임당 최대 2 밀리세컨드.


유니티 5.1 전 버전에서, 비동기 API 들은 프레임당 한 오브젝트만 통합했을 것입니다. 이는 버그로 간주되었으며, 유니티 5.2 에서 해결되었습니다. 유니티 5.2 부터는 오브젝트 로딩을 위한 프레임 타임 제한에 도달할 때까지 여러 개의 오브젝트가 로드될 것입니다. AssetBundle.LoadAsync 는 ( 모든 요소가 동일하다는 가정하에서 볼 때 ) 동기 API 보다는 완료되는 시간이 길 것입니다. 왜냐하면 LoadAsync 호출을 제출하는 것과 오브젝트가 엔진에 대해 이용가능한 상태가 되는 것 사이에 최소 한 프레임의 지연이 존재하기 때문입니다.


실제 오브젝트들과 애셋들을 가지고 테스트해 보면 다른 결과가 나옵니다. 5.2 전에는 특정한 큰 텍스쳐를 로우엔드 디바이스에서 로드하면, 동기 메서드를 사용했을 때 7 ms 가 나오고 비동기 메서드를 사용했을 때 70 ms 가 나왔습니다. 5.2 이후에는 그 차이가 0 에 가까워졌습니다.


3.5.2. AssetBundle dependencies


유니티 5 의 애셋번들 시스테멩서 애셋번들 사이의 종속성은 두 개의 API 를 통해 자동적으로 트래킹되는데, 이는 런타임 환경과 관련이 있습니다. 유니티 에디터에서는 AssetBundle 종속성이 AssetDatabase API 를 통해 질의될 수 있습니다. 애셋번들 할당과 종속성은 AssetImporter API 를 통해서 접근되거나 변경될 수 있습니다. 런타임에는 유니티가 애셋번들 빌드 동안에 생성된 종속성 정보를 로드할 수 있는 선택적인 API 를 제공합니다. 이는 ScriptableObject 기반의 AssetBundleManifest API 입니다.


부모 애셋번들의 UnityEngine.Object 들이 다른 애셋번들의 UnityEngine.Object 들에 대한 하나 이상의 참조를 가질 때, 한 애셋 번들은 다른 애셋번들에 대해 "종속적"입니다. 오브젝트간 참조에 대한 더 많은 정보를 원한다면, Assets, Objects and Serialization 기사의 Inter-Object references 섹션을 참고하십시오.


그 기사의 Serialization and instances 섹션에서 기술햇듯이, 애셋번들은 애셋번들에 포함되어 있는 개별 오브젝트의 파일 GUID 와 로컬 ID 에 의해 식별되는 소스 데이터를 위한 소스로서 기능합니다


오브젝느는 인스턴스 ID 가 처음 역참조될 때 로드되기 때문에, 그리고 그것의 애셋번들이 로드될 때 오브젝트는 유효한 인스턴스 ID 를 할당받기 때문에, 애셋번들이 로드되는 순서는 중요치 않습니다. 대신에 오브젝트 자체를 로드하기 전에 오브젝트의 종속성을 포함하는 모든 애셋번들을 로드하는 것은 중요합니다. 유니티는 부모 애셋번들이 로드될 때 자식 애셋번들을 자동으로 로드해 주지 않을 것입니다.


예:


머티리얼 A 가 텍스쳐 B 를 참조하고 있다고 가정합시다. 머티리얼 A 는 AssetBundle 1 에 패키징되어 있고, 텍스쳐 B 는 AssetBundle 2 에 패키징되어 있습니다.




이 경우, Asset Bundle 2 는 Asset Bundle 1 으로부터 머티리얼 A 를 로딩하기 전에 로드되어야만 합니다.


이는 Asset Bundle 2 가 Asset Bundle 1  전에 로드되어야만 한다든가 그 텍스쳐 B 가 Asset Bundle 2 로부터 명시적으로 로드되어야만 한다든가 하는 것을 암시하지는 않습니다. Asset Bundle 1 로부터 머티리얼 A 를 로딩하는 것보다 Asset Bundle 2 를 먼저 로드하기만 하면 충분합니다.


유니티는 Asset Bundle 1 이 로드될 때 자동으로 Asset Bundle 2 를 로드하지 않을 것입니다. 이는 스크립트를 통해 명시적으로 수행되어야만 합니다. Asset Bundle 1 과 2 를 로드하기 위해서 사용되는 특정 애셋번들 API 는 중요하지 않습니다. WWW.LoadFromCacheOrDownload 를 통해 로드된 애셋번들은 AssetBundle.LoadFromFile 이나 AssetBundle.LoadFromMemoryAsync 를 통해 로드된 애셋번들과 자유롭게 혼용될 수 있습니다.


3.5.3. AssetBundle manifest


BuildPipeline.BuildAssetBundles API 를 통해 애셋번들 빌드 파이프라인을 실행할 때, 유니티는 애셋번들의 종속성 정보를 포함하는 오브젝트를 직렬화합니다. 이 데이터는 개별 애셋번들에 저장되며, 이는 AssetBundleManifest 타입의 오브젝트를 포함합니다.


이 애셋은 애셋번들이 빌드된 부모 디렉토리와 같은 이름을 가지고 애셋번들 내에 저장될 것입니다. 만약 프로젝트가 그것의 애셋번들을 (projectroot)/build/Client/ 라는 폴더에 빌드한다면, 매니페이스를 포함하는 애셋번들은 (projectroot)/build/Client/Client.manifest 로 저장될 것입니다.


매니페스트를 포함하는 애셋번들은 다른 애셋번들들처럼 로드되거나 캐싱되거나 언로드될 수 있습니다.


AssetBundleManifest 오브젝트는 그 자체로 GetAllAssetBundles 라는 API 를 제공해 매니페스트와 함께 빌드된 모든 애셋번들들을 리스팅할 수 있도록 합니다. 그리고 특정 애셋번들의 종속성을 질의하기 위한 두 개의 메서드를 제공합니다.


AssetBundleManifest.GetAllDependencies 는 애셋번들의 종속성을 모두 반환합니다. 이는 애셋번들의 계층구조상의 모든 자손들의 종속성을 포함합니다.


AssetBundleManifest.GetDirectDependencies 는 애셋번들의 바로 아래 자손들의 종속성만을 반환합니다.


이 API 들은 모두 문자열 배열을 할당한다는 점에 주의하시기 바랍니다. 자주 사용하지는 마십시오. 그리고 애플리케이션 생명주기의 성능이 중요한 시점에는 사용하지 마십시오.


3.5.4. Recommandations


애플리케이션에서 성능이 중요한 부분에 도달하기 전에 필요한 오브젝트를 최대한 많이 로드하는 것이 좋습니다. 예를 들어 메인 게임 레벨이나 월드에 들어 가기 전에 로드하는 것이 좋습니다. 이는 모바일 플랫폼에서 특히 중요합니다. 이 환경에서 로컬 저장소에 대한 접근은 느리며 플레이를 하는 동안에 오브젝트를 마구 로드하고 언로드하는 것은 가비지 콜렉팅을 발생시킬 수 있습니다.


애플리케이션이 상호작용을 하는 동안에 오브젝트들을 로드하고 언로드해야만 하는 프로젝트의 경우, 오브젝트와 애셋번들을 언로드하는 것에 대한 더 많은 정보를 원한다면 AssetBundle usgae pattern 기사의 Managing loaded assets 섹션을 참고하십시오.

원문 : Assets, Objects and Serialization.

주의 : 번역이 개판이므로 원문을 참조하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



Assets, Objects and Serialization


확인 완료한 버전 : 5.4 - 난이도 : 고급


이 기사는 유니티 5 의 애셋, 리소스, 그리고 리소스 관리를 다루는 기사 시리즈의 두 번째 챕터입니다.


이 챕터는 유니티의 직렬화 시스템의 내부 구현과 유니티 에디터와 런타임에서 서로 다른 오브젝트 간의 빠른 참조를 유지하는 방법에 대해서 다룹니다. 오브젝트와 애셋의 기술적인 차이에 대해서도 논의합니다. 여기에서 다루는 주제들은 유니티에서 애셋들을 효율적으로 로드하고 언로드하는 방법을 이해하기 위해서 필수적입니다. 적절한 애셋 관리는 로딩 시간을 짧게 유지하고 메모리 사용을 적게 하기 위해서 중요합니다.


1.1. Inside Assets and Objects


유니티에서 데이터를 적절하게 관리하기 위한 방법을 이해하기 위해서는, 유니티가 데이터를 식별하고 직렬화하는 방법을 이해하는 것이 중요합니다. 첫 번째 중요 지점은 애셋과 UnityEngine.Object 의 차이점입니다.


애셋은 디스크 상의 파일이며, 유니티 프로젝트의 Assets 폴더에 저장됩니다. 예를 들어, 텍스쳐 파일들, 머티리얼 파일들, FBX 파일들은 모두 애셋입니다. 어떤 애셋들은 머티리얼들처럼 유니티 포맷( formats native to Unity )들로 된 데이터를 포함합니다. FBX 파일과 같은 다른 애셋들은 자체 포맷( native formats )들로 처리될 필요가 있습니다.


대문자 'O' 로 시작하는 UnityEngine.Object( 혹은 Object ) 들은 리소스의 특정 인스턴스를 집합적으로 기술하는 직렬화된( serialized ) 데이터의 집합입니다. 이는 유니티 엔진이 사용하는 어떤 리소스 타입이든 될 수 있습니다. 그 예로는 메쉬( mesh ), 스프라이트( sprite ), 오디오 클립( AudioClip ), 애니메이션 클립( AnimationClip ) 등이 있습니다. 모든 오브젝트들은 UnityEngine.Object 기저 클래스의 서브클래스입니다.


대부분의 오브젝트 타입들은 내장 타입( built-in type )인데, 두 개의 특별한 타입들이 있습니다.


    1. ScriptableObject 는 개발자가 자신만의 데이터 타입을 정의할 수 있도록 해 주는 편리한 시스템을 제공합니다. 이 타입들은 유니티에 의해 자연스럽게 직렬화되고 역직렬화( deserialized )되며, 유니티 에디터의 인스펙터 윈도우에서 다뤄집니다.
    2. MonoBehaviourMonoScript 를 링크하는 래퍼( wrapper )를 제공합니다. MonoScript 는 유니티가 특정 어셈블리( assembly )나 네임 스페이스( namespace ) 내의 특정 스크립팅 클래스에 대한 참조를 유지하기 위해서 사용하는 내부 데이터 타입입니다. MonoScript 는 어떠한 실제 실행 코드도 포함하지 않습니다.


애셋과 오브젝트 사이에는 one-to-many 관계가 성립합니다; 즉 주어진 애셋 파일은 하나 이상의 오브젝트를 포함합니다.


1.2. Inter-Object References


모든 UnityEngine.Object 는 다른 UnityEngine.Object 들에 대한 참조를 가질 수 있습니다. 이 다른 오브젝트들은 같은 애셋 파일에 있을 수도 있고 다른 애셋 파일에 있을 수도 있습니다. 예를 들어 머티리얼 오브젝트는 항상 하나 이상의 텍스쳐 오브젝트에 대한 참조를 가집니다. 이 텍스쳐 오브젝트들은 일반적으로 ( PNG 나 JPG 같은 ) 하나 이상의 텍스쳐 애셋 파일들로부터 임포트( import )됩니다.


직렬화 시에, 이 참조들은 두 개의 개별 데이터 조각으로 구성됩니다: File GUIDLocal ID.  파일 GUID 는 대상 리소스가 저장되어 있는 애셋 파일을 식별합니다. 지역적으로 유일한 로컬 ID 는 애셋 파일 내의 각 오브젝트들을 식별합니다. 왜냐하면 애셋 파일은 여러 개의 오브젝트들을 포함할 수 있기 때문입니다.


파일 GUID 는 .meta 파일에 저장됩니다. 이 .meta 파일들은 애셋이 처음 임포트될 때 생성되며 애셋과 같은 디렉토리에 저장됩니다.


위의 식별자와 참조 시스템은 텍스트 에디터에서 확인할 수 있습니다: 새 유니티 프로젝트를 생성하고 그것의 에디터 셋팅을 Visible Meta Files 를 노출하고 애셋을 텍스트로 직렬화하도록 합니다. 그 프로젝트에서 머티리얼을 생성하고 텍스쳐를 임포트합니다. 그리고 씬에 있는 큐브에 머티리얼을 할당하고 씬을 저장합니다.


텍스트 에디터를 사용해, 위의 머티리얼과 연관된 .meta 파일을 엽니다. "guid" 라 표시된 라인은 파일의 거의 최상단에 보일 것입니다. 이 라인은 머티리얼 애셋의 파일 GUID 를 정의합니다. 로컬 ID 를 찾으려면, 텍스트 에디터에서 머티리얼 파일을 엽니다. 머티리얼 오브젝트의 정의는 다음과 같이 보일 것입니다:


--- !u!21 &2100000

Material:

 serializedVersion: 3

 ... more data ...


위의 예제에서, & 뒤에 나오는 숫자가 머티리얼의 로컬 ID 입니다. 만약 머티리얼 오브젝트가 파일 GUID "abcdefg" 에 의해서 식별되는 애셋 내부에 존재하면, 그 머티리얼 오브젝트는 파일 GUID "abcedfg" 와 로컬 ID "2100000" 의 조합에 의해 유일하게 식별될 것입니다.


1.3. Why file GUIDs and local IDs?



왜 유니티에서는 파일 GUID 시스템과 로컬 ID 시스템이 필요할까요? 그 대답은 신뢰성( robustness ) 및 유연하고 플랫폼 독립적인 워크플로우를 제공하기 위함입니다.


파일 GUID 는 파일의 특정 위치에 대한 추상화를 제공합니다. 특정 파일 GUID 가 특정 파일과 연관되어 있는 한, 그 파일의 디스크 상의 위치는 중요하지 않습니다. 그 파일은 파일을 참조하는 모든 오브젝트들을 갱신하지 않고도 자유롭게 이동될 수 있습니다.


애셋 파일은 다수 개의 UnityEngine.Object 리소스들을 포함할 수 있기 때문에( 혹은 임포트를 통해 생성할 수 있기 때문에 ), 로컬 ID 는 다른 오브젝트들과 명확하게 구분될 수 있을 것을 요구받습니다.


만약 애셋 파일과 연관된 파일 GUID 가 사라진다면, 그 애셋 파일 내부의 모든 오브젝트에 대한 참조도 사라질 것입니다. 이것이 .meta 파일이 그것이 연관된 애셋 파일과 같은 파일 이름을 가지고 같은 폴더에 저장되어야만 하는 이유입니다. 유니티는 제거되거나 위치가 잘못된 .meta 파일들을 자동으로 재생성한다는 점에 주의하십시오.


유니티 에디터는 알려진 파일 GUID 와 파일 경로에 대한 맵을 가지고 있습니다. 맵 엔트리는 애셋이 로드되거나 임포트될 때마다 기록됩니다. 맵 엔트리는 애셋의 특정 경로를 애셋의 파일 GUID 에 링크합니다. 만약 유니티 에디터가 .meta 파일이 사라졌지만 애셋의 경로가 변경되지 않았을 때 열리면, 에디터는 애셋이 같은 파일 GUID 를 유지하도록 해 줍니다.


만약 .meta 파일이 유니티 에디터가 닫히고 있을 때 사라지거나 .meta 파일은 그대로 두고 애셋 파일만 옮기면, 그 애셋 안의 모든 오브젝트에 대한 참조가 깨집니다.


1.4. Composite Assets and Importers


[ Inside Assets and Objects ] 에서 언급했듯이, non-native 애셋 타입들은 유니티에 임포트되어야만 합니다. 이는 애셋 임포터를 통해서 수행됩니다. 이 임포터들은 보통 자동으로 실행되지만, 그것들은 AssetImporter API 와 그것의 서브클래스를 통해 스크립트에 노출될 수도 있습니다. 예를 들어 TextureImporter API 는 PNG 나 JPG 와 같은 개별 텍스쳐 애셋을 임포트할 때 사용할 설정들에 대한 접근을 제공합니다.


임포트 절차의 결과는 하나 이상의 UnityEngine.Object 입니다. 이것들은 유니티 에디터에서 부모 애셋 내부의 다중 서브 애셋 형태로 가시화됩니다. 예를 들어 스프라이트 아틀라스로서 임포트된 텍스쳐 애셋 내부에 포함된 다중 스프라이트들이 있습니다. 각 오브젝트들은 같은 애셋 파일 내에 저장된 소스 데이터와 같은 파일 GUID 를 공유할 것입니다. 그것들은 임포트된 텍스쳐 애셋 내에서 로컬 ID 를 통해 구분될 것입니다.


임포트 절차는 소스 애셋을 유니티 에디터에서 선택한 대상 플랫폼에 맞는 포맷들로 변환합니다. 임포트 절차는 텍스쳐 압축과 같은 무거운 연산을 포함할 수 있습니다. 유니티 에디터가 열릴 때마다 임포트 절차가 수행되는 것은 매우 비효율적입니다.


대신에, 애셋 임포팅의 결과들은 Library 폴더로 캐싱됩니다. 특히, 임포트 절차의 결과는 폴더에 저장되는데, 폴더의 이름은 애셋 파일 GUID 의 처음 두 숫자입니다. 이 폴더는 LIbrary/metadata/ 폴더 내부에 저장됩니다. 개별 오브젝트들은 하나의 바이너리 파일로 직렬화되는데, 그것은 애셋 파일 GUID 와 동일한 이름을 가지게 됩니다.


이는 실제적으로 non-native 애셋들뿐만 아니라 모든 애셋에 대해 사실입니다. 그러나 native 애셋들은 장황한 변환 절차나 재직렬화를 요구하지 않습니다.


1.5. Serialization and Instances


파일 GUID 와 로컬 ID 가 신뢰성있기는 하지만, GUID 비교는 느리며 실시간에 좀 더 성능이 좋은 시스템이 필요합니다. 유니티는 내부적으로 파일 GUID 와 로컬 ID 를 단순한 정수로 변환하는 캐시를 유지합니다. 이 캐시는 싱글 세션에서만 유일합니다. 이들은 인스턴스 ID 라 불립니다. 그리고 새로운 오브젝트가 캐쉬에 등록될 때 단순한 점진적으로 증가하는 순서로 할당됩니다.


그 캐쉬는 주어진 인스턴스 ID( 오브젝트의 소스 데이터의 위치를 정의하는 파일 GUID 와 로컬 ID ) 와 메모리 상의 오브젝트 인스턴스 사이의 매핑을 유지합니다. 이는 UnityEngine.Object 들이 서로에 대한 참조를 신뢰성있게 유지할 수 있도록 해 줍니다. 인스턴스 ID 참조에 대한 resolving 은 인스턴스 ID 에 의해 표현되는 로드된 오브젝트를 빠르게 반환할 수 있습니다. 만약 대상 오브젝트가 아직 로드되지 않았다면, 파일 GUID 와 로컬 ID 는 오브젝트의 소스 데이터에 대해 resolve 될 수 있으며, 그 후에 유니티는 오브젝트를 제때( just-in-time ) 로드할 수 있습니다.


시작시에, 프로젝트에 내장된 모든 오브젝트( 예를 들어 씬에서 참조되는 )와 Resources 폴더에 포함된 모든 오브젝트를 위한 데이터를 사용해 인스턴스 ID 캐시가 초기화됩니다. 런타임에 새로운 애셋이 임포트되거나 애셋 번들로부터 오브젝트가 로드될 때 새로운 엔트리가 캐쉬에 추가됩니다. 인스턴스 ID 엔트리는 오브젝트가 무효화될( stale ) 때만 캐쉬에서 제거될 것입니다. 이는 특정 파일 GUID 와 로컬 ID 에 대한 애셋번들이 언로드될 때 발생합니다.


애셋 번들을 언로딩이 인스턴스 ID 를 무효화할 때, 메모리 보존을 위해 인스턴스 ID 와 그것의 파일 GUID 및 로컬 ID 사이의 매핑이 제거됩니다. 만약 애셋 번들이 다시 로드되면, 새로운 인스턴스 ID 가 다시 로드된 애셋 번들로부터 로드된 오브젝트를 위해서 생성될 것입니다.


애셋 번들을 언로드하는 것의 영향에 대한 더 깊은 논의를 원한다면, AssetBundle Usage Patterns 기사의 Managing Loaded Assets 섹션을 참조하십시오.


특정 플랫폼에서의 특정 이벤트는 오브젝트를 out of memory 상태로 이끌 수 있습니다. 예를 들어 앱이 중단되면( suspended ), iOS 상의 그래픽스 메모리로부터 그래피컬 애셋들이 언로드될 수 있습니다. 만약 이 오브젝트들이 언로드된 애셋 번들로부터 만들어진 것이라면, 유니티는 그 오브젝트를 위해 소스 데이터를 다시 로드하는 것이 불가능할 것입니다. 이러한 오브젝트들에 대한 현존하는 참조들도 무효화될 것입니다. 앞의 예에서, 안 보이는( 없어진 ) 메쉬나 마젠타( 없어진 ) 텍스쳐 & 머티리얼을 가진 것처럼 오브젝트가 렌더링 될 것입니다.


구현 노트 : 런타임에 위의 컨트롤 플로우는 문자 그대로 정확한 것은 아닙니다. 파일 GUID 와 Local ID 를 런타임에 비교하는 것은 무거운 로딩 연산 동안에는 충분히 효율적이지 못할 것입니다. 유니티 프로젝트에서 빌드를 할 때, 파일 GUID 와 Local ID 는 결정론적으로( deterministically ) 더 단순한 포맷으로 매핑됩니다. 그러나 그 개념은 동일하게 남아 있으며, 파일 GUID 와 로컬 ID 항에 대한 생각은 런타임 동안에 유용한 비유( analogy )로 남아 있게 됩니다.


이것은 애셋 파일 GUID 가 런타임에 질의될( queried ) 수 없는 이유이기도 합니다.


1.6. MonoScript


MonoBehaviour 가 MonoScript 에 대한 참조를 가지고 있고, MonoScript 는 단순히 특정 스크립트 클래스를 배치하기 위해서 필요한 정보만을 유지한다는 것을 이해하는 것은 중요합니다. 어떠한 유형의 오브젝트라도 스크립트 클래스의 실행 코드를 포함하지는 않습니다.


MonoScript 는 세 가지 문자열을 포함합니다: 어셈블리 이름, 클래스 이름, 네임스페이스.


프로젝트를 빌드할 때, 유니티는 모든 loose script file 을 Assets 폴더에서 수집해서 그것들을 모노 어셈블리로 컴파일합니다. 특히, 유니티는 Assets 폴더 내에서 사용된 각각의 다른 언어들을 위한 어셈블리를 빌드합니다. 또한 Assets/Plugins 폴더에 포함된 스크립트를 위한 독립된 어셈블리를 빌드합니다. Plugins 서브 폴더 외부의 C# 스크립트들은 Assembly-CSharp.dll 에 배치됩니다. Plugins 서브 폴더 내부의 스크립트들은 Assembly-CSharp-firstPass.dll 에 배치되는 식입니다.


( 미리 빌드된 어셈블리 DLL 들과 함께 ) 이 어셈블리들은 유니티 애플리케이션의 최종 빌드에 포함됩니다. 그것들은 MonoScript 가 참조하는 어셈블리이기도 합니다. 다른 리소스들과는 다르게, 유니티 애플리케이션에 포함된 모든 어셈블리들은 애플리케이션이 처음 시작될 때 로드됩니다.


이 MonoScript 오브젝트는 애셋번들( 혹은 씬 혹은 프리팹 )이 내부에 있는 MonoBehaviour 컴포넌트들 내의 어떠한 실행 코드도 포함하고 있지 않은 이유입니다. 이는 서로 다른 MonoBehaviour 들이 특정 공유 클래스나 심지어는 다른 애셋 번들에 존재하는 MonoBehaviour 들을 참조할 수 있도록 해 줍니다.


1.7. Resource LifeCycle


UnityEngine.Object 들은 특정 시점에 메모리에 로드되거나 메모리로부터 언로드됩니다. 로딩 시간을 줄이고 애플리케이션의 메모리 사용량을 줄이기 위해서는, UnityEngine.Object 들의 리소스 생명 주기에 대해서 이해하는 것이 중요합니다.


UnityEngine.Object 를 로드하는 두 가지 방법이 있습니다: 오브젝트는 자동으로 혹은 명시적으로. 역참조되고( dereferenced ) 있으며 현재 메모리에 로드되지 않았으며 오브젝트 소스 데이터가 정확한 위치에 있는 오브젝트에 인스턴스 ID 가 매핑되었을 때 자동으로 로드됩니다. 오브젝트는 그것들을 생성하거나 리소스 로딩 API( 예를 들어 AssetBundle.LoadAsset ) 를 호출함을써 스크립트를 통해 명시적으로 로드될 수 있습니다.


오브젝트가 로드될 때, 유니티는 각 참조의 파일 GUID 와 로컬 ID 를 인스턴스 ID 로 변환함으로써 모든 참조를 resolve 하려 시도합니다.


두 가지 조건만 만족한다면, 오브젝트는 자신의 인스턴스 ID 가 처음으로 역참조될 때 요구에 의해( on-demand ) 로드될 것입니다:


    1. 인스턴스 ID 가 현재 로드되지 않은 오브젝트를 참조할 때.
    2. 인스턴스 ID 가 캐시에 등록된 올바른 파일 GUID 와 로컬 ID 를 가질 때.


이는 일반적으로 참조가 스스로 로드되고 resolve 된 후에 매우 짧은 시간에 발생합니다.


만약 파일 GUID 와 로컬 ID 가 인스턴스 ID 를 가지고 있지 않거나, 언로드된 오브젝트를 가진 인스턴스 ID 가 유효하지 않은 파일 GUID 와 로컬ID 를 참조한다면, 그 참조는 보존되고 실제 오브젝트는 로드되지 않을 것입니다. 이는 유니티에서 "(Missing)" 참조라고 나옵니다. 실행중인 애플리케이션이나 씬뷰에서는 "(Missing)" 참조가 그것들의 유형에 따라 다른 방식으로 가시화될 것입니다: 메쉬들은 비가시화 상태가 되고, 텍스쳐들은 마젠타로 나타나는 식입니다.


오브젝트들은 세 가지 특정 시나리오에서 언로드됩니다:


    1. 오브젝트들은 사용되지 않는 애셋들을 클린업할 때 자동으로 언로드됩니다. 이 절차는 씬들이 파괴될 때( Application.LoadLevel API 를 non-additive 방식으로 호출할 때 )나 스크립트에서 Resources.UnloadUnusedAssets API 를 호출할 때 자동으로 발동합니다. 이 절차는 참조가 없는( unreferenced ) 오브젝트들만을 언로드합니다: Mono 변수가 그 오브젝트에 대한 참조를 가지고 있지 않거나 그 오브젝트에 대한 참조를 가진 살아 있는 오브젝트가 하나도 없을 때만 오브젝트가 언로드될 것입니다.
    2. Resources 폴더로부터 생성된 오브젝트들은 Resources.UnloadAsset API 를 호출함으로써 명시적으로 언로드될 수 있습니다. 이러한 오브젝트들의 인스턴스 ID 는 유효한 상태로 유지되며, 여전히 유효한 파일 GUID 와 로컬 ID 엔트리를 포함할 것입니다. 어떤 Mono 변수나 다른 오브젝트가 Resources.UnloadAsset 을 사용해 언로드된 오브젝트에 대한 참조를 가지고 있다면, 그 오브젝트는 살아 있는 참조가 역참조되자 마자 다시 로드될 것입니다.
    3. 애셋번들로부터 생성된 오브젝트들은 AssetBundle.Unload(true) API 를 호출할 때 자동으로 즉시 언로드됩니다. 이는 오븢게트의 InstanceID 의 파일 GUID 와 로컬 ID 참조를 무효화하며, 언로드된 오브젝트에 대한 살아 있는 모든 참조들은 "(Missing)" 참조가 될 것입니다. C# 스크립트에서, 언로드된 오브젝트에 대한 메서드나 프라퍼티에 접근하는 것은 NullReferenceException 을 생성하게 될 것입니다.


AssetBundle.Unload(false) 가 호출되면, 언로드된 애셋번들로부터 생성된 살아 있는 오브젝트들은 파괴되지 않을 것입니다. 하지만 유니티는 그것들의 인스턴스 ID 의 파일 GUID 와 로컬 ID 참조를 무효화시킬 것입니다. 만약 나중에 오브젝트가 메모리에서 언로드되고 언로드된 오브젝트에 대한 살아 있는 참조들이 유지되고 있다면, 이 오브젝트들을 유니티가 다시 로드하는 것은 불가능할 것입니다.


1.8. Loading Large Hierarchies


유니티 GameObject 의 계층을 직렬화할 때( 예를 들어 프리팹을 직렬화할 때 ), 전체 계층이 완전히 직렬화된다는 것을 기억하는 것이 중요합니다. 즉, 계층 내부의 모든 GameObject 와 Component 들이 개별적으로 직렬화된 데이터 내에 표현된다는 것입니다. 이는 GameObject 의 계층을 로드하고 인스턴스화하는데 필요한 시간에 흥미로운 영향을 미칩니다.


GameObject 계층을 생성할 때, CPU 시간이 몇 가지 방식으로 소비됩니다:


    1. 소스 데이터를 읽는 시간( 저장소로부터, 다른 GameObject 로부터 등 ).
    2. 새로운 Transform 사이에서 부모 자식 관계를 설정하는 시간.
    3. GameObject 와 Component 를 인스턴스화하는 시간.
    4. GameObject 와 Component 를 깨우는( awaken ) 시간.


나머지 세 비용은 일반적으로 계층이 현존하는 계층으로부터 복사되느냐 저장소( 예를 들어 애셋번들 )에서 로드되느냐와 관계없이 일정합니다. 그러나 소스 데이터를 읽는 비용은 계층에 직렬화된 Component 및 GameObject 의 개수에 따라 선형적으로 증가하며, 이는 데이터 소스의 속도와 곱해집니다.


현재 모든 플랫폼에서, 저장소 디바이스에서 로드하는 것보다는 메모리 어딘가에서 데이터를 읽어 들이는 것이 훨씬 빠릅니다. 더우기, 이용가능한 저장소 매체의 성능 특성은 각 플랫폼에서 매우 다양합니다 -- 데스크탑 PC 는 모바일 디바이스보다 훨씬 빠르게 디스크에서 로드합니다.


그러므로 느린 저장소를 가진 플랫폼에서 프리팹을 로드할 때, 저장소로부터 프리팹의 직렬화된 데이터를 읽는 비용은 프리팹을 인스턴스화하는 비용을 급격하게 넘어 갈 수 있습니다. 즉, 로딩 연산의 비용은 스토리지 I/O 시간에 의해 결정됩니다.


이전에 언급했듯이, monolithic prefab 을 직렬화할 때, 각 GameObject 와 Component 의 데이터는 개별적으로 직렬화됩니다 - 심지어 데이터가 중복되어 있더라도 그렇습니다. 30 개의 동일한 요소를 가진 UI 스크린은 동일한 요소를 30 번 직렬화할 것입니다. 이는 많은 바이너리 데이터 덩어리를 생성합니다. At load time, the data for all of the GameObjects and Components on each one of those thirty duplicate elements must be read from disk before being transferred to the newly-instantiated Object. 이는 파일 읽기 시간이며, 큰 프리팹을 인스턴싱하는 비용의 대부분을 차지합니다.


유니티가 내포된 프리팹( nested prefabs )을 지원하기 전까지는, 극단적으로 큰 GameObject 계층을 인스턴스화하는 프로젝트들은유니티의 직렬화 및 프리팹 시스템에 완전히 의존하기보다는,  재사용되는 요소들을 개별 프리팹으로 쪼개고 런타임에 그것을 인스턴스화함으로써 그들의 큰 프리팹의 로딩 비용을 많이 감소시킬 수 있었습니다.


더우기, 프리팹이나 GameObject 계층이 일단 생성되면, 새로운 복사본을 저장소에서 로드하는 것보다 현존하는 계층을 복사하는 것이 더 빠릅니다.


유니티 5.4 노트: 유니티 5.4 는 메모리에서 transform 에 대한 표현을 수정했습니다. 각 root transform 의 전체 자식 계층은 작고 연속적인 메모리 영역에 저장됩니다. 다른 계층으로 reparent 되는 새로운 GameObject 를 인스턴스화할 때는, parent 인자를 허용하는 GameObject.Instantiate 오우버로드( overloads ) 메서드를 사용하는 것을 골려하십시오.


이 오우버로드 메서드를 사용하면 새로운 GameObject 를 위한 root transform 계층을 할당하는 것을 피할 수 있습니다. 테스트를 해 봤을 때, 이것은 인스턴스화 연산을 위해 요구되는 시간을 5 ~ 10 % 정도 빠르게 만들어 줬습니다.


-- 각주 생략

모티브


 

Unity 3D 는 다수 개의 게임 인스턴스를 용납하지 않기 때문에 시뮬레이션이 필요한 에디팅을 할 때는 정말 짜증나는 경우가 많다. 예를 들어 캐릭터 애니메이션 에디터를 만든다고 하자.

 

보통의 상용 엔진이라고 하면 새로운 씬이나 게임 인스턴스를 생성하도록 한다. 예를 들어 UE4 같은 경우에는 editor game instance, play-in-editor( pie ) game instance, 일반적인 game instance 들이 따로 존재할 수 있는 구조이다. 그리고 각 게임 인스턴스들의 입력, 씬 구성 등은 모두 다르다. 그러므로 에디터 메인 뷰를 띄워 놓은 상태에서 페르소나 애니메이션 툴을 따로 띄워 놓고 내부적으로 시뮬레이션을 수행할 수 있다.

 

그러나 유니티는 그런거 절대 없다. 시뮬레이션이 필요하다면 반드시 플레이 버튼을 눌러야 하며 씬도 한 번에 하나밖에 못 띄운다( 콜백을 받아서 편집 상태에서 시뮬레이션하는 방법이 있다고 하는데, 아직까지 해 본 적이 없어서 잘 모르겠다 ). 일단 게임 인스턴스라는 개념조차 없는 것 같으니 대충 비슷하게라도 처리할 수 있는 방법을 찾고 있었다. 그러던 차에 5.3.X 버전부터 멀티 씬 편집을 지원한다는 사실을 알게 되었다( 유니티도 UE 와 같이 스트리밍을 하고 싶었나보다 ).

 

유니티는 현재 시점에는 한글 도움말에는 이것에 대한 정보 자체를 공개하지 않고 있다. 하지만 영문 도움말에는 나와 있다. 이상하게 영문 도움말과 한글 도움말의 카테고리 구성조차 다르다. 뭐하자는 건지 잘 모르겠다. 어쨌든 이 멀티 씬 개념을 사용하면 적어도 오브젝트 관리라도 따로 할 수 있지 않을까라는 생각이 들어서 정확하게 어떤 개념인지 알아 보기로 했다. 목표는 아래와 같다.

 

1. 에디터에서 원하는 시점에 다중 씬을 구성할 수 있는지 확인.

2. 특정 씬의 개체를 획득할 수 있는지 확인.

3. 특정 씬을 편집할 수 없도록 잠글 수 있는지 확인.

4. 특정 윈도우를 띄웠을 때 원하는 뷰에서 특정씬만을 볼 수 있는지 확인.

5. 특정 씬만 플레이할 수 있는지 확인.

 

씬 구성


 

테스트 프로젝트를 하나 만들어서 "Main" 과 "Customizer" 라는 씬을 만들었다. Main 에는 카메라와 빈 게임 오브젝트가 들어 있고, Customizer 에는 도형들이 들어 가 있다. 서로 위치가 겹치지 않게 하기 위해서 ( 원하는 뷰가 나오는지 확인하기 위해서 ), 조금 다른 위치에 배치했다.

 

툴의 "Open Scene Additively" 메뉴를 사용해서 화면을 열면 다음과 같은 결과를 얻을 수 있다.

 

 

두 개의 씬이 올라 와 있고 서로 다른 계층 구조를 가지고 있는 것을 확인할 수 있다. 현재는 Main 씬이 활성화되어 있다.

 

새 윈도우에 씬 렌더링하기


 

위와 동일한 결과를 획득하면서 새로운 윈도우에 Customizer 씬을 렌더링하는 테스트를 수행했다. 이를 위해서 Customizer 라는 EditorWindow 를 제작했다. 주석을 열심히 달아 놓았으니 의미를 파악하기는 어렵지 않을 것이라 본다.

 

 

이렇게 했을 때 다음과 같은 결과를 얻을 수 있었다. Customizer 씬이 활성화되어 있는 것을 볼 수 있다. 씬이 활성화되어 있다는 의미는 다음과 같다. 새로운 게임 오브젝트들은 기본적으로 활성화된 씬에 포함된다.

 

 

 

즉 활성화 여부만으로는 편집을 막거나 렌더링을 막을 수 없다. 실제로 위의 상태에서 메인씬에 3D 텍스트를 추가해 보았다. 다음과 같은 결과를 얻을 수 있었다.

 

 

결론


 

위의 테스트를 통해서 다음과 같은 결론을 얻을 수 있었다.

 

1. 에디터에서 원하는 시점에 다중 씬을 구성할 수 있는지 확인 : 원하는 시점에 다중 씬을 구성할 수 있다.

 

2. 특정 씬의 개체를 획득할 수 있는지 확인 : 현재( 5.3.1f ) 특정 씬의 개체를 획득할 수 인터페이스는 없었다Workaround 가 있다GameObject 가 scene 이라는 멤버 필드를 가지고 있으므로, 모든 게임 오브젝트에 대해 루프를 돌면서 확인할 수는 있다. 5.3.2 버전 부터는 Scene.GetRootGameObjects() 라는 메서드를 지원한다고 하니 그것을 이용할 수도 있다.

 

3. 씬을 편집할 수 없도록 잠글 수 있는지 확인 : 잠글 수 없다. Workaround 가 있다. 만약 모달 윈도우를 띄울 수 있다면 비슷하게 할 수는 있을 것 같다. 그러면 추가/삭제가 활성화된 씬에서 이루어질 것이기 때문읻. 유니티에서 모달 시스템 구현을 하는 방법에 대한 아티클들이 있으니 참고해 볼 수 있을 것 같다.

 

4. 특정 윈도우를 띄웠을 때 원하는 뷰에서 특정씬만을 볼 수 있는지 확인 : 원하는 뷰에서 볼 수는 있지만 특정 씬만을 볼 수는 없다. Workaround 가 있다. 이쪽은 아직 자세하게 몰라서 그런데, tag 나 layer 같은 것을 써서 렌더링 요소를 filtering 할 수 있다고 들은 것 같다. 막 나갈라면 그냥 모든 요소 돌면서 Customizer 씬의 요소가 아니면 렌더링을 꺼 버리는 수가 있다. 물론 복구를 잘 해야 하지만...

 

5. 특정 씬만 플레이할 수 있는지 확인 : 이것은 4 번 항목과 유사한 문제라 할 수 있다.

 

결과적으로 볼 때, ( 설계를 매우 잘 해야 하겠지만 ) 여러 가지 꼼수들을 사용하면 다중 게임 인터페이스가 있는 것과 유사한 행동을 하도록 구성할 수는 있을 것 같다.

 

참고로 버그를 하나 발견했는데, Customizer 윈도우를 띄우고 나서 플레이를 누른다음에 창을 닫으면 Customizer 씬이 없어지지 않는다. 아마도 플레이 동작에서 씬에 대한 레퍼런스를 하나 들고 있는 것이 아닌가 싶다.

+ Recent posts