Search Unity

애플리케이션에서 콘텐츠 전송 네트워크(CDN)로 에셋을 스트리밍하거나, 하나의 대용량 바이너리로 에셋을 한꺼번에 패킹해봤다면 에셋 번들에 대해 들어보셨을 겁니다. 에셋 번들은 직렬화된 에셋(텍스처, 메시, 오디오 클립, 셰이더 등)을 하나 이상 포함하며 런타임 시 로드할 수 있는 파일입니다.

에셋 번들은 직접 사용하거나 Unity 어드레서블 에셋 시스템(줄여서 어드레서블)과 같은 시스템을 통해 사용할 수 있습니다. 어드레서블 시스템은 프로젝트 내 에셋을 보다 편리하게 관리하는 방법을 지원하는 패키지로, 에셋 번들보다 상위 수준의 추상화를 제공합니다. 어드레서블은 개발자와 에셋 번들 간의 직접적인 상호작용을 최소화해주지만, 에셋 번들을 이용하는 경우 메모리 사용량에 어떤 영향이 있는지 이해할 필요가 있습니다. 어드레서블 시스템에 대해서는 블로그 포스팅유나이트 코펜하겐 2019 세션을 참조하세요.

신규 프로젝트를 진행하는 개발자는 에셋 번들을 직접 사용하는 대신 어드레서블을 사용할 것을 권장합니다. 또한 프로젝트에서 이미 에셋 번들 방식을 사용하고 있다면 이번 포스팅에서 소개해드리는 에셋 번들이 런타임 메모리에 미치는 영향을 참고하여 가장 이상적인 결과를 얻으시기 바랍니다.

메모리 캐시로 인한 높은 메모리 사용

Unity는 WWW 클래스(현재 지원 중단)또는 UnityWebRequestAssetBundle(UWR)을 사용하여 LZMA 에셋 번들을 다운로드할 때 메모리 캐시와 디스크 캐시를 이용하여 에셋 번들 가져오기, 재압축 및 버전화를 최적화합니다.

메모리 캐시로 로드된 에셋 번들은 메모리를 많이 소모합니다. 따라서 에셋 번들의 콘텐츠에 자주, 신속하게 액세스해야 하는 경우 외에는 권장하지 않습니다. 그 대신 디스크 캐시를 사용하세요.

UnityWebRequestAssetBundle API에 버전 또는 해시 인수를 제공하면 Unity가 에셋 번들 데이터를 디스크 캐시에 저장합니다. 인수를 제공하지 않는 경우 Unity는 메모리 캐시를 사용합니다. 어드레서블은 기본적으로 디스크 캐시를 사용합니다. 이 동작은 UseAssetBundleCache 필드로 제어할 수 있습니다.

AssetBundle.LoadFromFile()AssetBundle.LoadFromFileAsync()의 경우 LZMA 에셋 번들을 보관할 때 항상 메모리 캐시를 사용합니다. 따라서 되도록 UnityWebRequestAssetBundle API를 사용하도록 합니다. UnityWebRequestAssetBundle API 사용이 어려운 경우, AssetBundle.RecompressAssetBundleAsync()를 사용하여 디스크에서 LZMA 에셋 번들을 재작성할 수 있습니다.

내부 테스트 결과, 디스크 캐시를 사용하는 경우와 메모리 캐시를 사용하는 경우 RAM 사용량 차이가 최소 10배에 달하는 것으로 나타났습니다. 따라서 메모리 소모량과 추가로 필요한 디스크 공간 및 에셋 인스턴스화 시간 중 어느 것이 더 중요한지 잘 판단해야 합니다.

에셋 번들 메모리 캐시가 애플리케이션의 메모리 사용량에 주는 영향을 알아보려면 네이티브 프로파일러(여기서는 Xcode의 Allocations Instrument 사용)를 사용하여 ArchiveStorageConverter 클래스의 할당을 점검합니다. 이 클래스가 RAM을 10MB 이상 차지한다면 메모리 캐시를 사용하고 있을 가능성이 높습니다.

