AssetBundle 의 의존성은 직접 열어야 따라온다
프리팹 번들을 열어 Instantiate 했더니 오브젝트가 분홍색으로 나옵니다. 에러 로그는 없습니다. 에디터에서는 멀쩡하던 것이 번들에서 꺼내니 머티리얼이 빠진 것입니다.
AssetBundle 에서 다른 번들에 있는 에셋은 그 번들을 직접 열어야 따라옵니다. 엔진은 의존 관계를 알지만 대신 열어 주지 않습니다. 그 목록은 빌드가 남긴 AssetBundleManifest 에 있습니다.
다른 번들의 에셋은 그 번들이 열려야 이어진다
Cube 프리팹은 cubea 번들에, Cube 가 쓰는 머티리얼과 셰이더는 mat 번들에 나눠 넣었습니다. cubea 의 프리팹은 머티리얼을 직접 담지 않고 “mat 번들 안의 그 오브젝트” 를 가리키기만 합니다. 그 상태에서 cubea 만 열고 Cube 를 만든 뒤, 2프레임 뒤에 mat 을 열었습니다.
| 단계 | 인스턴스의 머티리얼 | 화면 가운데 픽셀 |
|---|---|---|
cubea 만 열고 Instantiate |
null |
rgb(209,0,209) 마젠타 |
mat 번들 연 뒤 |
Lit |
rgb(140,133,129) 정상 |
그 뒤 새로 Instantiate |
같은 Lit |
정상 |
이미 만든 인스턴스도 나중에 이어졌습니다. 프리팹을 다시 LoadAsset 하면 처음과 같은 객체가 돌아왔고, 그 프리팹의 머티리얼도 채워져 있었습니다. 끊긴 참조는 “대상이 없음” 으로 굳는 게 아니라 “아직 안 열린 번들의 그 오브젝트” 를 계속 가리키고 있다가, 번들이 열리면 이어집니다. 화면 결과는 GPU Resident Drawer 를 켜든 끄든 같았습니다.
그래도 mat 이 열리기 전 프레임에는 마젠타가 화면에 나갑니다. 고쳐진다고 순서를 무시해도 되는 것은 아닙니다.
의존 목록은 AssetBundleManifest 에 있다
번들을 빌드하면 출력 폴더 이름과 같은 매니페스트 번들이 하나 더 나옵니다. 그 안의 AssetBundleManifest 가 번들마다 기대는 번들 목록을 갖고 있습니다. 이 프로젝트에서 cubea 의 의존 목록은 mat 하나였습니다.
// 매니페스트 번들은 출력 폴더 이름과 같은 이름으로 나온다
AssetBundle manifestBundle = AssetBundle.LoadFromFile(Path.Combine(root, "Bundles"));
var manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 기대는 번들을 먼저, 끝까지 따라가서 연다
foreach (string dep in manifest.GetAllDependencies("cubea"))
Open(dep);
AssetBundle cube = Open("cubea");
GameObject prefab = cube.LoadAsset<GameObject>("Cube");
GetAllDependencies 는 의존의 의존까지 펼친 목록이고, GetDirectDependencies 는 바로 기대는 것만 줍니다. 열린 번들을 다시 LoadFromFile 하면 에러가 나니, 이미 연 번들은 이름으로 기억해 두고 다시 쓰는 도우미를 하나 둡니다.
readonly Dictionary<string, AssetBundle> m_Opened = new Dictionary<string, AssetBundle>();
AssetBundle Open(string name)
{
if (!m_Opened.TryGetValue(name, out AssetBundle bundle))
{
bundle = AssetBundle.LoadFromFile(Path.Combine(root, name));
m_Opened[name] = bundle;
}
return bundle;
}
여기서 AssetBundle 의 단위가 드러납니다. 의존은 번들 → 번들입니다. Cube 하나가 머티리얼 하나를 쓰더라도 mat 번들 전체를 열고, 내릴 때도 번들째입니다. 이 도우미에 “누가 이 번들을 쓰는지” 를 세는 기능까지 붙이면, 그것이 Addressables 가 대신 해 주는 일입니다.
직접 확인하는 테스트 코드
cubea 와 mat 을 나눠 빌드하고, mat 을 늦게 열어 이미 만든 인스턴스가 바뀌는지 봅니다.
// 에디터: Cube 와 그 머티리얼을 다른 번들에 넣는다
var builds = new[]
{
new AssetBundleBuild { assetBundleName = "cubea", assetNames = new[] { "Assets/Prefab/Cube.prefab" } },
new AssetBundleBuild { assetBundleName = "mat", assetNames = new[] { "Packages/com.unity.render-pipelines.universal/Runtime/Materials/Lit.mat" } },
};
var manifest = BuildPipeline.BuildAssetBundles("Builds/Bundles", builds,
BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows64);
Debug.Log(string.Join(", ", manifest.GetAllDependencies("cubea"))); // mat
// Player: 의존 번들 없이 만든 뒤, 늦게 연다
IEnumerator LateDependency(string root)
{
AssetBundle cube = AssetBundle.LoadFromFile(Path.Combine(root, "cubea"));
GameObject go = Instantiate(cube.LoadAsset<GameObject>("Cube"));
yield return null;
Debug.Log($"mat 전: {go.GetComponent<Renderer>().sharedMaterial}"); // null
AssetBundle mat = AssetBundle.LoadFromFile(Path.Combine(root, "mat"));
yield return null;
yield return null;
Debug.Log($"mat 후: {go.GetComponent<Renderer>().sharedMaterial}"); // Lit
}
Cube 가 쓰는 머티리얼이 프로젝트의 어느 파일인지는 프리팹의 머티리얼 GUID 로 확인합니다. URP 기본 큐브는 Assets 안의 머티리얼이 아니라 URP 패키지의 Lit.mat 을 쓰는 경우가 있어서, 다른 파일을 mat 번들에 넣으면 의존이 생기지 않습니다.
주의할 점
1. 의존 번들을 먼저 연다
안 지키면 생기는 문제. 의존 번들이 열리기 전에 만든 오브젝트는 그 사이 마젠타로 그려집니다. 나중에 이어지긴 하지만 한두 프레임이라도 화면에 나갑니다.
이렇게 쓴다. GetAllDependencies 로 받은 번들을 전부 연 뒤에 본 번들에서 에셋을 꺼냅니다.
2. 같은 번들을 두 번 열지 않게 기억해 둔다
안 지키면 생기는 문제. 이미 열린 번들을 LoadFromFile 로 다시 열면 실패합니다. 여러 번들이 같은 의존 번들을 기대면 이 일이 쉽게 생깁니다.
이렇게 쓴다. 연 번들을 이름으로 담아 두는 사전을 두고, 열기 전에 먼저 찾아봅니다. 내릴 때도 그 사전을 거쳐서, 다른 번들이 아직 기대고 있는지 확인합니다.
정리
AssetBundle 의 의존성은 번들 → 번들이고, 쓰는 쪽이 직접 열어야 따라옵니다.
- 의존 번들 없이 만든 오브젝트는 머티리얼이 비어 마젠타로 그려지고, 경고는 없습니다.
- 나중에 의존 번들을 열면 이미 만든 인스턴스의 참조도 이어집니다. 그래도 그 전 프레임은 화면에 나갑니다.
AssetBundleManifest.GetAllDependencies로 기대는 번들을 먼저 열고, 연 번들은 이름으로 기억해 둡니다.