【Unity】Scriptable Build Pipelineによるビルドフロー構築入門

UnityのScriptable Build Pipelineによるビルドフロー構築方法について簡単にまとめました。

Unity2020.1.10
Scriptable Build Pipeline 1.7.3

Scriptable Build Pipelineとは?

Scriptable Build Pipelineを使うとアセットバンドルなどのビルドフローを自由に構築することができます。
例えばアセットバンドル同士の依存関係を構築したり、依存関係を書いたファイルを出力したりする処理をカスタムすることができます。

またこれを使ったデフォルトのビルドパイプラインがあらかじめUnityで定義されており、これはAddressableアセットシステムで使用されています。
従来のカスタムできなかったパイプラインに比べ、パフォーマンスが向上したのとインクリメンタルビルドが改善されたようです。

なおScriptable Build PipelineはUnity 2018.3以降で動作します。

インストール

Scriptable Build PipelineのインストールはPackage Managerから行います。

f:id:halya_11:20201027145047p:plain
Package Manager

Package Managerによるインストール方法は以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

IBuildTaskを理解する

それではScriptable Build Pipelineの使い方を見ていきます。
まずはIBuildTaskというインタフェースを理解する必要があります。

アセットバンドルのビルドは例えば以下のようにいくつかのフェーズに分けられます(実際にはもっと複雑です)。

  1. プラットフォーム(iOSAndroidなど)を変更する
  2. アセット同士の依存関係を収集する
  3. 依存関係の情報に基づいてアセットバンドルをビルドする

Scriptable Build Pipelineでは、これらのフェーズごとにIBuildTaskを実装したクラスを作ります。
そして以下のようにIBuildTaskのリストをBuildTasksRunner.Runに渡すことで各フェーズの処理を実行します。

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline;

public static class Example
{
    [MenuItem("Example/Build")]
    private static void Build()
    {
        // 実行する順にIBuildTaskを作成・登録する
        var tasks = new List<IBuildTask>
        {
            new LogPlatform()
        };
        
        // (これは次節で説明します)
        var contexts = new BuildContext();

        // IBuildTaskを実行する
        var returnCode = BuildTasksRunner.Run(tasks, contexts);
        
        Debug.Log(returnCode);
    }
}

// 今のプラットフォームを出力するだけのBuildTask
public class LogPlatform : IBuildTask
{
    public int Version => 1;

    public ReturnCode Run()
    {
        Debug.Log(EditorUserBuildSettings.activeBuildTarget);
        return ReturnCode.Success;
    }
}

これを実行するといまのプラットフォームがログ出力されることが確認できます。

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

IContextObjectを理解する

さて次に前節のようなIBuildTaskにパラメータを渡すことを考えます。
この場合にはScriptable Build Pipelineで用意されているDIの仕組みを使います。
具体的には以下のようにIContextObjectを実装したクラスにパラメータを格納して、IBuildTaskにDIします。

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

public static class Example
{
    [MenuItem("Example/Build")]
    private static void Build()
    {
        var tasks = new List<IBuildTask>
        {
            // プラットフォームを切り替える
            new SwitchPlatform(),
            // 今のプラットフォームをログ出力する
            new LogPlatform()
        };
        
        var contexts = new BuildContext();
        // SwitchPlatform用のContextを追加する
        var switchPlatformContext = new SwitchPlatformContext(BuildTargetGroup.Android, BuildTarget.Android);
        contexts.SetContextObject(switchPlatformContext);

        var returnCode = BuildTasksRunner.Run(tasks, contexts);
        
        Debug.Log(returnCode);
    }
}

public class LogPlatform : IBuildTask
{
    public int Version => 1;

    public ReturnCode Run()
    {
        Debug.Log(EditorUserBuildSettings.activeBuildTarget);
        return ReturnCode.Success;
    }
}

// プラットフォームを切り替えるBuildTask
public class SwitchPlatform : IBuildTask
{
    // DI対象のフィールドにはInjectContextを付ける
    // DIが任意の場合は第二引数をtrueに
    [InjectContext(ContextUsage.In)]
    private readonly ISwitchPlatformContext _context = null;

    public int Version => 1;

    public ReturnCode Run()
    {
        return EditorUserBuildSettings.SwitchActiveBuildTarget(_context.Group, _context.Target)
            ? ReturnCode.Success
            : ReturnCode.Error;
    }
}

// SwitchPlatform用のContextのインタフェース
public interface ISwitchPlatformContext : IContextObject
{
    BuildTargetGroup Group { get; }
    BuildTarget Target { get; }
}

// SwitchPlatform用のContextの実装クラス
public class SwitchPlatformContext : ISwitchPlatformContext
{
    public BuildTargetGroup Group { get; }
    public BuildTarget Target { get; }
    
    public SwitchPlatformContext(BuildTargetGroup group, BuildTarget target)
    {
        Group = group;
        Target = target;
    }
}

