Unity/개념 및 분석

Unity AssetBundle 기본 개념

최애뎡 2021. 4. 26. 16:36
728x90
반응형

+ 저는 지금 2020 이상 버전을 사용 중인데 2018.2 이상 버전부터는 에셋 번들 관리자의 지원이 중단되었다고 합니다. 결국 Addressable asset system을 사용해야 하는데 asset bundle의 개념이 머리에 어느 정도 있어야 addressable을 제대로 사용할 수 있을 것이라 판단하여 작성합니다.

-AssetBundle-

- 말 그대로 asset들을 묶은 파일이다.

-사용 목적-

- 메모리 관리

- 초기 인스톨 사이즈를 줄이기 위함

[모바일 게임을 초기에 스토어에서 다운로드하고 나서 게임 시작 후 새 리소스를 다운로드하는데 새로이 받는 리소스들이 에셋 번들이라 생각하면 된다.]

- 런타임에 불러 사용 -> 컨텐츠 패치

=> 사실상 모바일에선 거의 무조건적으로 사용할 수밖에 없다.

잘 사용하면 여러 이점들이 있지만 잘못 사용하면 메모리에 심각한 문제를 야기할 수 있다.

나 같은 초심자의 경우 또는 보통 프로젝트 초기에는 볼륨이 작기 때문에 스크립트에서 public GameObject obj;를 선언하고 인스펙터 창에서 obj에 오브젝트를 할당하여 사용하곤 한다. 그러나 추후 프로젝트의 볼륨이 점점 커지면서 한 씬에 한 번에 참조된 오브젝트들이 많아지게 되면 메모리 문제가 생길 수 있다. [인스펙터 창에 오브젝트나 Prefab을 올려 사용하면 씬 데이터를 불러오는 시점에 메모리에 올리게 된다.]

이를 해결하기 위해 Resources 폴더를 사용한다.

나도 보통 그랬고 잘 모르면 조금 검색하다 바로 찾아볼 수 있기 때문이다.

사실 Resources 폴더는 이름으로 간편하게 접근할 수 있고 필요시에 로드할 수 있어서 효율적이기도 하다!

다만 메모리 관리가 복잡해지고 Resources 폴더 안에 data의 크기가 커지면 커질 수 록 앱의 시작 시간과 빌드 시간이 당연히 증가할 것이고 초기 빌드 용량 또한 커질 수밖에 없다. [Resources.Load를 사용할 때 리소스가 메모리에 올라간다.]

그렇기에 보통 빠른 프로토타입을 위해 사용하거나 정말 빈번하게 사용하는 몇 경우 정도(아이콘과 같은 작은 리소스)에 사용하기에 좋다. [Resources 폴더를 사용하면 또 한 가지 불행한 점이 있다...... Resources.Load를 사용할 때 에셋 이름으로 불러들이기 때문에 함부로 에셋의 이름을 변경하기가 참 힘들다.. 에셋의 이름을 바꾸면 코드를 또 바꿔줘야 하는 엄청난 불편함이 존재한다.(1~2개면 모를까 10개 이상만 돼도 끔 찍 하 다.)]

* Resources 폴더에 관해 잠시 언급

Unity에서도 Resources 폴더는 사용하지 말라고 권장한다. 이유야 여러 가지인데 가장 큰 이유는 당장 이 폴더에 들어가 있는 에셋들을 사용하지 않음에도 불구하고 메모리에 올라가기 때문에(처음 어플이 실행 시 Resources 폴더에 있는 내용들을 읽어드림) 앱의 시작 시간이 느려진다.(앱 시작 시 검정 화면 뜨는 시간이 길어짐)

이는 특히 모바일용 앱 개발에 큰 영향을 미칠 수밖에 없다.

요즘이야 스마트폰의 성능이 많이 좋아졌지만 그 래 도 안 된 다 . 당연한 것...

즉 test 할 때(개발 초기 -> 프로토타입을 구현할 때) 정도 사용하기에는 적절하다.

추후에는 당연히 AssetBundle, Addressable을 사용해야 한다.

[Addressable은 AssetBundle의 상위 버전이 아니라 AssetBundle을 조금 더 효과적으로 관리하고 사용할 수 있도록 도와주는 system이기 때문에 당연히 AssetBundle의 개념을 알고 있어야 한다.]

대략적으로 보면

Resources 폴더 -> 에셋 번들 교체(스토리지에서 에셋 번들을 로드하고 사용하고자 하는 오브젝트를 꺼냄) -> 런칭 시점이 다가올 때쯤에는 로컬 디스크에서 에셋 번들을 가져오는 게 아니라 웹에서 다운로드하는 방식으로 변경!

-에셋 번들의 구조-

Header(ID, 압축방식 등 + Lookup Table) + Data Segment(assets)

