AssetBundle 은 에셋을 Player 밖 파일로 꺼낸다
게임이 커지면 모든 에셋을 설치 파일 하나에 넣을 수 없게 됩니다. 스토어 용량 제한에 걸리고, 이벤트 맵 하나를 고치려고 앱 전체를 다시 심사받아야 하고, 쓰지도 않는 에셋이 처음부터 메모리를 차지합니다.
AssetBundle 은 에셋 일부를 Player 밖의 아카이브 파일로 빼내서, 따로 받고 따로 열고 따로 내리게 하는 장치입니다. 그 전에 유니티가 에셋을 Player 에 어떻게 싣는지부터 보면 왜 필요한지가 보입니다.
Player 빌드는 참조를 따라 에셋을 싣는다
Player 를 빌드하면 유니티는 Scene List 에 넣은 씬에서 출발합니다. 씬이 가리키는 프리팹, 프리팹이 가리키는 머티리얼, 머티리얼이 가리키는 셰이더와 텍스처를 차례로 따라가며 실을 것을 모읍니다. 따라가는 길은 에셋 파일에 저장된 GUID 와 fileID 입니다. GUID 는 .meta 에 적힌 “어느 파일인지”, fileID 는 “그 파일 안의 어느 오브젝트인지” 입니다.
그래서 어느 씬에서도 참조하지 않는 에셋은 프로젝트에 있어도 빌드에 안 들어갑니다. 예외가 하나 있습니다. Resources 폴더입니다.
Resources 폴더에 아무도 참조하지 않는 텍스트 파일 하나, Resources 밖에 똑같이 참조 없는 파일 하나를 두고 빌드해 빌드 리포트를 봤습니다. Resources 안의 것은 resources.assets 에 들어가 Player 에서 Resources.Load 로 읽혔고, 밖의 것은 빌드에 없었습니다.
Resources.Load("bg") 처럼 문자열로 찾기 때문입니다. 빌드는 어느 코드가 어떤 이름을 부를지 모르니 전부 실을 수밖에 없습니다. Unity 의 가이드는 Resources 에 대해 “쓰지 마라(Don’t use it)” 고까지 적습니다. 메모리를 세밀하게 관리하기 어렵고, 에셋이 많으면 시작할 때 인덱스를 만드느라 시간이 들고, 플랫폼별 콘텐츠나 부분 업데이트를 할 수 없다는 이유입니다.
AssetBundle 은 그 일부를 Player 밖으로 뺀다
매뉴얼은 AssetBundle 을 에셋을 묶어 두는 아카이브 파일이라고 정의하고, 쓰는 이유를 셋으로 듭니다.
- 필요할 때 받아 오기. DLC, 에피소드 업데이트, 라이브 서비스처럼 앱을 다시 내지 않고 콘텐츠를 더합니다
- 설치 크기 줄이기. 처음 설치 파일에는 꼭 필요한 것만 넣고 나머지는 나중에 받습니다
- 플랫폼에 맞추기. 플랫폼마다 번들을 따로 만들어 다른 플랫폼용 에셋을 싣지 않습니다
빌드는 “어느 에셋을 어느 번들에 넣을지” 를 적어 넘기면 됩니다.
var builds = new[]
{
new AssetBundleBuild { assetBundleName = "cube", assetNames = new[] { "Assets/Prefab/Cube.prefab" } },
new AssetBundleBuild { assetBundleName = "scene", assetNames = new[] { "Assets/Scenes/InDoor.unity" } },
};
BuildPipeline.BuildAssetBundles("Builds/Bundles", builds,
BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows64);
결과로 cube, scene 같은 번들 파일과 번들끼리의 의존 관계를 담은 매니페스트 번들이 나옵니다. 번들 안을 열어 보면 CAB-<해시> 라는 직렬화 파일과, 텍스처나 메시처럼 큰 데이터를 담은 CAB-<해시>.resS 가 들어 있습니다. Player 의 데이터 파일과 같은 형식을 아카이브 하나에 담은 것입니다.
런타임에는 파일을 열고, 안에서 에셋을 이름으로 꺼냅니다.
AssetBundle bundle = AssetBundle.LoadFromFile(path);
GameObject prefab = bundle.LoadAsset<GameObject>("Cube");
Instantiate(prefab);
// ...
bundle.Unload(true); // 번들과, 번들에서 꺼낸 에셋을 내린다
번들 하나가 받기, 열기, 내리기의 단위가 된다
빼낸 대가로 새 단위가 생겼습니다. 받는 것도, 여는 것도, 내리는 것도 번들 단위입니다. 에셋 하나를 쓰려 해도 그 에셋이 든 번들을 열어야 하고, 그 에셋의 머티리얼이 다른 번들에 있으면 그 번들도 열어야 합니다. 어떤 에셋을 어느 번들에 넣지 않으면, 그 에셋을 쓰는 번들마다 몰래 복사됩니다. 내릴 때도 번들을 통째로 내리거나 남겨 둬야 합니다.
그래서 AssetBundle 을 쓰는 일은 대부분 번들을 어떻게 나누고, 어떤 순서로 열고, 언제 내릴지 를 정하는 일이 됩니다. 매뉴얼이 저수준 API 대신 Addressables 를 권하는 이유도 이 관리 부담 때문입니다.
번들에 넣을 수 없는 것도 있습니다. 매뉴얼은 “AssetBundle 은 어셈블리를 담을 수 없다” 고 적습니다. 새 C# 클래스나 바뀐 코드는 번들로 배포할 수 없고, ScriptableObject 같은 직렬화된 데이터만 담깁니다.
직접 확인하는 테스트 코드
Resources 의 동작은 빌드 리포트로 확인합니다. 아래 에디터 스크립트는 Player 를 빌드하고 리포트에서 두 파일이 어디에 들어갔는지 찍습니다. Assets/Resources/Unreferenced.txt 와 Assets/Unused/NotInResources.txt 를 미리 만들어 두고, 둘 다 어디서도 참조하지 않습니다.
[MenuItem("Tools/Check Resources Packing")]
static void CheckResourcesPacking()
{
BuildReport report = BuildPipeline.BuildPlayer(new BuildPlayerOptions
{
scenes = new[] { "Assets/Scenes/SampleScene.unity" },
locationPathName = "Builds/Check/Game.exe",
target = BuildTarget.StandaloneWindows64,
});
foreach (string asset in new[] { "Assets/Resources/Unreferenced.txt", "Assets/Unused/NotInResources.txt" })
{
string where = "빌드에 없음";
foreach (PackedAssets packed in report.packedAssets)
foreach (PackedAssetInfo info in packed.contents)
if (info.sourceAssetPath == asset)
where = $"{Path.GetFileName(packed.shortPath)} ({info.packedSize} B)";
Debug.Log($"{asset}: {where}");
}
}
Resources 안의 파일은 resources.assets 로, 밖의 파일은 “빌드에 없음” 으로 나오면 같은 결과입니다.
주의할 점
1. Resources 에는 시작할 때 꼭 필요한 최소한만 둔다
안 지키면 생기는 문제. Resources 에 넣은 것은 아무도 안 써도 전부 설치 파일에 실리고, 문자열로 찾으니 빌드가 정리해 주지 못합니다. 나중에 번들로 옮기려 해도 어디서 그 이름을 부르는지 코드 전체를 뒤져야 합니다.
이렇게 쓴다. Resources 는 부트스트랩용 설정처럼 처음부터 끝까지 필요한 작은 것만 둡니다. 나머지는 씬 참조나 번들로 싣습니다.
2. 코드 변경은 번들로 내보낼 수 없다
안 지키면 생기는 문제. 번들에 새 컴포넌트를 붙인 프리팹을 넣어 배포하면, 설치된 Player 에 그 클래스가 없어서 컴포넌트가 빠진 채로 올라옵니다.
이렇게 쓴다. 번들로 내보낼 변경은 이미 Player 에 있는 클래스의 데이터만 바꿉니다. 새 스크립트가 필요한 변경은 앱 업데이트로 냅니다.
정리
AssetBundle 은 에셋 일부를 Player 밖 아카이브 파일로 빼서 따로 받고 따로 열고 따로 내리게 합니다.
- Player 빌드는 Scene List 에서 참조를 따라 싣고, Resources 폴더는 참조가 없어도 전부 싣습니다.
- 번들에는 데이터만 들어가고 코드는 못 들어갑니다. 안은 Player 데이터와 같은 직렬화 파일과
.resS입니다. - 대신 받기, 열기, 내리기가 모두 번들 단위가 되어, 번들을 나누고 여는 순서를 정하는 일이 생깁니다.