ArchiveStorageConverter 클래스의 메모리 사용량을 나타내는 Xcode의 Allocations Instrument

에셋 번들이 실제로 로드하는 요소 확인

대규모 프로젝트의 에셋 번들을 구축할 때는 Unity가 기본적으로 중복된 정보를 최소화한다고 가정하면 안 됩니다. 대신 유니티 컨설팅 및 개발팀에서 Python 언어로 작성한 AssetBundle Analyzer를 사용하여 생성된 에셋 번들의 중복 데이터 인스턴스를 편리하게 파악할 수 있습니다. 커맨드 라인을 통해 사용하는 이 툴은 생성된 에셋 번들에서 정보를 추출한 후, SQLite 데이터베이스에 저장하여 다양한 보기 방식을 제공합니다. 데이터베이스는 DB Browser for SQLite와 같은 툴을 사용하여 쿼리할 수 있습니다. 이 툴은 수동으로 또는 어드레서블을 통해 제작한 모든 번들에서 빌드 파이프라인의 비효율성 문제를 파악하고 해결하는 데 사용할 수 있습니다.

DB Browser for SQLite를 사용하여 AssetBundle Analyzer 툴의 결과를 분석하는 모습

또는 프로젝트에 바로 통합할 수 있는 AssetBundle Browser 툴다운로드해서 사용할 수도 있습니다. 이 툴은 어드레서블과 비슷한 기능을 지원하므로, 어드레서블을 사용하는 경우에는 해당되지 않습니다.

AssetBundle Browser 툴을 사용하면 개별 Unity 프로젝트의 에셋 번들 설정을 확인하고 수정할 수 있습니다. 이 툴은 빌드 기능을 비롯하여 종속성으로 인해 가져온 중복된 에셋(예: 텍스처)을 알려주는 등 유용한 기능을 지원합니다.

AssetBundle Browser 툴은 종속성으로 인해 두 개 이상의 번들에서 가져온 중복된 에셋에 대해 알려줍니다.

에셋 중복으로 인한 메모리 손실

에셋 번들에 에셋을 정리할 때는 종속성에 유의해야 합니다. Unity는 에셋 번들 토폴로지와 상관없이 (리소스 폴더에 있거나 리소스 폴더와 관련된) 애플리케이션 바이너리 내에 존재하는 에셋과 에셋 번들에서 로드해야 하는 에셋을 구분합니다. 이 두 에셋 유형은 서로 완전히 다른 세상에 존재한다고 표현할 수 있습니다. 예를 들어, ‘리소스 폴더 세상’에 존재하는 에셋의 인스턴스에 대해 강한 참조(hard reference)가 있는 에셋 번들을 생성할 수는 없습니다. 대신 Unity는 이러한 에셋을 참조하기 위해 ‘에셋 번들 세상’에서 사용할 수 있는 에셋 사본을 만듭니다.

두 컴포넌트에 ‘logo’ 이미지에 대한 강한 참조가 있습니다. 이러한 컴포넌트가 플레이어와 함께 하나의 번들로 묶이거나 에셋 번들에 포함되는 등 서로 다른 아카이브로 직렬화되는 경우, 각 컴포넌트에 이미지의 사본이 별도로 포함됩니다.

게임의 로고를 예로 들어보겠습니다. 로고는 게임이 시작되는 동안 로딩 씬의 UI에 표시됩니다. 이러한 로딩 화면은 원격 에셋이 디스크로 스트리밍되기 전에 표시되어야 하므로 로고 에셋을 즉시 사용할 수 있도록 빌드에 포함해야 합니다.

이 로고는 사용자가 언어, 사운드 환경 설정 및 기타 설정을 선택하는 데 사용하는 UI 옵션 패널에도 사용됩니다. 이 UI 패널이 에셋 번들에서 로드되면 해당 에셋 번들이 로고 에셋의 사본을 자체적으로 생성합니다.

로딩 화면과 옵션 패널이 동시에 로드되는 경우, 로고 에셋 사본이 중복으로 로드되므로 불필요한 메모리 소모가 발생합니다.