Lookup Table - data segment에 들어있는 asset들의 index 정보를 가지고 있다. (tree 구조)

-Data Segment의 압축 방식- (에셋 번들의 압축 방식, 물론 압축을 안 할 수도 있다.)

1. LZMA(None)

- Data Segment 안에 있는 모든 asset들을 하나로 묶어 하나의 파일로 만든다.

- 압축은 많이 되지만 비효율적

- 1번들 1에셋의 경우 적합

- 네트워크 대역폭이 문제일 시 적합

- 에셋 번들이 한번 사용돼서 압축이 풀리게 되면 그다음부터는 LZ4로 자동으로 압축이 풀리고 디스크에 캐싱 되기 때문에 최초 로딩이 오래 걸릴 수 있지만 그 뒤에는 LZ4의 효율로 돌아간다.

2. LZ4의 압축방식(chunkBasedCompression)

- asset들을 하나로 묶는 것이 아닌 여러 덩어리로 나누어서 저장한다.

[chunk는 작은 데이터 블록으로 생각하면 됨 -> asset이 하나의 블록보다 크면 여러 블록에 저장되는 방식 즉 분산돼서 압축되기 때문에 효율성이 올라간다.]

- 일반적인 권장 옵션

-의존성 (Dependency)-

예를 들어

A라는 번들에 3d 오브젝트 a가

B라는 번들에는 여러 Material들이

C라는 번들에는 여러 Animation들이

존재한다 가정할 때

A에 있는 오브젝트 a가 B, C 번들의 Material, Animation을 참조할 때 A 번들은 B, C 번들을 의존하게 된다.

이것을 의존성이라 한다.

이 상황에서 A, C 번들만 있고 B가 없을 경우 Material이 빠지기 때문에 a 오브젝트는 unity editor 상에서 보라색으로 나올 것이다.. 허허ㅋㅋ [당연하다.]

* 여기서 중요하게 파악해야 하는 점은 A 안에 있는 오브젝트를 제대로 사용하려면 A 번들만 로드가 돼야 하는 게 아니라 B, C도 로드가 되어야 한다는 점이다.

그렇다면 이것들은 코드로는 어떻게 표현하면 좋을까

번들의 manifest(번들에 관련된 정보들이 있다.)에 의존성 정보를 이용하여 의존성이 걸려있는 번들을 로드한다.

즉 꼭 써야 하는 필요한 번들만 로드해서 메모리 낭비를 줄인다.

물론 메모리 낭비도 에셋 번들을 제대로 사용했을 경우에나 해당한다.

의존성을 어떻게 구성하느냐는 에셋 번들에서 메모리를 위해 굉장히 중요하다.

예로

에셋 번들이 1, 2, 3로 3개 존재하고

1과 2 안에 각각 material이 하나씩 존재한다.

에셋 번들 3에는 texture이 하나 존재한다.

여기서 1, 2안에 있는 material이 에셋 번들 3에 있는 texture에 의존하고 있다면 에셋 번들 3을 로드하면 되지만

에셋 번들 3에 있는 texture가 번들화되지 않았다면 에셋 번들 1, 2가 로드됐을 때 각 번들 안에 있는 material 은 각각 texture을 가져와야 하는데 이 texture은 번들이 아니기 때문에 에셋 번들 1, 2에 있는 material은 각각 texture을 가진 체 번들이 로드된다. [texture가 1 나면 될 것을 2개가 메모리에 올라간다.]

+ 에셋 번들의 Variant 개념도 확실히 알고 넘어가야 합니다.

https://kukuta.tistory.com/192

 

Unity Asset Bundle

!!NOTE!! 유니티 2018.2 이상 버전 부터 AssetBundleManager의 지원을 중단한다는 공식 발표가 있었습니다. 2019.3 버전 부터 '어드레서블 에셋 시스템'이라는 새로운 에셋 관리 시스템이 추가 되며 , 2018 LTS

kukuta.tistory.com

- 이분이 정말 자세히 설명을 해주셨어요 :)

그렇다면 최적의 에셋 번들의 개수는?

[영상에서도 그랬지만 결국엔 현재 프로젝트의 상황과 개발자의 판단이 전부이다. 그래도 어느 정도의 기준은 있다.]

1. 너무 적은 에셋 번들을 가지고 있을 경우

- 실행 시 메모리 사용량이 증가할 수도 있다.(Lookup Table은 모든 Asset들을 지정하기 때문에)

