AssetBundle 의 의존성은 직접 열어야 따라온다
Esc

    AssetBundle 의 의존성은 직접 열어야 따라온다

    –Unity메모리

    프리팹 번들을 열어 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 로 기대는 번들을 먼저 열고, 연 번들은 이름으로 기억해 둡니다.