1672
1
0

Addressablesを大規模開発でも使いやすいようにカスタマイズした話

Published at December 10, 2019 7:27 p.m.
Edited at December 14, 2019 5:55 p.m.

Happy Elementsカカリアスタジオ Advent Calendar 2019の15日目の記事です。
本日の記事は「あんスタ!!Music」開発エンジニアの私からお送りします。

この記事では、Addressablesを大規模開発でも使いやすいようにカスタマイズした事例をご紹介します。

Addressablesとは

Addressablesとは、AssetBundleを扱う際に必要な様々な実装をフレームワーク化しまとめた物です。
Unity Package Managerから導入することができます。

具体的には、以下のような機能を備えています。

  • 依存関係にあるAssetBundleの自動ロード
  • 参照カウンタによるアンロード管理
  • 専用プロファイラによるロード状況の可視化
  • Code Stripping対策用のlink.xmlの出力機能
  • AssetBundleのビルドを行わずに動作を確認できるシミュレーションモード

詳細については以下の資料が参考になるかと思います。

【Unite 2018 Tokyo】そろそろ楽がしたい!新アセットバンドルワークフロー&リソースマネージャー詳細解説

Addressablesの欠点

これまでのAssetBundleに関する面倒事を一手に引き受けてくれるAddressablesですが、以下のような欠点を抱えています。

スクリプトから挙動を制御し辛い

Addressablesではあらゆる設定がUnityEditor上からできるようになっています。
例えばAssetBundleの命名やビルド先、ロード時のパスなどはAddressablesウィンドウとInspectorから予め設定しておくような形になっています。

GUIを用いてコーディングせずに設定が行えるのは直感的で楽ではあるのですが、アセットの数が膨大になるような開発環境においては少々取り回しの悪い仕様となってしまいます。

また、最新版では改善されていますが、以前は初期化処理のカスタマイズがし辛かったり、アセットの更新(ContentCatalogの更新)が初期化後に行えなかったりなどといった問題もありました。

アセットの数が増えるとビルドに時間がかかる

AssetBundleの仕様上の問題として、ビルドするアセットの数が増えると、少しの変更でもビルド時間が長くなってしまうという問題があります。
具体的には、アセットの数が数千・数万といった数になると、たった一つのアセットが変更されただけでもビルドに数十分掛かるといったような問題が起こります。
これは恐らくビルド前にアセットの変更検知やAssetBundle同士の依存関係の構築などを行う必要があるためで、ビルド対象となるアセットの数が増えるほどその時間が長くなってしまいます

この問題は一度にビルド対象とするアセットを最小限にすることで回避することが出来ます。

しかし、Addressablesではビルド対象とするアセットを細かく制御することができないため、この問題を回避することが難しいです。
(※ 設定したグループ単位でビルド対象にするかどうかを制御することは可能ですが、スクリプトで細かく制御することはできません)

Addressablesの欠点を解消する

上記のような欠点を解消するために、Addressablesをカスタマイズしたライブラリ「Aura」を開発することにしました。
(私が開発を始めたような言い方ですが、実際にはメルクストーリアチームのエンジニアが開発を始め、私がレビュー・デバッグ・機能追加などを担当しています)

カスタマイズの方法について

Addressablesの内部動作は大きく分けて以下の2つの実装によって成り立ちます。

  • ResourceLocator: アセットに対応するキーから、アセットの配置場所と依存関係(IResourceLocation)を導き出す実装
  • ResourceProvider: アセットの配置場所(IResourceLocation)から、実際にアセットのロードやアンロードを行う実装

そして、Addressablesは外部から実装を注入することで動作をカスタマイズすることができます。
つまり、独自に実装を行ったResourceLocatorとResourceProviderを注入することで、自由度の高いカスタマイズが可能になります。

また、AddressablesではScriptable Build Pipelineという次世代のAssetBundleビルドAPIが使われています。これを用いることでAssetBundleのビルドをスクリプトによって細かく制御することが可能になります。

これらの材料を用いて、Addressablesのカスタマイズを行います。

実装方針

Addressablesは、AssetBundleのビルド時にアドレスとアセットの対応関係や依存関係をまとめたContentCatalogを生成し、ランタイムでそれを読み込む事によってアセットのロード時の諸々の処理を実現しています。(詳しくはこの資料のP45〜を参照)

しかし、このビルドの仕組みによって前述のようなあらゆる制約が生まれてしまうこととなっています。
そこで、AssetBundleとContentCatalogのビルドの仕組みは全て独自のものに置き換えることにしました。