asset이 많이 늘어나면 늘어날수록 에셋 번들의 Header가 커질 것이고 에셋 번들을 로드할 때 LZ4나 압축하지 않은 에셋 번들은 Header 부분만 가져오게 된다.(header만 메모리에 올림, 사실 이 부분은 최적화되어있기 때문에 메모리에 여러 개 올려도 상관없지만 그러면 안됨) 그런데 에셋 번들이 너무 적을 경우, 특히 극단적으로 번들을 하나만 만들었는데 이 안에 모든 에셋이 있는 경우를 가정하게 되면 Header의 크기가 극단적으로 커질 것이고 이게 메모리에 올라오게 될 것이다. 심지어 최초 로딩 시 한꺼번에 이 큰 헤더 파일이 올라오기 때문에 메모리 낭비가 생길 수 있다.

- 다운로드 양의 증가한다. (업데이트 시 불리)

실질적으로 업데이트를 위해 에셋 번들을 많이 사용하게 되는데 에셋 번들의 분류가 너무 적으면 번들 하나의 크기가 클 것이고 이러면 한번 다운로드할 때 걸리는 시간이 클 것이다.

2. 너무 많은 에셋 번들을 가지고 있을 경우

- 빌드 시간이 증가함

빌드 할 때마다 에셋 번들을 쭉 검사하게 되는데 번들이 많을 경우 당연히 빌드 시간이 증가한다.

-개발을 복잡하게 만들 수 있음

의존성을 생각해야 한다. 에셋 번들이 많아질 경우 당연히 의존성도 많아질 수밖에 없다. 번들에 들어있는 에셋을 로드할 때 의존하고 있는 다른 에셋 번들들도 미리 로드를 해야 하는데 의존성이 많을 경우 복잡도가 올라간다.

- 전체 다운로드 시간을 증가시킴

에셋 번들을 너무 많이 나눌 경우에는 추가적인 오버헤드 정보들이 많이 들어가기 때문에 전체 사이즈가 커지고 다운로드 시간을 증가시킨다.

위와 같은 문제들을 최대한으로 최적화하기 위해 그룹화를 권장한다.

-그룹화-

1. 논리적 그룹화

- UI, 캐릭터, 환경 등 논리적으로 묶을 수 있는 요소

(근데 웃긴 게 사실 말이 좋아 그룹화지 개발자가 판단을 잘해서 잘 나누는 게 중요하다.)

2. Downloadable Content(DLC)에 가장 적합

- 내가 필요한 부분만 바꿀 수 있기 때문에 적합함 이유는

예를 들어 1부터 100까지 컨텐츠가 있을 경우 각 레벨이 번들로 나누어 있다 가정할 때 추가가 되거나 각 레벨별로 바뀔 경우 각 레벨의 번들만 바꿔주면 된다.

3. 언제 어디서 사용될지 잘 알고 있어야 한다. (정확히 알고 있어야 함)

- 당연한 이야기겠지만 정확히 알아야 낭비 없이 불러서 사용할 수 있다.

2. 종류별 그룹화

- 오디오 트랙 또는 국가별 언어 파일 등 같은 타입 별 그룹화

- 동시 사용하는 컨텐츠별 그룹화 [예로 콘텐츠의 레벨을 나눈 것처럼 각 레벨에서 한 번에 동시에 부를 그런 것들]

- 각 레벨별 사용하는 모든 캐릭터, 텍스처, 음원을 하나로 그룹화

but 단점은 레벨 1, 2, 3에 공통적으로 사용하는 것도 따로 레벨 1, 2, 3으로 분류돼야 한다.

여기서 추가로 준 팁은

- 자주 변경되는 것과 변경되지 않는 것들을 분리

- 모델과 연관된 텍스처, 애니메이션 등을 그룹화

- 여러 에셋 번들이 참조하는 에셋은 공용 에셋 번들로 이동 (이게 중요한 듯)

[이게 무슨 말이냐면 번들이 A, B, C가 있고 A, B, C가 D를 참조할 때 D를 공용 에셋 번들로 빼버리는 게 좋단 소리!]

-절대 동시에 로드하지 않는 에셋들을 그룹화

-번들 내 50% 이하의 에셋이 같이 로드되지 않는 경우 분리를 고려

- 적은 수의 에셋을 가지며 자주 로드하는 에셋 번들은 통합을 고려

등등 여러 방식이 있지만 사실 개발하면서 그 프로젝트에 맞는 가장 적합한 방식을 개발자가 찾아서 잘 나누는 것이 중요한 듯하다.

AssetBundle 로드 API

- WWW.LoadFromCacheOrDownload

어떤 URL로부터 에셋 번들을 다운로드하고 그걸 로컬 스토리지에 캐싱 해서 저장한다.

여기서 만약에 번들이 압축되어 있으면 캐싱 될 때 번들의 압축을 풀어서 저장된다.

다만 에셋 번들을 다운로드할 때 데이터를 메모리에 계속 올려둔다. -> 번들 크기만큼 메모리에 올라가게 된다.

그렇기에 unity 5.3 이상에서는 UnityWebRequest 클래스를 사용하길 권장하는데 영상이 3년 전이라 그런 건지 5점대 버전이 있었네.. 뭐 하튼

