애셋번들 기초 (AssetBundle Fundamentals) 3 – 번역

애셋번들 기초 (AssetBundle Fundamentals) 3

– 원문 링크 –

 

애셋번들 기초 1
애셋번들 기초 2

3.5 애셋번들에서 애셋 로딩하기 (Loading Assets From AssetBundles)

애셋번들과 연결된 세 가지의 종류의 API를 사용해서, 애셋번들로부터 UnityEngine.Objects를 로드할 수 있습니다: LoadAsset, LoadAllAssets, LoadAssetWithSubAssets.
이 API들에 접미어 Async가 추가된 비동기 방식의 API들도있습니다: LoadAssetAsync, LoadAllAssetsAsync, LoadAssetWithSubAssetsAsync.

동기 방식의 API는 비동기 API에 비해서 적어도 한 프레임 빠릅니다. 특히 유니티 5.1 이전 버전에서 이 사실이 적용됩니다.
유니티 5.2 이전에는 모든 비동기 API들이 프레임 당 최대 하나의 오브젝트를 로드했습니다. 즉, LoadAllAssetsAsync와 LoadAssetWithSubAssetAsync는 동기 방식의 API에 비해서 상당히 느립니다.
이 동작은 유니티 5.2에서 수정되었습니다. 이제 비동기 방식의 로드를 통해서 프레임 당 time-slice의 최대치까지 여러 오브젝트를 로드할 수 있습니다.
이 동작 방식의 기술적인 이유 및 time-slicing에 대한 자세한 내용은 로우-레벨 로딩 섹션을 참고하시기 바랍니다.

LoadAllAssets는 독립된 여러 개의 UnityEngine.Objects를 로드할 때 사용하는 것이 좋습니다. 애셋번들 내의 대부분의 오브젝트(또는 모든 오브젝트)의 로드가 필요한 경우에만 사용해야 합니다.
다른 두 API에 비해서 LoadAllAssets는 LoadAssets를 여러번 호출하는 것보다 약간 빠릅니다.
따라서, 로드해야하는 애셋의 수가 많고, 한번에 로드해야하는 숫자가 애셋번들이 가진 전체 콘텐츠의 2/3 미만인 경우에, 애셋번들을 여러 개의 작은 번들로 분리하고 LoadAllAssets를 사용해서 로드하는 것을 고려해보는 것이 좋습니다.

애니메이션이나 스프라이트 아틀라스(다수의 스프라이트를 가진)를 사용하는 FBX 모델과 같이, 여러 내장 오브젝트를 포함하는 복합 애셋을 로딩하는 경우에 LoadAssetWithSubAssets를 사용하는 것이 좋습니다.
로드해야하는 모든 오브젝트가 동일한 애셋에서 비롯되었지만 해당 애셋번들에 관련없은 오브젝트가 다수 저장되어있는 경우, 이 API를 사용하시기 바랍니다.

다른 경우에는 LoadAsset 또는 LoadAssetAsync를 사용하시기 바랍니다.

 

3.5.1 로우-레벨 로딩 세부사항 (Low-level loading details)

UnityEngine.Object 로딩은 유니티의 메인 쓰레드에서 처리합니다: 오브젝트의 데이터는 작업 쓰레드(worker thread)의 저장소에서 읽습니다.
유니티의 쓰레드에 민감한 부분(스크립팅, 그래픽스)을 건들지 않는 모든 항목은 작업 쓰레드에서 변환됩니다.
예를 들어, VBO(Vertex Buffer Object)는 메쉬로부터 생성되고, 텍스쳐는 압축이 해제됩니다.

유니티 5.3 이전 버전에서는 오브젝트 로딩이 연속적으로 발생했으며 오브젝트 로딩의 특정 부분은 메인 쓰레드에서만 처리될 수 있었습니다. 이를 “통합(integration)”이라고 부릅니다.
작업 쓰레드에서 오브젝트의 데이터 로딩을 완료한 후에 메인 쓰레드에서 통합이 완료될 때까지 일시 중지(paused)상태로 남아있습니다(다음 오브젝트를 로드하지 않습니다).

