Addressablesの意図しない依存関係をチェックしたい

こんにちは、エンジニアリングマネージャーの渡辺(@mochi_neko_7)です。

Synamonの新プロダクト SYNMN (読みはシナモン)

synmn.app

のベータ版のリリースが始まり、8月頃に記事を書いた Unity の Addressables 周りの仕組みが運用に乗り始めました。

synamon.hatenablog.com

synamon.hatenablog.com

今回はそんな Addressables の運用時に遭遇した、Addressables 内の AssetBundle の依存関係の課題とその解決をするために作った依存関係チェックの仕組みに関する記事になります。

要約すると、Addressables が AssetBundle 間の依存関係を勝手に良い感じに解決をしてくれるのですが、そのせいで意図しない AssetBundle をロードしてしまうのを避けるために、依存関係をチェックするツールを作ったという話です。

もし同じように Addressables の依存関係で悩んでいる方の参考になれば幸いです。

Addressables における依存関係解決とそれによる副作用

Addressables でアセットをロードする際には、そのアセットが他の AssetBundle に含まれているアセットに依存している場合にはそれらも自動的にロードをしてくれます。

依存関係の情報は、Catalog の解説記事

synamon.hatenablog.com

でもコメントしているように、Catalog に明示的に含まれていますし、AssetBundle の内部にもそれらしき情報が埋め込まれています。

つまり Addressables はビルド時にアセット間の依存関係を適切に処理し、ロード時にもAssetBundle 間の依存関係を自動的に解決をしてくれます。

ですがそれは逆に言えば意図しない依存関係が入ってしまっている場合には、それも勝手に解決してしまうということです。

実際に起きた例をご紹介しましょう。

とある SceneA があり、それを Addressables で管理をしていました。

その SceneA をその内部で使用している3Dモデルや Material などのアセットごとコピーし、SceneA' としました。

フォルダ構成は以下のような形で、細かいアセットはフォルダごとまるっと Addressable に設定しています。

- SceneA (Addressable)
    - SceneA.unity (Addressable)
    - SceneA で使用しているアセット
- SceneA' (Addressable)
    - SceneA'.unity (Addressable)
    - SceneA' で使用しているアセット

ただコピーしただけでは SceneA'.unity 内の参照先のアセットは SceneA で使用しているアセット のままのため、これをコピーした後の SceneA' で使用しているアセット に差し替えます。

これで SceneA と SceneA' は互いに独立した依存関係を持たない Scene とすることができるように思えますが、実際には差し替え忘れの SceneA のアセットへの参照が残っていたり、FBXなどのインポートしたアセットのメタデータにかなりわかりにくい形で参照が残ってしまうケースがあります。

その場合には SceneA' だけを使用したいと思って Scene をロードしたのに、裏では SceneA のアセットを含んでいる AssetBundle までロードしてしまう、といったことが起きます。(実際に起きました)

このような意図しない依存関係を外すためには、アセット間の依存関係を解析して問題点を修正することが必要です。

依存関係の調べ方

Addressables における依存関係はどうやって調べれば良いのでしょうか。

Catalog の解説記事

synamon.hatenablog.com

でもコメントしているように、Catalog には Dependencies というプロパティでIResourceLocation の単位で依存関係の情報を持っていました。

ですがそもそも Catalog の内部情報を覗くのは頑張ってツールを作れば可能なものの(実際作ったのですが)容易ではないですし、覗けたとしても IResourceLocation の指しているアセットに具体的にどのような依存関係を持っているのかの詳細までは分かりません。

また、Unity公式が提供している AssetBundles-Browser

github.com

を使ってビルド済みの AssetBundle の中に埋め込まれている依存関係を覗くこともできます。

ですがこちらもおそらく AssetBundle 単位でしか情報がなく、しかもその ID がパッと見てもどの AssetBundle を指しているのか分かりづらいものでした。

ではどうすれば良いのかと悩みましたが、Catalog に依存関係の情報が含まれているということは、Addressables のビルド時には依存関係の完全な情報を持っているだろうという推測ができます。

Addressables の Build Script を調べてみると、BuildScriptPackedMode

docs.unity3d.com

の実装内部に CalculateAssetDependencyData

docs.unity3d.com

というそれらしきクラスを使用していて、その内部に持っている IDependencyData が利用できそうだと調べがつきます。

docs.unity3d.com

