この記事ではAssetBundleの基本的な知識は説明しません。
すでにAssetBundleを利用したことがある人向けの記事になっています。
新機能 Adressable Asset System
アセットをビルドから切り出せるAssetBundleをより簡単に扱えるようになるシステムが登場しました。
https://blogs.unity3d.com/jp/2019/07/15/addressable-asset-system/
- 任意の文字列でアセットを簡単にAssetBundleからLoadできる
- AssetBundleの依存関係解決の処理も用意してある
- AssetBunldeのまとめかたを調整できるEditor
- プロファイラで読み込み状況を確認できる
といった便利な機能が用意されているリソース管理のオールインワンといってもいいパッケージです。
今からAssetBundleを導入する必要があるひとは、まずは考えるべき選択肢かとおもいますが
- できて間もないから不安で使いたくない
- すでにある自作ライブラリで運用してる
- 自作ライブラリのほうが便利だ
- Adressableの仕組みが合わない
などの理由でAdressable Asset Systemを使わないという選択肢を取っている人もいると思います。
しかし、Adressable Asset Systemではできるけど、古いUnityではできなかった機能が中にはいくつか存在します。個人的に一番欲しかった機能はBuiltInShaderを分離する機能です。
なぜBuiltInShaderを分離するのか
Unity5から導入されたこのメソッドでは、事前にAssetBundle名を割り当てたアセットを収集してAssetBunldeを簡単に作ってくれるのですが、あまり細かな制御はできませんでした。
手軽にできる反面、よく調べないと気が付かない罠に気が付かずにパフォーマンスを悪化させていることがあります。
複数のAssetBundleに同じアセットが重複して保存される
特に考えずにビルドしたときに陥る罠に、いくつかのAssetBundleに同じデータが重複して入ってしまうという問題があります。
(↑ New Render Textureが4つのmaterialのAssetBundleそれぞれに含まれてしまっている様子)
うっかりテクスチャやマテリアルやShaderなど、いくつかのPrefabなどで使いまわすアセットにAssetBundle名を指定し忘れるとこのようなことになります。AssetBundleは依存関係を構築できるので、共通で使うアセットにもちゃんとAssetBundle名を設定してあげれば基本的には回避できます。
ただし、このときどうしてもAssetBundle名を設定できない困ったアセットがあります。それがUnityに組み込まれているBuiltInAssetです。Project Viewに表示されないのでAssetBundle名を指定できず、それを参照しているアセットがあるAssetBundleにはすべて重複して別々のアセットとして保存されてしまいます。
BuiltInShaderはモバイル版の天敵
そんなBuiltInAssetの中でもさらにタチが悪いのがShaderの存在です。Shaderは実行時に初めて使うときにコンパイルされるのですが、上記の問題で重複したShaderは別ものとして扱われるので、毎回コンパイルされてしまいます。特にUI-DefaultやStanderedShaderなどよく使われるShaderは地獄のような状況になる場合があります。(注1)
この問題を回避するために、わざわざDefaultのUIのShaderとマテリアルを残らず差し替えたり、徹底的にBuiltInShaderを使わないなどの辛い対応をした人もいるかと思います。(注2)
前置きがながくなりましたがこの問題を簡単に解決できるのがScriptable Build Piplineになります。
注1) そもそもUnityも最初は数個のパッチファイルを作る程度に考えていて、AssetBundleを滅茶苦茶細かく分けて作ることを想定していなかったのかもしれない。大量のアセットを頻繁にアップデートで細やかに更新するようなソシャゲの運用は想定していなかった可能性がある
注2) もしかしたらスマートな方法がもっとあったかもしれない
Scriptable Build Pipline (SBP) を使う
正直Scriptable Build PiplineはAddressable Asset Systemを作る上で既存のAssetBundleビルドシステムの限界に気が付いて作られた福産物なのかと思えるほど、Addressableのビルドシステムのために必要な機能が盛り込まれています。
(Assetのロードする際の名前を好きに変えたりできるようになってる)
細かい説明はテラシュールブログがとても分かりやすいのですが、簡単に説明するならSBPを使うことで、AssetBundleのビルドの処理にTaskを自由に作ってカスタマイズできるようになります。
DefaultBuildTasks.Preset.AssetBundleBuiltInShaderExtraction
そしてforumの公式の人の回答を見るとすでに待ち望んでいた機能が用意されいるのがわかります。SBPでビルドする処理のサンプルが以下です。
public static class BuildAssetBundlesExample
{
public static bool BuildAssetBundles(string outputPath, bool useChunkBasedCompression, BuildTarget buildTarget, BuildTargetGroup buildGroup)
{
var buildContent = new BundleBuildContent(ContentBuildInterface.GenerateAssetBundleBuilds());
var buildParams = new BundleBuildParameters(buildTarget, buildGroup, outputPath);
if (useChunkBasedCompression)
buildParams.BundleCompression = BuildCompression.DefaultLZ4;
IBundleBuildResults results;
var tasks = DefaultBuildTasks.Create(DefaultBuildTasks.Preset.AssetBundleBuiltInShaderExtraction);
ReturnCode exitCode = ContentPipeline.BuildAssetBundles(buildParams, buildContent, out results, tasks);
return exitCode == ReturnCode.Success;
}
}
DefaultBuildTasksというPresetを提供する機能の中に AssetBundleBuiltInShaderExtraction なるカスタマイズされたTaskが用意されているのです!
var tasks = DefaultBuildTasks.Create(DefaultBuildTasks.Preset.AssetBundleBuiltInShaderExtraction);
特に重要なのは CreateBuiltInShadersBundle.csに記載されている処理なのですが、BuiltInAssetを探して別のAssetBundleにまとめる処理のサンプルコードが書かれています。
実際にこの方法でビルドするとUnityBuiltInShaders.bundleが生成され、BuiltInShaderの重複ロードが発生しなくなります。
AssetBundleBrawserで確認すると、AssetBundleからBuiltInShaderの参照が切り離されているのが確認できます。
このSBPはAdressableを使っていなくても使えるPackageなので、既存のAssetBundleを管理するライブラリがある人でも導入できる可能性があります。
しかしAssetBundleManifestに対応せず
ただし、悲しいことに、DefaultBuildTasksはAssetBundleManifestに対応していませんでした…
buildTasks.Add(new PostWritingCallback());
// Generate manifest files
// TODO: IMPL manifest generation
return buildTasks;
Manifestを使わないAdressableのために用意した機能なので、Manifestを作る機能が後回しになるのは仕方ないことなのかもしれませんが、Manifestがなければ依存関係のAssetBundleを探せなくなってしまいます。
IBundleBuildResults からManifestの代わりを自作する
実はManifestをロードしていなくても、依存関係のあるAssetBundleを読み込むことは可能です。Manifestはあくまで依存関係のあるAssetBundleを取得するメソッドを用意しているだけなので、そのリストさえ自分で作成してしまえば代わりにすることができます。
ありがたいことにContentPipeline.BuildAssetBundlesの IBundleBuildResultsにビルドの結果が記載されているのでそれを使います。
interface IBundleBuildResults {
Dictionary<string, BundleDetails> BundleInfos { get; }
}
class BundleDetails {
public string FileName {get; set;}
public uint Crc {get; set;}
public Hash128 Hash {get; set;}
public string[] Dependencies {get; set;}
}
このresultからBundleInfosをファイルにシリアライズして保存すれば、AssetBundleManifest.GetAllDependenciesなどの基本的なメソッドの代わりを提供するクラスは自作することができます。
AssetNameがファイルパスになっているのを直す
Manifestを自作することで任意のアセットをロードするのに必要なすべてのAssetBundleを読み込むことができました。しかし、以前のUnityのAssetBundleのLoad時にアセットをロードするのに必要な文字列はアセットのファイル名だったのですが、SBPのデフォルトではファイルパスになってしまっています。
これをもとに戻すにはBundleBuildContentのAddressesを変更する必要があります。
var buildContent = new BundleBuildContent(ContentBuildInterface.GenerateAssetBundleBuilds());
//GC気になる人は適宜直してね
foreach (var adress in buildContent.Addresses.Keys.ToList())
{
buildContent.Addresses[adress] = System.IO.Path.GetFileName(buildContent.Addresses[adress]);
}
AddressesのKeyはGUID, Valueがファイルパスになっているので、これをファイル名に書き換えればファイル名でAssetBundle.Loadが利用できるようになります。
ManifestとAssetNameが大きな変更になっていますが、ここまでカスタマイズできるようになれば、既存のAssetBundleのライブラリでもSBPを使えるようになるのではないでしょうか? (注3)
注3) IgnoreTypeTreeChangeが現状未サポートらしいので、それが無理だとキツイかも
まとめ
今回はBuiltInShaderを分離するためだけにSBPを導入してみましたが、DefaultBuildTasksを参考にTaskを自作することでかなりの自由なAssetBundleのビルドパイプラインを構築することができます。
- 差分ビルドで変更のあったAssetBundleを抽出したい
- 別のプロジェクトのAssetBundleと組み合わせて運用したい
- ビルドする前にファイルを差し替えたい
- ビルド直前に暗号化してAssetBundleに入れたい
などはできるのではないかと思っています。