【Unity】Scriptable Build Pipelineで同じアセットバンドルに含まれるアセット一覧を取得する

UnityのScriptable Build Pipelineであるアセットと同じアセットバンドルに含まれるアセット一覧を取得する方法をまとめました。

Unity2020.1.10
Scriptable Build Pipeline 1.7.3

はじめに

Scriptable Build Pipeline(SBP)を使うとアセットバンドルのビルドフローを自由に構築することができます。
SBP自体の説明は以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

さてSBPがアセットバンドルをビルドする際には、あるアセットが参照しているアセットを参照元のアセットと同じアセットバンドルに格納します。
この時には当然内部的にあるアセットから参照するアセット一覧を検索しているのですが、この参照検索方法はちょっと特殊です。
エディタで参照検索する方法としては以下のようなAssetDatabase.GetDependenciesEditorUtility.CollectDependenciesがあります。

light11.hatenadiary.com

しかしこれらを使ってもSBPが行っている参照検索と同じ結果は得られません。
そのためこの記事ではSBPを使ってあるアセットと同じアセットバンドルに含まれるアセットの一覧を取得してみます。

実装する

実は今回やりたいこととほぼ同じことを行っているツールがAddressableアセットシステムに用意されています。

light11.hatenadiary.com

これのBuild Bundle Layoutの実装を参考にして実装をしたのが以下です。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Pipeline;
using UnityEditor.Build.Pipeline.Injector;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Tasks;
using UnityEngine;

public class AssetBundleDependencyLogger
{
    public void LogAssetBundleLayout(IEnumerable<(string assetBundleName, string assetPath)> explicitBundles)
    {
        var assetBundleBuilds = new Dictionary<string, AssetBundleBuild>();
        foreach (var (assetBundleName, assetPath) in explicitBundles)
        {
            if (assetBundleBuilds.TryGetValue(assetBundleName, out var assetBundleBuild))
            {
                var paths = assetBundleBuild.assetNames.ToList();
                paths.Add(assetPath);
                assetBundleBuild.assetNames = paths.ToArray();
            }
            else
            {
                assetBundleBuild = new AssetBundleBuild
                {
                    assetBundleName = assetBundleName,
                    assetNames = new [] {assetPath}
                };
                assetBundleBuilds.Add(assetBundleName, assetBundleBuild);
            }
        }
        
        // AssetBundleのレイアウトは行うが生成までは行わないタスクを作る
        var buildTasks = CreateBuildTasks("builtinShaderBundle");
        var extractDataTask = new ExtractDataTask();
        buildTasks.Add(extractDataTask);
        
        // ↑のタスクを実行する
        var builds = assetBundleBuilds.Values;
        var exitCode = RefreshAssetBundles(builds, BuildTarget.Android, buildTasks);
        if (exitCode < ReturnCode.Success)
        {
            Debug.LogError(exitCode);
            return;
        }
        
        // 結果をログ出力する
        foreach (var bundle in builds)
        {
            var bundleName = bundle.assetBundleName;
            foreach (var assetPath in bundle.assetNames)
            {
                Debug.Log($"Asset: {assetPath}{Environment.NewLine}AssetBundle: {bundleName}{Environment.NewLine}Explicit: true");
            }
        }
        foreach (var fileToBundle in extractDataTask.WriteData.FileToBundle)
        {
            var fileName = fileToBundle.Key;
            var bundleName = fileToBundle.Value;
            var guids = GetImplicitGuidsForBundle(fileName, extractDataTask);
            foreach (var guid in guids)
            {
                var assetPath = AssetDatabase.GUIDToAssetPath(guid.ToString());
                Debug.Log($"Asset: {assetPath}{Environment.NewLine}AssetBundle: {bundleName}{Environment.NewLine}Explicit: false");
            }
        }
    }
    
    /// <summary>
    /// AssetBundleのレイアウトは行うが生成までは行わないタスクを作る
    /// </summary>
    /// <param name="builtinShaderBundleName"></param>
    /// <returns></returns>
    private static IList<IBuildTask> CreateBuildTasks(string builtinShaderBundleName)
    {
        return new List<IBuildTask>
        {
            // Setup
            new SwitchToBuildPlatform(),
            new RebuildSpriteAtlasCache(),
            
            // Player Scripts
            new BuildPlayerScripts(),
            
            // Dependency
            new CalculateSceneDependencyData(),
            new CalculateAssetDependencyData(),
            new StripUnusedSpriteSources(),
            new CreateBuiltInShadersBundle(builtinShaderBundleName),
            
            // Packing
            new GenerateBundlePacking(),
            new UpdateBundleObjectLayout(),
            
            new GenerateBundleCommands(),
            new GenerateSubAssetPathMaps(),
            new GenerateBundleMaps()
        };
    }
    