独自ビルドシステムの概要

Auraでの独自ビルドシステムでは、ビルドはざっくりと以下の手順に分けられます。

  1. アセットのパッキングの単位を定義する
  2. AssetBundleのビルドとAssetBundleのメタ情報の出力を行う
  3. ContentCatalogに相当する、「カタログファイル」の出力を行う

1. アセットのパッキングの単位を定義する

AssetBundleのビルドの前に、まずアセットをどのような単位で一つのAssetBundleにビルドするか・どのような文字列(アドレス)を用いてアセットを呼び出すかを定義します。

Addressablesではアセットのパッキングの単位はUnityEditor上で設定することができ、その内容はScriptableObjectとして保存されています。
独自ビルドシステムでは、アセットのパッキングの単位をエントリーファイルと呼ばれるJSON形式のファイルで定義するようにしました。

1つのエントリーファイル内に記述したアセットが1つのAssetBundleとしてビルドされる形になり、AssetBundle名はエントリーファイルの名前が用いられます。

以下の例では、2つのPrefabがprefabsという1つのAssetBundleにパッキングされ、それぞれprefab1prefab2というアドレスを用いて呼び出すことができるという定義になります。

prefabs.entry
{
    "assets": [
        {
            "asset_path": "Assets/Prefabs/Prefab1.prefab",
            "address": "prefab1",
            "labels": []
        },
        {
            "asset_path": "Assets/Prefabs/Prefab2.prefab",
            "address": "prefab2",
            "labels": []
        }
    ]
}

JSON形式で定義できるようにすることで、Unity外でスクリプトによって定義ファイルを生成することが容易になり、柔軟性の高いビルドフローを構築することができます。

2. AssetBundleのビルドとAssetBundleのメタ情報の出力を行う

エントリーファイルの定義を済ませた後、ビルドを行います。
この時、ビルド対象にするAssetBundleに対応するエントリーファイルの一覧をビルド用のスクリプトに渡すことで、対応するAssetBundleのみをビルドすることが可能になっています。

ビルド用のスクリプトはRubyで記述しており、以下のようにエントリーファイルの一覧を記したファイルとビルド対象プラットフォームを指定することでビルドを実行することができます。

※ 何故Rubyなのかと言うと、弊社ではRuby on Railsを用いてサーバーサイドの開発を行っている関係上、こういったスクリプト類はRubyで記述するという慣例があるためです

ビルドコマンド例
$ ruby scripts/asset_bundle_build.rb input_entries.txt -b iOS
input_entries.txtの例
prefabs.entry
hoge/hoge.entry
hoge/fuga.entry

(実際にはビルドに用いるUnityEditorのパスやUnityプロジェクトのパスなどを記した設定ファイルも用意する必要があるのですが、今回の説明では省きます)

内部的にはUnityをコマンドラインから起動しライブラリのメソッドを呼び出しているのですが、その他にもLibraryフォルダをプラットフォームごとにキャッシュしたり、プロジェクトをシンボリックリンクで複製して複数プラットフォームのビルドを並列で実行できるようにするなど、ビルド高速化のためのあらゆる工夫が入っています。

なお、ビルド対象とするAssetBundleを絞り込む機能はライブラリ側には用意していないため、エントリーファイルの一覧の生成はライブラリ外部での対応が必須となります。
基本的には追加・変更されたアセットに対応するAssetBundleをビルド対象にすれば良いのですが、AssetBundle間で依存関係が発生する場合には依存するAssetBundleもビルド対象に含めるなどの対応が必要になります。

ビルド対象の自動検出までをライブラリの機能に含めようとするとかなり大掛かりな仕組みになってしまい、ライブラリのメンテナンスが大変になってしまうことと、ビルドフローがライブラリによって縛られすぎると各タイトルのユースケースに沿った柔軟な対応が出来なくなってしまうため、ライブラリとしては最低限の機能の提供に留めています。
(ライブラリの開発は現在ほぼ2人のみで行っており、それぞれアプリの開発のほうも兼任しているため、ライブラリの開発・メンテに大きく工数を割くことができないという事情もあります)

なお、ここでは詳しく解説しませんが、「あんスタ!!Music」ではgitのコミット差分からビルド対象に含める必要のあるアセットを自動で検出する仕組みを用意しています。

ビルドを実行すると、AssetBundleと共にBundleInfoファイルと呼ばれるAssetBundleのメタ情報が記されたファイルが出力されます。