UnityWebRequest 클레스는 www 클래스를 대체할 목적으로 만들어진 것이다.

다운로드하는 동안에 메모리 문제를 완화해 준다.

여기서 에셋 번들을 로드할 때 모바일과 에디터의 방식이 조금은 다른데

모바일의 경우는 에셋 번들의 헤더 정보만 메모리에 올리게 되고(실제 오브젝트를 로드하는 메서드를 부를 때 메모리에 오브젝트도 올라간다. 그렇다고 해서 모바일에서 모든 에셋 번들을 로드해 두면 안 됨 모바일 자체가 메모리가 넉넉지 않고 에셋 번들의 헤더 정보도 생각보다 메모리가 높기 때문이다. 특히 안드로이드가 아이폰보다 헤더 정보의 메모리가 더 높은 현상이 있다.) 에디터는 번들을 통째로 메모리에 올리게 된다.

즉 에셋 번들에 대한 메모리 프로파일링을 할 때 에디터에서 하는 게 의미가 없다.

반드시 타겟 기기에서 해봐야 하야한다.

이처럼 메모리 관리에 온 신경이 집중되어야 하는데 메모리 문제가 가장 많이 생기는 경우는 알고 보면 씬을 전환할 경우이다.

일단 씬전환의 사이클을 알아야 한다.

씬이 전환될 때 Resources.UnloadUnusedAsset()이 함수가 불리게 되고 사용되지 않은 리소스들이 제거가 된다.

씬이 전환될 때 유니티에서 내부적으로 자동적으로 이 함수를 호출한다.

그렇다면 이 함수가 내부적으로 불리는데 왜 메모리의 문제가 발생할까?

이유는 Resources.UnloadUnusedAsset()이 호출되는 시점에서 발생한다.

예로 Scene a가 로드되어 있고 Scene b를 로드해야 할 때

Scene a의 리소스를 제거하고 Scene b가 로드되는 것이 아니라

Scene b가 로드된 후에 Scene a의 리소스들이 제거된다는 점이다.

즉 중간에 Scene a와 b의 리소스들이 메모리에 같이 올라오는 순간이 존재한다.

그렇기에 중간에 로드하는 씬이 존재하는 것이다.

Scene a에서 Scene b를 로드할 때 Scene c (로드 화면만 띄어주는 메모리가 적은 간단한 씬)을 먼저 로드해서 scene a의 메모리를 먼저 내리고 Scene b를 로드하는 것!

즉 Scene a -> Scene c 로드 -> Scene a 메모리 내리고 -> Scene b 로드 -> Scene c의 메모리 내리고

요렇게 된다.

지금까지는 메모리를 로드하는 내용을 알아봤다.

메모리 관리를 위해 메모리를 로드했으면 당연히 Unload 또한 해주어야 한다.

AssetBundle.Unload - 번들 내의 모든 에셋을 언로드 한다.

언로드 함으로써 에셋 번들에 있느 오브젝트에 대한 메모리를 전부 해제한다.

만약 인자가 false로 둘 경우 에셋 번들 안에 있는 압축된 에셋의 파일 데이터는 언로드 되지만, 이 에셋 번들에 의해 로드된 실제 오브젝트는 그대로 유지된다. 당연히 이 에셋 번들에서 추가적으로 오브젝트를 로드할 수 없게 된다. true일 경우 이미 로드된 오브젝트도 포함되어 제거된다.

이는

에셋 번들 A가 있고 안에 m이라는 material이 있을 때 A를 로드 후 m을 로드하면 m은 A에서부터 파생되었다는 링크 정보를 가지게 된다. 즉 Unload를 true로 했다면 번들이 제거되면서 m도 같이 제거되는데, Unload를 false로 했다면 링크 정보가 끊기기 때문에 번들이 제거돼도 m은 메모리상에 올라가 있다.

여기서 다시 에셋 번들을 로드하고 m을 로드하면 A에서부터 로드된 m은 A에 연결되어 있지만 전에 제거하지 못했던 m은 다시 링크되지 않고 그대로 홀로 메모리에 남아있게 된다. [m이 메모리상에 2개가 있게 된다.]

-끗-

그닥 지금 시점에서까지 알 필요가 없는 내용들을 최대한 줄여가며 빼오다 보니 조금 애매하게 끝난 감이 있다.

위 내용은

https://www.youtube.com/watch?v=Lx61ZEKEvnQ&t=104s

+

https://www.youtube.com/watch?v=Z9LrkQUDzJw

+

구글링에서 찾은 여러 자료들을 바탕으로 작성하였습니다.

위 내용을 1시간 동안 정독하신다면 2시간의 youtube 영상의 내용과 구글링을 통해 나오는 내용들을 얻어 가시게 됩니다!

[얼 추 개 념 만]

반응형