    /// <summary>
    /// 与えられたBuildTaskを元にアセットバンドルビルドを行う
    /// </summary>
    /// <param name="builds"></param>
    /// <param name="targetPlatform"></param>
    /// <param name="tasks"></param>
    /// <returns></returns>
    private static ReturnCode RefreshAssetBundles(IEnumerable<AssetBundleBuild> builds, BuildTarget targetPlatform, IList<IBuildTask> tasks)
    {
        var tempFolderPath = $"Temp/{nameof(AssetBundleDependencyLogger)}";
        if (!Directory.Exists(tempFolderPath))
        {
            Directory.CreateDirectory(tempFolderPath);
        }
        var content = new BundleBuildContent(builds);
        var group = BuildPipeline.GetBuildTargetGroup(targetPlatform);
        var parameters = new BundleBuildParameters(targetPlatform, group, tempFolderPath);

        var code = ContentPipeline.BuildAssetBundles(parameters, content, out _, tasks);
        if (Directory.Exists(tempFolderPath))
        {
            Directory.Delete(tempFolderPath, true);
        }

        return code;
    }
    
    private static IEnumerable<GUID> GetImplicitGuidsForBundle(string fileName, ExtractDataTask extractDataTask)
    {
        var explicitAssetPaths = extractDataTask.WriteData.AssetToFiles.Keys;
        return extractDataTask.WriteData.FileToObjects[fileName]
            .Where(x => !explicitAssetPaths.Contains(x.guid))
            .Select(x => x.guid)
            .Where(x => 
            {
                var path = AssetDatabase.GUIDToAssetPath(x.ToString());
                return IsPathValidForEntry(path);
            });
    }

    private static bool IsPathValidForEntry(string path)
    {
        if (string.IsNullOrEmpty(path))
        {
            return false;
        }

        path = path.ToLower();
        if (path == CommonStrings.UnityEditorResourcePath ||
            path == CommonStrings.UnityDefaultResourcePath ||
            path == CommonStrings.UnityBuiltInExtraPath)
        {
            return false;
        }

        var ext = Path.GetExtension(path);
        if (ext == ".cs" || ext == ".js" || ext == ".boo" || ext == ".exe" || ext == ".dll" || ext == ".meta")
        {
            return false;
        }

        return true;
    }
    
    // ビルドの過程で生成されたデータをDIさせて参照できるようにするだけのクラス
    public class ExtractDataTask : IBuildTask
    {
        public int Version => 1;

        public IDependencyData DependencyData => _dependencyData;

        public IBundleWriteData WriteData => _writeData;

        public IBuildCache BuildCache => _buildCache;

#pragma warning disable 649
        [InjectContext(ContextUsage.In)] 
        private IDependencyData _dependencyData;

        [InjectContext(ContextUsage.In)]
        private IBundleWriteData _writeData;

        [InjectContext(ContextUsage.In)]
        IBuildCache _buildCache;
#pragma warning restore 649

        public ReturnCode Run()
        {
            return ReturnCode.Success;
        }
    }
}

考え方としては、実際にSBPによるビルド時に使われるBuildTaskのうち、レイアウトの構築に関わる部分だけを実行します。
またそれだけでは成果物が見える状態にならないので、成果物を公開するためのExtractDataTaskを最後に実行します。

これで構築されたアセットバンドルのレイアウトが得られるので、ここから暗黙的な依存が発生しているアセットを取得しています。

実行する

上記のクラスは以下のようにして使用します。

public class Example
{
    [MenuItem("AssetBundleDependencySpec/Test Build")]
    public static void Test()
    {
        var assetBundleInfos = new List<(string assetBundleName, string assetPath)>
        {
            ("AB001", "Assets/ScriptableBuildPipelineExample/TestPrefab1.prefab"),
            ("AB002", "Assets/ScriptableBuildPipelineExample/TestPrefab2.prefab"),
        };
        var spec = new AssetBundleDependencySpecification();
        spec.LogAssetBundleLayout(assetBundleInfos);
    }
}
  • TestMaterial1.matを参照するTestPrefab1.prefab
  • TestMaterial1.matを参照するTestPrefab2.prefab
  • ビルトインシェーダを参照するTestMaterial.mat

の三つのアセットを作って実行したところ、以下の結果が得られました。

f:id:halya_11:20201027215654p:plain
ログ出力

構築されたレイアウトが正常に取れていることが確認できました。

関連

light11.hatenadiary.com