1つのAssetBundleに対して1つのBundleInfoファイルが生成され、BundleInfoファイルには以下の情報が含まれます。

  • CRC
  • バージョン管理に用いるハッシュ値
  • ファイルサイズ
  • AssetBundle名
  • 依存関係にあるAssetBundleの名前
  • AssetBundleに含まれるアセットのパス
  • AssetBundleが依存するスクリプトの情報(アセンブリ・クラス名)

これらの情報は、後述するカタログファイルの出力に用いられます。

3. ContentCatalogに相当する、「カタログファイル」の出力を行う

Addressablesでは、AssetBundleの依存関係やアドレスなどの情報をContentCatalogというJSON形式のファイルにまとめ、それをアプリ起動時に読み込むことでアドレスを用いたアセットの読み込みを実現しています。

Auraの独自ビルドシステムでは、AddressablesにおけるContentCatalogに相当するものをカタログファイルと呼び、独自のバイナリ形式で出力しています。
カタログファイルのビルドもRubyスクリプトで行います。
AssetBundleのビルド時と同じくカタログに含めるAssetBundleに対応するエントリーファイルの一覧を渡すことでビルドを行います。
このエントリーファイル一覧をCatalogIndexファイルと呼びます。
また、ここでCode Stripping対策に用いるlink.xmlも出力することができます。

ビルドコマンド例
# -bオプションで指定するBuildTargetは何でも良い
$ ruby scripts/catalog_build.rb master.catalog_index iOS,Android -b Android
$ ruby scripts/link_xml_build.rb link.xml -b Android

カタログファイルは頻繁にアプリからDLすることになるため、容量を切り詰めるためにJSON形式ではなく独自のバイナリ形式としています。

アセットのロードまでの流れ

ライブラリの制御のために、以下のようなファサードクラスを用意しました。
ライブラリの初期化やカタログファイルのロードはここから行います。

  • Aura
    • Initialize:ライブラリの初期化を行う(ResourceLocator、ResourceProviderの登録もここで行う)
    • LoadCatalog:カタログファイルをResourceLocatorに登録する
    • ClearCatalogs:ResourceLocatorに登録したカタログ情報を消去する
    • IsValidKey:渡されたアドレスがResourceLocatorで解決できるか確認する
    • GetDownloadSize:渡されたアドレスからダウンロードが必要な容量を計算する

GetDownloadSizeはAddressables側にも同じ機能を持ったメソッドが用意されていますが、ライブラリ側で独自に用意している理由はライブラリ開発当初にはAddressables側にまだ機能が用意されていなかったことと、カタログファイルのデータ構造の違いによるものです。

実際にアセットをロードするまでに必要な手順は以下になります。

  1. Aura.Initialize
  2. Aura.LoadCatalog
  3. Addressables.LoadAssetAsync

Aura.Initialize内でAddressablesの初期化とResourceLocator、Providerの注入を行い、Aura.LoadCatalogでカタログファイルのロードを行います。
その後はAddressablesのメソッドを用いることでアセットのロード・アンロードが可能になります。

AssetBundleのロード先のURLのカスタマイズ

AddressablesではAssetBundleのロード先のURLは事前に設定しておく必要があり、ランタイムで細かくカスタマイズし辛い仕様となっています。
Auraでは、以下のようなinterfaceを実装したクラスを用意することでスクリプト側から簡単にURLを制御することを可能にしています。

IUriBuilder.cs
public interface IUriBuilder
{
    string BuildUri(IResourceLocation location);
}

IResourceLocationのDataの部分にAssetBundleの名前やハッシュ値などが入っているため、それを用いてURLを返すような実装を行うことができます。

UriBuilderの簡単な実装例
public class UriBuilder : IUriBuilder
{
    public string Host;
    public bool UseHashUri;
    public string PlatformName;

    public string BuildUri(IResourceLocation location)
    {
        var data = location.Data as ResourceProviderOptionData;
        if (data != null)
        {
            if (UseHashUri)
            {
                return $"{Host}/{PlatformName}/{data.path}.{data.hash}";
            }
            
            return $"{Host}/{PlatformName}/{data.path}";
        }
        return location.InternalId;
    }
}

Addressablesをカスタマイズする利点

ここまで自前で実装するなら、最初からAddressablesを使わずに自前で実装したほうが良いのではと思われるかもしれません。
実際、できるならそうした方が無駄なく処理効率の良い実装ができ、ライブラリのAPIも大規模開発のユースケースに合ったものに整理することができると思います。