そのプロパティの AssetInfoSceneInfo がそれぞれ GUID などの情報を持っている AssetLoadInfoSceneDependencyInfo の Map (C#の Dictionary) となっているため、これらの Values が全てのアセット、Sceneの依存関係として利用することができそうです。

AssetLoadInfo.referencedObjects を見ればそのアセットがどのアセットを参照しているかの情報が書かれていますし、GUID が分かれば AssetDatabase を使用してアセットの Path に変換できます。

docs.unity3d.com

あとは IDependencyData をどうやって取得するかですが、ExtractDataTask という IBuildTask を利用するとビルド処理内部で保持している情報を外から利用できます。

docs.unity3d.com

ですのでこれを Build Script 内の Build Task の処理に追加してあげればよいとなります。

var extractDataTask = new ExtractDataTask();
var buildTasks = new List<IBuildTask>
{
         new CalculateAssetDependencyData(),
         new GenerateBundlePacking(),
         new GenerateBundleCommands(),
         new WriteSerializedFiles(),
         new ArchiveAndCompressBundles(),
         extractDataTask // <- Insert
};
var dependencyData = extractDataTask.DependencyData;

このような形で、Addressables の Build Script を少しカスタマイズしたものを自作することで、Addressables 内のビルド処理内で保持している依存関係の情報を直接利用することができます。

あとは得られた依存関係の情報を解析して、そのプロジェクトで持たせたくない依存関係を検知する仕組みを作ってあげれば良いです。

例えば先に挙げた Scene のコピーの例では、SceneASceneA' のフォルダを跨るような依存関係が存在してしまうと不要なアセットをロードしてしまうため、AssetLoadInfo のファイルの Path と AssetLoadInfo.referencedObjects のファイルの Path を比較して同一フォルダに含まれるかチェックする、といった形で実装できます。

URP を採用しているプロジェクトではなるべく Builtin の Shader は使わずに URP Shader を使用したいと思いますが、URP 対応していないアセットを手動で変換した時に差し替え忘れたものを検知することもできます。

FBX ファイルを Unity 上でコピーするとインポート設定に含まれている Material の参照が Editor では見えない形で残ってしまったりするのですが、そういった分かりづらい依存関係も全て検知できます。

アセットの運用面で気をつけるべきことではありますが、人間のチェックなのでどうしても限界はありますし、こういったことは仕組みで解決できるように整えてあげると運用が少し楽になると思います。

FileType に関する注意点

上記の方法で依存関係を全て拾った場合には、スクリプト系や Unity の Builtin アセット、Addressablesで管理されていないアセットなどへの参照も含まれます。

依存先のアセットの情報は ObjectIdentifier という struct で参照できるのですが、そのプロパティに FileType を持っています。

docs.unity3d.com

少し分かりづらいため、簡単な説明と具体例を挙げておきます。

  • SerializedAssetType
    • Object is contained in a standard asset file type located in the Assets folder.
      • Assets フォルダに配置されている標準的なアセットファイルに含まれるオブジェクト
    • 具体例
      • SerializedObject
      • Material
      • Scene
      • Animation
      • AudioMixer
      • etc...
  • MetaAssetType
    • Object is contained in the imported asset meta data located in the Library folder.
      • Library フォルダに配置されている、インポートされたアセットのメタデータのオブジェクト
    • 具体例
      • Script
      • Shader
      • Prefab
      • JPGやPNG、FBXなどのインポートして使用している外部データ
  • NonAssetType
    • Object is contained in file not currently tracked by the AssetDatabase.
      • ファイル内に含まれている、AssetDatabase で管理されていないオブジェクト
    • 具体例
      • Library/unity default resources (Unity の builtin の Mesh、GUI のアセットなど)
      • Resources/unity_builtin_extra (Unity の Builtin Shader、デフォルトの Material、uGUI の Spriteなど)
  • DeprecatedCachedAssetType
    • Object is contained in a very old format. Currently unused.
      • とても古いフォーマットのオブジェクト、現在は使用されていない
    • 具体例
      • 不明(Unity2021.3 のプロジェクトでは見つかりませんでした)

これら全ての依存関係が含まれるため、実際のプロジェクトで持ちたくない依存関係に合わせて無視したいものをフィルタしてあげないと不要な依存関係までチェックしてしまいます。

おわりに

今回のまとめになります。

  • Addressables はアセット、AssetBundle の依存関係を自動的に解決してくれる仕組みを持っている
  • ただし意図しない依存関係が含まれてしまうと、それも勝手に解決してしまい、本来不要なアセットをロードしてしまう場合がある
  • 意図しない依存関係を取り除くためには、依存関係の情報をチェックする仕組みを作れば良い
  • 依存関係の情報は、Addressables の Build Script を少しカスタマイズすると拾うことができる
  • その中にはスクリプトやUnity の Builtin なアセットなどの参照も全て含まれるため、FileType などを見ながら適切にフィルタしてあげる必要がある

Addressables はとても便利なので積極的に利用したいものですが、仕組みをよくわかっていないと今回の依存関係のケースのように運用時に意図しない挙動をしてしまう場合もあります。

ですが Addressables に関する情報は特に日本語ではまだまだ少ない印象なので、馴れていない方だと苦労する場面も多いと思います。

自分もまだまだ分かっていない部分もあります。(ContentState の使い道など...)

テーマとしてはかなりピンポイントな内容になりますが、もし Addressables の運用面で同じようなことに困っている方の参考になれば幸いです。