유니티 5.3 부터는 오브젝트 로딩이 병렬 처리됩니다. 다수의 오브젝트의 역직렬화(deserialize), 처리, 통합을 작업 쓰레드에서 할 수 있습니다.
오브젝트 로딩이 완료되면, 이 오브젝트의 Awake 콜백이 호출되고 다음 프레임 동안에 Awake 이후부터 유니티 엔진의 LifeCycle의 나머지 부분까지 사용이 가능합니다.

동기 방식인 AssetBundle.Load 메소드는 오브젝트의 로딩이 완료될 때까지 메인 쓰레드를 일시중지 시킵니다.
유니티 5.3 이전 버전에서 비동기 방식인 AssetBundle.LoadAsync 메소드는, 메인 쓰레드에서 오브젝트가 통합해야 할 때까지 메인 쓰레드를 중단시키지 않습니다.
이들 역시 time-slice 오브젝트 로딩이기 때문에 오브젝트 통합이 프레임 시간에서 특정 밀리 세컨드(초)이상을 초과하지 않습니다.
이 밀리 세컨드 값은 Application.backgroundLoadingPriority 속성에 의해서 설정됩니다:

  • ThreadPriority.High: 프레임 당 최대 50밀리 초
  • ThreadPriority.Normal: 프레임 당 최대 10밀리 초
  • ThreadPriority.BelowNormal: 프레임 당 최대 4밀리 초
  • ThreadPriority.Low: 프레임 당 최대 2밀리 초

유니티 5.1 이전 버전에서는 비동기 방식의 API가 프레임 당 하나의 오브젝트만 통합합니다.
이 동작은 버그로 간주되어 유니티 5.2에서 수정되었습니다. 유니티 5.2 버전부터는 오브젝트 로딩에 사용되는 프레임 시간 제한에 도달할 때까지 여러 개의 오브젝트가 로드됩니다.
AssetBundle.LoadAsync는, LoadAsync 호출과 오브젝트가 엔진에서 사용될 수 있게되는 최소 한 프레임의 지연(delay) 때문에, 대응되는 동기 방식의 API보다 항상 작업을 완료하는데 시간이 더 오래 걸립니다(다른 모든 조건이 동일하다고 가정했을때).

실제 오브젝트와 애셋을 사용해서 테스트한 결과가 그 차이를 말해줍니다.
유니티 5.2 이전에는 로우-엔드 장치에서 큰 텍스쳐를 로딩할때 동기 방식으로는 7ms가 걸렸고 비동기 방식으로는 70ms가 필요했습니다.
유니티 5.2 이후 버전에서는 관찰되는 차이가 거의 0에 가까웠습니다.

 

 

3.5.2 애셋번들 의존성 (AssetBundle dependencies)

유니티 5의 애셋번들 시스템에서 애셋번들 간의 의존성(dependencies)은 런타임 환경에 따라서, 두 개의 다른 API를 통해서 자동으로 추적됩니다.
유니티 에디터에서는 AssetDatabase API를 통해서 애셋번들 의존성을 쿼리(요청)할 수 있습니다.
AssetImporter API를 통해서 애셋번들 할당 및 의존성에 접근하고 변경할 수 있습니다.
런타임(runtime) 시 유니티는 ScriptableObject 기반의 AssetBundleManifest API를 통해서 애셋번들 빌드 중에 생성된 의존성 정보를 로드할 수 있는 선택적 API를 제공합니다.

애셋번들은 부모 애셋번들의 오브젝트(UnityEngine.Objects)중에서 하나 이상이 다른 애셋번들의 오브젝트를 참조할때 다른 애셋번들에 “종속(dependent)”됩니다.
오브젝트 간 참조에 대한 자세한 내용은 애셋, 오브젝트, 직렬화 문서의 참조(reference) 섹션을 참고하시기 바랍니다.

직렬화 및 인스턴스 섹션에서 설명했듯이, 애셋번들은, 애셋번들에 포함된 각 오브젝트의 File GUID와 Local ID로 식별되는 원본 데이터의 소스(source) 역할을 합니다.

오브젝트는 Instance ID가 처음 참조가 해제될때 로드되고 애셋번들이 로드될 때 유효한 Instance ID가 오브젝트에 할당되기 때문에, 애셋번들이 로드되는 순서는 중요하지 않습니다.
그대신, 오브젝트 자체를 로드하기 전에, 해당 오브젝트의 의존성이 포함된 애셋번들을 모두 로드하는 것이 중요합니다.
유니티는 부모 애셋번들이 로드될 때 자식 애셋번들을 자동으로 로드하지 않습니다.