이 문제는 스크린 간의 하드 링크를 해제하는 방법으로 해결할 수 있습니다. 로고가 에셋 번들에 있는 경우, 일정량 이상의 스트리밍이 발생해야만 에셋에 대한 참조를 확보할 수 있습니다. 로고가 바이너리(예: 리소스 폴더)에 있는 경우, UI 패널이 로고 에셋에 대해 약한 참조(weak reference)를 보유하고 Resources.Load와 같은 API를 통해 로드되어야 합니다.

로고 이미지에 대한 약한 참조로 스트링이 사용되었습니다. 사용자 스크립팅에서 스트링을 사용해야만 런타임 시 이미지를 로드하고 적절한 컴포넌트에 할당할 수 있습니다.

사용자 스크립팅에서 스트링을 사용해야만 런타임 시 이미지를 로드하고 적절한 컴포넌트에 할당할 수 있습니다. 로고 에셋이 있는 에셋 번들을 애플리케이션의 StreamingAssets 디렉토리에 포함하는 방법도 좋은 절충안이 될 수 있습니다. 그러면 계속해서 에셋 번들에서 에셋을 로드하되, 번들을 로컬에서 호스팅하므로 콘텐츠 다운로드에 소요되는 시간을 절약할 수 있습니다.

에셋 참조에 스트링, 경로 또는 GUID를 사용하면 직관적이지 않지만, 약한 참조가 사용된 필드에는 Unity의 기본 드래그 앤 드롭 참조 기능을 활성화하는 커스텀 인스펙터를 만드는 것이 좋습니다. 그리고 반드시 Unity의 메모리 프로파일러 패키지를 사용하여 메모리에서 중복된 에셋을 식별하세요. 어드레서블 시스템은 자체적인 메커니즘에 기반하여 종속 관계에서 중복을 확인합니다. 자세한 내용은 기술 자료를 참조하세요.

마무리

어드레서블 시스템은 에셋 번들보다 높은 수준의 추상화를 제공하지만, 원리를 알아두면 위에서 설명한 것과 같은 심각한 성능 문제를 방지할 수 있습니다.

이 시리즈의 후속 포스팅과 로드맵을 준비하고 있습니다. 혹시 더 자세히 다루었으면 하는 분야가 있다면 댓글로 알려주시기 바랍니다.

 

14 replies on “메모리 사용량을 절감하는 에셋 번들 사용법”

I pay a quick visit daily a few websites and blogs to read
articles, but this blog provides feature based articles.

This might be a noob question but I can not find what will be included in the asset bundle. If I use the AssetBundle Browser tool to create a bundle then the only the prefab is bundled, not the FBX that this prefab is dependent on. Am I missing something? I was under the impression that ALL resources would be in the bundle?

Given that modern decompression can be so fast why not have an option for compressed in RAM. In theory you can use less ram and it will be so fast when used that it will probably be indistinguishable from direct RAM access or only a fraction slower?

** For Next Article **

Would be nice for you to demonstrate best practices for the advanced and complex use cases.

1) Adding additional game content to a published build, via addressables, without addressables
2) Updating existing game content on a published build, via addressables, without addressables,
3) How to solve the Pink Shader issue in Editor, via addressables, without addressables.
4) How to organize the assets of an AssetBundle, how to decide which assets should be separate AssetBundles, which should be in the same.
5) How to know in editor that some Assets were changed that are part of AssetBundles, which means that you need to reupload your AssetBundles.

Hello!
I find this post very useful since I am always using AssetBundles to dynamically load scenes in runtime.
But apparently there are conflict informations on which method to use.
This articles states this:

“AssetBundle.LoadFromFile() and AssetBundle.LoadFromFileAsync() always use the memory cache for LZMA AssetBundles. We therefore recommend using the UnityWebRequestAssetBundle API instead.”

But the documentation on the class UnityWebRequestAssetBundle.GetAssetBundle says otherwise:

