Addressables版 IPreprocessBuild IPostprocessBuild を実装する


# はじめに

この記事では題名の通りAddressables版の `IPreprocessBuild` と `IPostprocessBuild`、
つまりビルド前後処理を組み込むための手順を解説します。

それに合わせてAddressablesのビルドスクリプトに関して軽い説明を添えているので、
実装をなぞりつつAddressablesのカスタマイズに関して少し詳しくなれます。

# この記事の対象者

- Addressablesをすでに使ったがある人
    - Addressables自体に関する解説はすでにたくさんあるのでそっちを見てください

# 環境

- Unity 2022.3.16.f1
- Addressables 1.21.21

# 実装概要

Addressablesでビルド前/ビルド後に処理を組み込むにはそのタイミングで処理を行うよう実装を追加する必要があります。

これを実現するためにAddressablesのビルドスクリプトを自前で用意します。
といってもAddressablesを実際にビルドする部分の処理は触らず、最小限の実装で済むように進めていきます。

# Addressablesのビルドスクリプトを作成

まずは最小単位のAddressablesビルドスクリプトを用意します。

`UnityEditor.AddressableAssets.Build.DataBuilders.BuildScriptPackedMode`を継承したクラスを作成します。
`BuildScriptPackedMode`はEditor用アセンブリに含まれるクラスなので、
継承クラスの定義場所は`Editor`ディレクトリ以下に置くなどしてEditor用アセンブリに含まれるように配置しましょう。

```csharp:AddressablesBuildScript_Postprocessable.cs
using UnityEngine;

namespace UnityEditor.AddressableAssets.Build.DataBuilders
{
    [CreateAssetMenu(fileName = "BuildScriptPacked_Postprocessable.asset", 
        menuName = "Addressables/Content Builders/Postprocessable Build Script")]
    public class AddressablesBuildScript_Postprocessable : BuildScriptPackedMode
    {
        public override string Name
        {
            get { return "Postprocessable Build Script"; }
        }
    }
}
```

`BuildScriptPackedMode`は`Default Build Script`を定義したclassで、
初期状態のAddressablesに設定されているAddssablesビルドスクリプトです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575311/59a902db-42be-32ce-f259-f87f8ed4cd99.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575311/b9ec40fe-52f4-9527-416b-f59692a776af.png)

Addssablesビルドスクリプトは`UnityEditor.AddressableAssets.Build.DataBuilders.BuildScriptBase`を継承していれば作成できるのですが、
ビルドプロセスを大きく変えるようなことがない限りは基本的なビルドプロセスが入っている`BuildScriptPackedMode`を継承元に選ぶ方が楽に実装が済みます。

今回用意した`AddressablesBuildScript_Postprocessable`も、
このまま実行すれば`Default Build Script`と同じ動きをします。

# Postprocess interfaceを準備

ビルドスクリプトから呼び出されるInterfaceを用意しておきます。
`IPreprocessBuild` `IPostprocessBuild` に該当するinterfaceです。

```csharp:IAddressablesEditor.cs
using UnityEditor.AddressableAssets.Build;
using UnityEditor.Build;

namespace UnityEditor.AddressableAssets
{
	public interface IPreprocessAddressablesBuild: IOrderedCallback
	{
		public void OnPreprocessAddressablesBuild(AddressablesDataBuilderInput builderInput);
	}

	public interface IPostprocessAddressablesBuild : IOrderedCallback
	{
		public void OnPostprocessAddressablesBuild(AddressablesDataBuilderInput builderInput);
	}
}
```

`IOrderedCallback`は処理の呼び出し順を制御するためのinterfaceです。

実装することで呼び出し順を明確に制御できるようにするのが目的です。

`AddressablesDataBuilderInput`はAddressablesビルドに使用されるデータです。

この内容に沿ってAddressablesはビルドされます。
内容を変更することでAddressablesのビルド内容をコード上で制御したりできます。

# AddssablesビルドスクリプトからProcess Interfaceを呼び出す