これを実行するとプラットフォームの切り替え処理が走ってからログ出力が行われることを確認できます。

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

実際にIBuildTaskが使用されている部分を見る

ここまで理解できたところで、IBuildTaskが実際に作られている部分を見てみます。
AddressablesではUnityEditor.Build.PipelineのDefaultBuildTasks.CreateあたりでBuildTaskが作られています。

アセットバンドル用のBuildTaskを作成しているところだけ抜き出すと以下のような感じです。

static IList<IBuildTask> AssetBundleCompatible()
{
    var buildTasks = new List<IBuildTask>();

    // Setup
    buildTasks.Add(new SwitchToBuildPlatform());
    buildTasks.Add(new RebuildSpriteAtlasCache());

    // Player Scripts
    buildTasks.Add(new BuildPlayerScripts());
    buildTasks.Add(new PostScriptsCallback());

    // Dependency
    buildTasks.Add(new CalculateSceneDependencyData());
#if UNITY_2019_3_OR_NEWER
    buildTasks.Add(new CalculateCustomDependencyData());
#endif
    buildTasks.Add(new CalculateAssetDependencyData());
    buildTasks.Add(new StripUnusedSpriteSources());
    buildTasks.Add(new PostDependencyCallback());

    // Packing
    buildTasks.Add(new GenerateBundlePacking());
    buildTasks.Add(new GenerateBundleCommands());
    buildTasks.Add(new GenerateSubAssetPathMaps());
    buildTasks.Add(new GenerateBundleMaps());
    buildTasks.Add(new PostPackingCallback());

    // Writing
    buildTasks.Add(new WriteSerializedFiles());
    buildTasks.Add(new ArchiveAndCompressBundles());
    buildTasks.Add(new AppendBundleHash());
    buildTasks.Add(new PostWritingCallback());

    // Generate manifest files
    // TODO: IMPL manifest generation

    return buildTasks;
}

大量のBuildTaskが実行順に登録されていることが確認できます。
自前でビルドフローを作るにはこのあたりの流れを自作していくことになります。

実際にIContextObjectが使用されている部分を見る

次に実際にIContextObjectが使用されている部分を見ていきます。
これはUnityEditor.Build.PipelineのContentPipeline.BuildAssetBundles()あたりを見るのがよさそうです。
このメソッドの一部を抜粋すると以下のようになります。

buildContext = new BuildContext(contextObjects);
buildContext.SetContextObject(parameters);
buildContext.SetContextObject(content);
buildContext.SetContextObject(result);
buildContext.SetContextObject(interfacesWrapper);
buildContext.SetContextObject(progressTracker);
buildContext.SetContextObject(buildCache);
// If IDeterministicIdentifiers was passed in with contextObjects, don't add the default
if (!buildContext.ContainsContextObject(typeof(IDeterministicIdentifiers)))
    buildContext.SetContextObject(new Unity5PackedIdentifiers());
buildContext.SetContextObject(new BuildDependencyData());
buildContext.SetContextObject(new BundleWriteData());
buildContext.SetContextObject(BuildCallbacks);

こちらも多数のIContextObjectがDIするために登録されていることを確認できました。

おまけ: 軽くカスタムしたいだけの場合

以上、Scriptable Build Pipelineを使ってビルドフローを構築する方法について簡単にまとめました。

ただ実際には、ビルドフローをイチから構築するというよりは一部だけカスタムできれば十分というケースもあるかと思います。
そのような場合には以下のようにCompatibilityBuildPipeline.BuildAssetBundlesに与える引数を調整することでオプショナルな設定を行うことができます。

using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline;

public class Example
{
    public static bool BuildAssetBundles(string outputPath, bool forceRebuild, bool useChunkBasedCompression,
        BuildTarget buildTarget)
    {
        // オプションを設定
        var options = BuildAssetBundleOptions.None;
        if (useChunkBasedCompression)
        {
            options |= BuildAssetBundleOptions.ChunkBasedCompression;
        }
        if (forceRebuild)
        {
            options |= BuildAssetBundleOptions.ForceRebuildAssetBundle;
        }

        // アセットバンドルビルド情報を取得
        // ※これはAddressablesじゃなく旧来の方式でAssetBundle名をつけたものを集めるAPIなので必要に応じて変える
        var bundles = ContentBuildInterface.GenerateAssetBundleBuilds();
        // Addressableの名前だけフルパスからファイル名に変更する
        for (var i = 0; i < bundles.Length; i++)
        {
            bundles[i].addressableNames = bundles[i].assetNames.Select(Path.GetFileNameWithoutExtension).ToArray();
        }

        var manifest = CompatibilityBuildPipeline.BuildAssetBundles(outputPath, bundles, options, buildTarget);
        return manifest != null;
    }
}

この辺りにはマニュアルに書いてあるのでそちらを参照してください(上記のコードもほぼこのマニュアルのものです)。

docs.unity3d.com

参考

docs.unity3d.com