こんにちは、エンジニアのオオバです。

お悩みさん
お悩みさん
  • Pack Togetherだと必要なキャラ分だけダウンロードできない
  • Pack Separatelyにしたらファイル数が多すぎて困った
  • 一括ダウンロード用のラベルと併用して大丈夫?
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    以前、スマホ向けゲームのインゲームを作っていたときに、Addressablesのbundleの分け方で悩んだことがあります。

    インゲーム用のグループは、グループ全体を1つのbundleにまとめる「Pack Together」でした。

    そこに、次の要望が出てきました。

    「一括ダウンロードしなかった人は、インゲーム開始前に必要なキャラ分だけ落としたい」

    ところが、Pack Togetherのままだと、キャラ1体分のアドレスを指定してもグループ全体が落ちてきます。

    かといってエントリごとにbundleを分ける「Pack Separately」は、以前試したことがありますが、ファイル数が多すぎて戻した経緯があります。

    ファイル数が多いと逆にダウンロード時間が伸びてしまう弊害が起きたためです。

    まとめすぎても、分けすぎてもダメ。

    最終的には、その間を取るモードで落ち着きました。

    Pack Together By Labelを使う

    Pack Together By Labelは、同じラベルの組み合わせを持つエントリを1つのbundleにまとめるモードです。

    キャラごとにラベルを付けると、キャラ1体=1 bundleになります。

    今回のポイント
    • Pack Together By Labelでキャラ・敵ごとにbundleを分割
    • 全エントリに付与した一括DL用ラベルは分け方に影響しない
    • ビルドせずにエディタスクリプトで分かれ方を試算可能
    • ステージデータは別グループでPack Separatelyにした

    起きたこと:Pack TogetherとPack Separatelyの板挟み

    AddressablesのDownloadDependenciesAsyncは、アドレスを1つ渡しても、そのアセットが入っているbundle単位でダウンロードします。

    Pack Togetherはグループ全体が1 bundleです。

    なので、キャラ1体分のアドレスを渡しても、インゲーム用グループのbundleが丸ごと落ちてきていました。

    初回のインゲームに入る前が、とても長くなる状態です。

    そこで思い浮かぶのがPack Separatelyでした。

    エントリごとにbundleを分けるので、必要なものだけを落とせます。

    ただ、インゲーム用グループだけでエントリが数百件ありました。

    その大半が、キャラごとのデータファイルです。

    以前Pack Separatelyを試したときは、ファイル数が多すぎて逆にダウンロード時間が伸びてしまったため、Pack Togetherに戻しています。

    「全部まとめる」と「全部分ける」の2択だと、どちらも詰む状況でした。

    解決:Pack Together By Labelでキャラごとにまとめる

    Pack Together By Labelなら、bundleの単位をラベルで決められます。

    キャラごとに、キャラのIDを含むラベルを付けました。

    結果は、キャラの数ぶんのbundle+共通1+シーン1。

    エントリの数だけファイルができるのと比べれば、十分に現実的な数です。

    敵とステージは、名前や不要ファイルがまだ固まっていませんでした。

    なので最初は、あえてラベルの対象外にして共通側に入れています。

    ラベル付けはSmartAddresserで自動化

    ラベルを手で付けていくと、付け忘れや付け間違いが必ず起きます。

    そこでラベル付けは、SmartAddresserの「ラベルルール」で自動化しました。

    アセットパスを正規表現でマッチさせて、ラベルを付けるルールです。

    キャラのフォルダ名からラベルが決まるので、新しいキャラを足しても人がラベルを管理する必要はありません。

    SmartAddresser(GitHub)

    一括DL用の「All」ラベルと併用していいのか

    そのプロジェクトには、もともと一括ダウンロード用に「All」というラベルが全エントリに付いていました。

    最初は「Pack Together By Labelを使うなら、1アセット1ラベルにしないとダメなのでは?」と考えました。

    Allとキャラのラベルが両方付いていたら、分け方が崩れそうな気がしますよね。

    Addressablesのソースを読んで処理を確認

    気になったので、Addressablesのソースを読みました。

    見たのは BuildScriptPackedMode.PrepGroupBundlePacking です。

    Pack Together By Labelのとき、各エントリのラベルを連結した文字列をキーにして、bundleをまとめていました。

    ラベルは内部で SortedSet に入っているので、連結する順序は常に同じです。

    つまり、全エントリにAllが付いていれば、Allは分け方に影響しません。

    ダメな2つのケース

    逆に、分け方が崩れるのは次の2つです。

    2つ目の場合、2キャラのラベルの組み合わせで、3つ目のbundleができてしまいます。

    そのまま動く一括ダウンロード

    一括ダウンロード(ExecuteDownloadLabel("All"))は、bundleが増えても動きます。

    同じbundleに入っている2つ目以降のアドレスは、ダウンロード済み(サイズ0)としてスキップされるためです。

    ビルドせずにbundleの分かれ方を試算する

    ラベルを付けたあと、毎回ビルドして分かれ方を確かめるのは時間がかかります。

    ここで役に立ったのが、さきほどの BuildScriptPackedMode.PrepGroupBundlePacking です。

    このメソッドは public static になっています。

    グループとスキーマを渡すと、AssetBundleBuild の配列が返ってきます。

    なので、エディタスクリプトから呼ぶだけで、bundle名とエントリ数を一覧にできました。

    さらに、各bundleのアセットに AssetDatabase.GetDependencies をかけて暗黙の依存を集めると、Analyzeの「重複依存」と同じことが手元でできます。

    ビルド前に「何個のbundleに分かれて、何が重複するか」が分かるのは、かなり安心感がありました。

    落とし穴:仮素材の流用で依存アセットが重複する

    試算してみて、驚いたことがあります。

    あるキャラのモーションFBXが、20個前後のbundleに複製されていたんです。

    原因は、まだ実装していないキャラが、仮で他のキャラのモーションやモデルを流用していたことでした。

    アドレスが付いていない依存アセットは、参照しているbundleごとにコピーされます。

    流用元のFBXにはアドレスが付いていなかったので、流用しているキャラの数だけ複製されていました。

    本番の素材に差し替われば消える前提だったので、当時は許容しています。

    試算しなければ、ビルドしたあとも気付かなかったかもしれません。

    インゲーム開始前の差分ダウンロード

    bundleを分けたら、次はインゲームの前に必要な分だけを落とす処理です。

    タイミングは、編成とステージが確定した直後にしました。

    ロードの前、画面が暗転している間です。

    流れは次のとおり。

    1. 編成キャラのモデルのアドレスと、ステージのシーンのアドレスを集める
    2. GetDownloadSizeAsync でダウンロードサイズを調べる
    3. 0より大きければ DownloadDependenciesAsync でダウンロードする

    モデルのアドレスを渡せば、同じbundleに入っているモーションも丸ごと落ちてきます。

    キーには存在が保証されたアドレスを渡す

    ダウンロードのキーには、ラベル名を渡すこともできます。

    ただ、存在しないキーを渡すと、GetDownloadSizeAsync が InvalidKeyException で失敗します。

    なので、存在が保証されているアドレスを渡す方が安全だと判断しました。

    あわせて、タイトル画面の初回ダウンロードはAllからInitial(共通系だけのラベル)に切り替えています。

    更新時の再ダウンロードも小さくなる

    bundleを分けたことで、アセット更新時の再ダウンロードにも効果がありました。

    bundleは、ファイル単位で丸ごと入れ替わります。

    bundleの中身の差分だけを落とす仕組みはありません。

    Pack Togetherだと、キャラ1体のモーションを追加しただけでも、グループ全体の再ダウンロードになります。

    ラベルで分けたあとは、そのキャラのbundleだけで済みました。

    分けるかどうかは「サイズ × 更新頻度」

    運用で更新するアセットは、更新の単位でbundleを分けておくべきだと思います。

    ただし、アイコン類のような小さいものは、丸ごと落とし直しても数MB程度です。

    わざわざ分ける必要はありません。

    判断の基準は 「サイズ × 更新頻度」 にしました。

    なお、古いハッシュのbundleはキャッシュに残ります。

    Cache Clear Behaviorの既定値は、容量が足りなくなったときに消す設定です。

    補足:ラベル一覧(m_LabelTable)の競合は気にしなくていい

    新しいラベルが増えるたびに、AddressableAssetSettings.asset の m_LabelTable に行が足されます。

    「ブランチ間でここが競合したらどうしよう」と不安になりました。

    調べてみると、m_LabelTable はUIでラベルの候補を出すための一覧でした。

    Editor/Build配下を検索しても、ビルド処理からは参照されていません。

    ラベルの本体は、各グループのentryに保存されています。

    AddressableAssetEntry.SetLabel も、force を付けない限り一覧をチェックしません。

    SmartAddresserは、適用するときに足りないラベルを一覧へ追加し直してくれます。

    なので、競合したら片方を採用して再インポートすれば直ります。

    その後:ステージは別グループでPack Separatelyにした

    ラベルで分けたあと、実機でダウンロード量を計測しました。

    すると、ステージ1つを遊ぶために、全ステージのシーンが入ったbundleを丸ごと落としていました。

    インゲーム前のダウンロードのうち、大半がステージです。

    テスト用の小さいステージなのに、ダウンロード量の多くを占めていました。

    計測しないと気付けなかったところです。

    シーンはグループ単位で1つにまとまる

    シーンは、Unityが自動で別のbundleに分けてくれます。

    ただし、グループ単位では1つにまとまります。

    ステージはその後も増えていく予定だったので、ステージ専用のグループを作り、Pack Separately(シーン1つ=1 bundle)にしました。

    グループの作成はエディタスクリプトで行い、CreateGroup に既存グループの Schemas を渡しています。

    ロードパスなどの設定をそのまま複製できるので便利でした。

    グループを追加したときは、AddressableAssetSettings.asset の m_GroupAssets の登録行もコミットに含めます。

    副作用:共有テクスチャが重複する

    Pack Separatelyにしたことで、副作用も出ました。

    ステージ間で共有しているテクスチャ(アドレス無し)が、bundleごとに複製されるようになったんです。

    試算では、重複ぶんだけで約100MB。

    大きめのテクスチャが何重にも入っている、といった状態でした。

    一括ダウンロードの総量は増えます。

    それでも、1回のインゲームで落とす量は大きく減りました。

    共有素材は、あとで別のグループへ切り出す方針にしています。

    その後:敵も1体=1 bundleにした

    最初はラベルの対象外にしていた敵も、分けることにしました。

    フォルダを敵ごとに分け直す

    敵のデータファイルは、1つのフォルダにまとめて並んでいました。

    これを、キャラと同じく敵ごとのフォルダへ移しています。

    Unityの AssetDatabase.MoveAsset で移せば、GUIDは変わりません。

    フォルダの .meta もUnityが作ってくれます。

    Pack Separatelyか、ラベルか

    敵はこれからも増え続ける見込みだったので、Pack Separatelyとラベルのどちらにするかを検討しました。

    依存を調べると、データファイル同士で共有している素材はほとんどありません。

    一方で、1体あたりのファイルは約10個です。

    Pack Separatelyだと、100体になれば約1,000個のbundleになります。

    なので、ラベル(敵1体=1 bundle)を選びました。

    「ラベルが増えるのがイマイチ」問題

    正直、ラベルがどんどん増えていくのは少しイマイチだと感じていました。

    ただ整理してみると、SmartAddresserでフォルダ名から自動で付けているので、人が管理する必要はありません。

    増えるのは、ラベル一覧の行だけです。

    実際の変更も、キャラ用ルールの正規表現を、敵のフォルダ名にもマッチするよう広げるだけで済みました。

    インゲーム前ダウンロードに敵を追加する

    敵は初回ダウンロードから外し、インゲーム開始前のダウンロードに、そのステージに出てくる敵のモデルを追加しました。

    ここで注意したいのが、さきほどの InvalidKeyException です。

    カタログに無いアドレスが1つでも混ざると、GetDownloadSizeAsync ごと失敗します。

    なので、事前にアドレスの存在を確認して、無いものは外してから渡すようにしていました。

    まとめ

    本記事では、AddressablesのbundleをPack Together By Labelでラベル単位に分けた話をしてきました。

    簡単に全体をまとめます。

    今回のまとめ
    • Pack Togetherはグループ丸ごと、Pack Separatelyはファイル数が多すぎる
    • Pack Together By Labelならラベルでbundleの単位を決められる
    • 全エントリに付いた一括DL用ラベルは分け方に影響しない
    • PrepGroupBundlePackingでビルドせずに分かれ方を試算できる
    • アドレスの無い依存アセットはbundleごとに複製される
    • シーンが増えるステージは別グループでPack Separatelyにした
    • ダウンロードのキーは存在を確認したアドレスを渡す

    bundleの分け方は、最初に決めたら終わりにはなりません。

    キャラが増え、敵が増え、ステージが増えるたびに、ちょうどいい単位は変わっていきます。

    振り返ると、いちばん効いたのは、ビルドせずに試算する仕組みと、実機でのダウンロード量の計測でした。

    数字を見て初めて、ステージがダウンロード量の大半を占めていたことに気付けています。

    Addressablesのbundleの分け方で迷っている方の参考になればうれしいです。

    オススメ記事
    検証環境