[Addressablesのビルドスクリプトを作成](#Addressablesのビルドスクリプトを作成) で作成したclass内にProcess呼び出し処理を追加します。

長いので小分けに記載します。

## Postprocess Interfaceを呼び出す関数を実装

Postprocess interfaceのついた型を収集し、呼び出す関数を実装します。

```csharp:AddressablesBuildScript_Postprocessable.cs
    ...
    
    public class AddressablesBuildScript_Postprocessable : BuildScriptPackedMode
    {
    
        ...
                
        private static Exception _OnPreprocessAddressablesBuild(AddressablesDataBuilderInput builderInput)
        {
            var processTypes = TypeCache.GetTypesDerivedFrom<IPreprocessAddressablesBuild>();

            List<IPreprocessAddressablesBuild> instances = new List<IPreprocessAddressablesBuild>();
            try
            {
                foreach (var processType in processTypes)
                {
                    instances.Add((IPreprocessAddressablesBuild)Activator.CreateInstance(processType));
                }
                instances = instances
                    .OrderBy(m => m.callbackOrder)
                    .ToList();

                foreach (var instance in instances)
                {
                    instance.OnPreprocessAddressablesBuild(builderInput);
                }
            }
            catch (Exception exception)
            {
                return exception;
            }
            finally
            {
                foreach (var instance in instances
                    .Where(m => m != null)
                    .OfType<IDisposable>())
                {
                    instance.Dispose();
                }
            }

            return null;
        }
        private static Exception _OnPostprocessAddressablesBuild(AddressablesDataBuilderInput builderInput)
        {
            var processTypes = TypeCache.GetTypesDerivedFrom<IPostprocessAddressablesBuild>();

            List<IPostprocessAddressablesBuild> instances = new List<IPostprocessAddressablesBuild>();
            try
            {
                foreach (var processType in processTypes)
                {
                    instances.Add((IPostprocessAddressablesBuild)Activator.CreateInstance(processType));
                }
                instances = instances
                    .OrderBy(m => m.callbackOrder)
                    .ToList();

                foreach (var instance in instances)
                {
                    instance.OnPostprocessAddressablesBuild(builderInput);
                }
            }
            catch (Exception exception)
            {
                return exception;
            }
            finally
            {
                foreach (var instance in instances
                    .Where(m => m != null)
                    .OfType<IDisposable>())
                {
                    instance.Dispose();
                }
            }

            return null;
        }

        ...        
    }
}
```

`TypeCache.GetTypesDerivedFrom<T>`を使用してPostprocess interfaceを実装した型を収集、
収集した`Type`を`Activator.CreateInstance`でインスタンス化し、
`IOrderedCallback.callbackOrder`で処理順を整理してPostprocessを呼び出しています。

また、インスタンス化したPostprocess classが何かしらの廃棄処理が必要な場合を考えて`IDisposable.Dispose`が実装されている場合は終了時に呼び出しています。

Addressablesのビルドフローを崩さずにエラーを出すには専用のリザルトデータを返す必要があるため、
Exceptionが出た場合は止めずにその内容を返すようにしています。

## Addressablesのビルド前/ビルド後にPostprocessが呼ばれるようにする

Addressablesのビルド前/ビルド後にここまでに記述した関数を呼ぶように実装します。

```csharp:AddressablesBuildScript_Postprocessable.cs
    ...
    
    public class AddressablesBuildScript_Postprocessable : BuildScriptPackedMode
    {
    
        ...
              
        protected override TResult BuildDataImplementation<TResult>(AddressablesDataBuilderInput builderInput)
        {
            var setting = builderInput.AddressableSettings;

            {
                var exception = _OnPreprocessAddressablesBuild(builderInput);

                if (exception != null)
                {
                    return AddressableAssetBuildResult.CreateResult<TResult>(null, 0, exception.Message);
                }
            }

            var result = base.BuildDataImplementation<TResult>(builderInput);

            {
                var exception = _OnPostprocessAddressablesBuild(builderInput);
                if (exception != null)
                {
                    UnityEngine.Debug.LogException(exception);
                }
            }

            return result;
        }

        ...        
    }
}
```

`BuildDataImplementation`はAddressablesのビルド処理が実装される関数です。

`base.BuildDataImplementation`には`Default Build Script`のビルド処理が入っているので、
これの呼び出し前と呼び出し後にPostprocessの処理を置いています。

`_OnPreprocessAddressablesBuild`から発行されるExceptionに対してはAddressablesのビルドフローに沿って例外を返すために
`AddressableAssetBuildResult.CreateResult<T>`を使用してビルドを中断するように実装しています。
Addressablesはこのリザルトデータをもってビルドフローを停止し、ログを出します。

逆に`_OnPostprocessAddressablesBuild`から発行されるExceptionに対してはビルド自体は正常に終了したことを評価して、ビルドフローを中断させずにログを出すのみにとどめています。


以上でPostprocess呼び出し処理の実装完了です。

# Addressablesビルドスクリプトとして登録する

Addressablesのビルドスクリプトとして使用するにはデータを作成する必要があります。

## AddressablesビルドスクリプトのScriptableObjectを作成する

Unityのプロジェクトビュー内で右クリックし、コンテキスト内から`Addressables/Content Builders/Postprocessable Build Script`を押下して
ここまでで作ったビルドスクリプトのScriptableObjectを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575311/29728c42-55b3-92df-4c43-bc48b548165e.png)