しかし、大規模に開発したものはそれだけメンテナンスのコストも大きくなってしまいます。
カカリアスタジオでは少数精鋭でのチーム作りを掲げているため(詳しくは2日目の記事を参照)、各アプリごとのエンジニアの数は平均約5人ほどと、恐らく同業他社と比べてもかなり少ない人数でアプリの開発・運営を行っています。
また、今の所はこういったライブラリ開発を専門に行う部署も存在しないため、各アプリチームに所属するエンジニアがアプリの開発と兼任してライブラリ開発を行うことになっています。

そのため、最小限の工数で必要十分な機能を持ったライブラリを開発する必要がありました。そこで、今回はAddressablesをカスタマイズするという方針を採択しました。

Addressablesはアセットのロード周りの設計のコンセプトはよく出来ていた(弊社でのユースケースとマッチしていそうだった)ことと、カスタマイズの口も用意されていたため、これに乗っかることにしました。
当時はまだAddressables自体preview段階の状態だったためなかなかリスキーな選択ではありましたが、結果としては概ね目論見通りに工数を節約しつつ、必要な機能が実装できたと考えています。
他のライブラリのアーキテクチャに依存する事はリスクも伴いますが、そこは工数とのトレードオフになるのかなと思います。

開発段階ではAddressables自体にもバグが多く、都度Unity公式フォーラムを確認したりバグ報告を行うなど決して楽な道程ではありませんでしたが、最近ではバグも少なくなってきたため安定して使えるようになってきた印象です。
(とはいえ、ResourcesやAssetBundleへの理解が全く無い状態で使うには少し厳しい印象があります)

Addressable Profilerがとても便利

余談ですが、ロード周りの仕組みはAddressablesの実装をほぼそのまま用いているため、Addressable Profilerがそのまま使えます。これが非常に便利で、「あんスタ!!Music」の開発でも非常に役立っています。

アセットの解放漏れはよく起こりがちな上、パフォーマンスにも影響を与えやすい不具合ですが、こうして分かりやすい形でプロファイリングが行える事で不具合を未然に防ぐことができます。
こうしたAddressablesの便利な資産を活用できることも利点の一つです。

採用実績

前述の通り、本記事で紹介したライブラリ「Aura」は「あんスタ!!Music」の開発でも利用しており、その体験版にあたる11/8にリリースされたあんさんぶるスターズ!!Music - ONLY YOUR STARS! Edition -においても同様に使われています。

体験版では3Dライブ関連のアセットをAssetBundle化し、Addressablesで読みこんでいます。楽曲選択後に出るダウンロードの確認ダイアログやプログレスバーなどは、まさにAuraとAddressablesを使って実装している部分です。

体験版のアプリではありますが、本番環境での運用実績を立てることが出来て一安心しています。
現在開発中の新作においても採用予定で、今後は社内で用いるAssetBundleのライブラリはこちらに統一していく予定となっています。

おわりに

以上、Addressablesを大規模開発でも使いやすいようにカスタマイズしたお話でした。
Addressablesもpreviewが取れ、そろそろ採用を考えている所も多いかと思われますが、この記事が参考になれば幸いです。

また、カカリアスタジオでは絶賛採用募集中です。
この記事を読んで弊社での開発に興味を持たれた方は、是非採用サイトのほうもご覧になって下さい。
弊社でこういった込み入った技術の話はこれまであまり表に出してこなかったのですが、この記事で少しでも興味を持って頂けたなら幸いです。

個人的な印象では弊社の技術スタックは比較的保守の方向に寄っていますが、今回ご紹介したようにAddressablesのような最新のライブラリを開発に導入するなど、必要に応じて意欲的な挑戦もできるような環境でもあります。こうした技術の採択ができるのも少人数開発ならではの強みだと思います。
皆さんのご応募をお待ちしております!

参考資料

Addressablesの概要を知るには、以下の公式の資料がわかりやすくまとまっています。
まだ開発初期段階だった頃の資料のためやや古い情報もありますが、全体像を把握するには役立ちます。

DeNAさんの以下の資料では本記事と非常に似たようなアプローチを取っており、参考にさせて頂いた部分もあります。Addressablesのカスタマイズをするにあたって非常に参考となる資料のため、是非一読することをお勧めします。

Scriptable Build Pipelineについては以下の資料が参考になります。

また、Unity公式のAddressablesの開発フォーラムも要チェックです。Addressablesの更新やバグの情報はここを見るのが一番です。