“Note, that while you can use this API to load Asset Bundle from local storage (using file:// URI or jar:file// on Android), this is not recommended, use AssetBundle.LoadFromFileAsync instead.”

In my case, I am currently downloading the AssetBundles from StreamingAssets using the WebRequest class.
My question is: should I keep using the WebRequest method (as this article recommends), or should I switch to LoadFromFileAsync (as the documentation recommends)?

In short; Asset Bundles are garbage over which you have very little control.

They bundle hierarchy of references without telling you. Even with digging in some poor documentation, you would never figure out what exact type of bundles you need to use without running some profiling. Lot of undocumented thing; such if you try to load a type from a bundle, it loads EVERYTHING from the bundle. Don’t get me started on the stupid gymnastic needed to prevent shader duplication IN EVERY SINGLE BUNDLE! Or how Atlas don’t work with Asset Bundle out of the box.

Here’s some of the issues raised with them: https://forum.unity.com/threads/standard-shader-duplicated-in-asset-bundle-build.593248/#post-5020673

Addressables don’t fix any of the issues with Asset Bundles; it simply hides the issues under a layer of abstraction, making it even harder to figure out why something doesn’t work or eat so much memory.

It looks like AssetBundles and Addressables currently have some limitations. For example it would be nice if there would be exposed LZMA compression and extraction endpoints in a similar manner like dotnet uses for DeflateStream and GZipStream (i.e. by passing byte array or encoded string). This would allow to have generic, non-platform specific compressed assets plus it would give ability to create assets on runtime.

Great post! our teams are heavily using asset bundles already. a few questions:

1. We are manually downloading asset bundles (via WWW and then save them to disk using http://www.bytes), so we are not going through any of the asset bundle “specific” APIs – the disk cache is not used in these cases, correct ?

“When Unity downloads an LZMA AssetBundle using the WWW class (which is now deprecated) or UnityWebRequestAssetBundle (UWR), Unity optimizes the fetching, recompressing, and versioning of AssetBundles using two caches: the memory cache and the disk cache.”

I mean – this information is not relevant for our case at the moment.

2. “AssetBundles loaded into the memory cache consume a large amount of memory. Unless you specifically want to frequently and rapidly access the contents of an AssetBundle, the memory cache is probably not worth the memory cost. Instead, use the disk cache.”

Is there any way to avoid the memory cache then?

3. AssetBundle.LoadFromFile() and AssetBundle.LoadFromFileAsync() always use the memory cache for LZMA AssetBundles. We therefore recommend using the UnityWebRequestAssetBundle API

If all our bundles are using LZMA, will we benefit anything from moving to UnityWebRequestAssetBundle ?

Hey there!

I’ve run by your questions to one of our staff members who provided me with the following response:

1: Correct! The AssetBundleCache will not be used unless a cache version/hash is provided in the download.

2: The naming “memory cache” is rather misleading. This is simply decompression. If you have an LZMA AssetBundle, then it needs to fully decompress in memory as the entire file before any data can be used. If you are loading an LZ4 AssetBundle then it can decompress by chunks that cover the data blocks that you need. Uncompressed, it would not have any need to decompress into memory “cache” at all.

3: There could be some benefit in that it will recompress the AssetBundle. In this regard however, you can manage your own cache and use LoadFromFile. By using AssetBundle.RecompressAssetBundleAsync, using the two runtime enum LZ4/Uncompressed, you can mimic similar behaviour of using our Caching API. By recompressing an AssetBundle you receive from http://WWW.bytes to Uncompressed or LZ4 to manage how much decompress in memory is needed.

The assetbundles methods are very integrated into Unity and sometimes linked to versions of Unity and always by platform for things like Shaders. A better way might be to come up with independent cross-platform compatible assets – Textures (platform independant, convert on load or with copy per platform), Meshes, AudioClips, Shared Material (definition) library instead of shaders. This is less optimal but more flexible

The blog posting keep asking for a captcha but none exists

Perfect timing, I’m working on a live game for mobile right now, Addresables is a treasure! I easily implemented a remote config system for themes in the game, so for every occasion, the team can change UI and feel of the game without sending a forced update on the store.
However, I’m not completely sure if my system is robust and maintainable. More best practices and structural tutorials on addressables would be nice. <3

Comments are closed.