## AddressablesSettingにビルドスクリプトを設定する

AddressablesGroups Window内の `Build/New Build` 内に表示させるにはAddressablesSettingに設定する必要があります。

[AddressablesビルドスクリプトのScriptableObjectを作成する](#AddressablesビルドスクリプトのScriptableObjectを作成する) で作成したScriptableObjectを使用し、
`Default Build Script`に対して上書きしてしまいましょう。
(今回は`Default Build Script`が不要になるので上書きしています。
 `Default Build Script`も併用したい場合は下部にある＋ボタンから追加しましょう。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575311/a493a741-58a4-9976-cd45-3de3a2e75c9e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575311/37a484e2-2bd9-3f5e-1d41-566dc7b06f75.png)

以上でAddressablesGroups Window内の `Build/New Build` 内に新しく定義したビルドスクリプトが表示され、使用できるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575311/e2a3c5ba-c11b-6b2e-37fe-baf3e628a9f9.png)

# 完成！

以上で定義した`IPreprocessAddressablesBuild`と`IPostprocessAddressablesBuild`を使用してAddressablesのビルド前/ビルド後に処理を組み込めるようになりました。

## 試しに使ってみる

適当にログを出すPostprocessを追加します。
```csharp:Postprocess_Sample.cs
using System.Linq;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Build;
using UnityEngine;

namespace Samples
{
    public class Preprocess_Sample : IPreprocessAddressablesBuild
    {
        public int callbackOrder => 0;

        public void OnPreprocessAddressablesBuild(AddressablesDataBuilderInput builderInput)
        {
            var groups = builderInput.AddressableSettings.groups;
            Debug.Log($"preprocess {builderInput.Target.ToString()}: {string.Join(',', groups.Select(m=>m.name))}");
        }
    }

    public class Postprocess_Sample : IPostprocessAddressablesBuild
    {
        public int callbackOrder => 0;

        public void OnPostprocessAddressablesBuild(AddressablesDataBuilderInput builderInput)
        {

            Debug.Log("done build");
        }
    }
}
```

そしたらAddressablesGroups Window内の`Build/New Build/Postprocessable Build Script`を使用してビルドしましょう。
ビルドが終了すると正常に設定した処理が実行され、ログが出ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575311/e4de858f-9adc-611c-3717-c23ed76304e2.png)

## で、どう活用すればいいの?

例えばEditorにしか存在せず、ランタイムに載らない情報をビルドのタイミングで確定させて載せたり、
`AddressablesDataBuilderInput`の内容を編集してロムに乗せるAddressablesGroupを制限したりするのに使用できます。

筆者はDLC対応をするときに使用しました。

DLCを処理できるかどうかの判定の一部にファイルの存在検証があったのですが、
Addressablesでビルドされるデータが格納されるディレクトリパスには`[BuildTarget]`(=ビルド先のプラットフォーム名)が入ります。
信頼性のために、これはAddressablesのビルド時に評価された値で取得したいです。

下の画像ではその解決のために、DLCのアセットバンドルのファイルパス情報を`IPreprocessAddressablesBuild`を使用してScriptableObjectに格納しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575311/fe405ac6-324c-7fda-0254-ee7c37f9b897.png)


# おしまい

今回はAddressables版の IPreprocessBuild IPostprocessBuild を構築しました。

Addressablesを積極的にカスタムしよう！みたいな記事はあんまり見かけないので、
自身で対応する時にその情報収集や調査に苦労しました。

ので、そんな同族がこの記事で救われればいいなと思います。