예제:

재질(Material)A가 텍스쳐 B를 참조한다고 가정합니다. 재질 A는 애셋번들 1에 묶여있고, 텍스쳐 B는 애셋번들 2에 묶여 있습니다.

assetbundle dependencies
이 경우, 애셋번들 1에서 재질 A를 로드하기 전에 애셋번들 2를 먼저 로드해야합니다.

이는 애셋번들 1보다 애셋번들 2를 먼저 로드해야 한다는 것이나 애셋번들 2에서 텍스쳐 B를 명시적으로 로드해야 한다는 것을 의미하지는 않습니다.
단지, 애셋번들 1의 재질 A를 로드하기 전에 애셋번들 2를 먼저 로드하면 됩니다.

유니티는 애셋번들 1이 로드될 때 자동으로 애셋번들 2를 로드하지 않습니다. 이 작업은 스크립트에서 수동으로 직접 처리해야합니다.
애셋번들 1과 2를 로드하는데 사용되는 API는 서로 다른 API를 사용해도 문제 없습니다.
WWW.LoadFromCacheOrDownload를 통해서 로드된 애셋번들은 AssetBundle.LoadFromFile 또는 AssetBundle.LoadFromMemoryAsync를 통해서 로드된 애셋번들과 같이 사용하는데 아무런 문제가 없습니다.

 

 

3.5.3 애셋번들 매니페스트 (AssetBundle manifests)

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

이 애셋은, 해당 애셋번들이 생성되는 상위 위치의 디렉토리 이름과 동일한 이름의 애셋번들에 저장됩니다.
어떤 프로젝트가 (projectroot)/build/Client/에 위치한 폴더에 애셋번들을 생성하면, 매니페스트(manifest)가 포함된 애셋번들은 (projectroot)/build/Client/Client.manifest로 저장됩니다.

매니페스트가 포함된 애셋번들은 다른 애셋번들처럼 로드하고, 캐시(Cache)에 저장 및 해제 가능합니다.

AssetBundleManifest 오브젝트 자체는 GetAllAssetBundles API를 통해서, 매니페스트와 동시에 생성된 모든 애셋번들을 나열하는 기능을 제공하고, 특정 애셋번들의 의존성(dependencies) 정보를 요청하는 두 개의 메소드를 제공합니다.

AssetBundleManifest.GetAllDependencies 메소드는 특정 애셋번들의 모든 의존성 정보를 반환하며, 여기에는 애셋번들의 자식 정보와 자식의 자식 등의 의존성 정보도 포함됩니다.

AssetBundleManifest.GetDirectDependencies 메소드는 특정 애셋번들의 바로 하위의 자식 정보만 반환합니다.

이 두 API는 모두 문자열 배열을 메모리에 할당합니다. 따라서 제한적으로 사용하고, 프로그램이 실행되는 동안에 성능에 민감한 부분에서는 사용하지 않는 것이 좋습니다.

 

3.5.4 권장사항 (Recommendations)

메인 게임의 레벨이나 월드와 같이, 사용자가 프로그램의 성능이 중요한 부분에 진입하기 전에, 가능한 많은 오브젝트를 로드하는 것이 가장 좋습니다.
이는 특히, 로컬 저장소에대한 액세스가 느리고, 플레이 시간에 메모리 로드 및 해제로 인한 가비지 컬렉터(garbage collector)의 메모리 수집 동작(부하가 매우 큼)이 발생할 수 있는 모바일 플랫폼에서 매우 중요합니다.

프로그램이 실행되는 동안 오브젝트를 로드 및 해제해야하는 프로젝트의 경우, 애셋번들 사용 패턴 글의 로드된 애셋 관리하기 섹션에서 오브젝트와 애셋번들의 해제에 대한 더 자세한 내용을 참고하시기 바랍니다.

 

내용 끝까지 읽어주셔서 감사합니다.
배너 클릭은 저에게 많은 힘이 됩니다.
감사합니다 🙂

 

RonnieJ

프리랜서 IT강사로 활동하고 있습니다. 게임 개발, 웹 개발, 1인 기업, 독서, 책쓰기에 관심이 많습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다