AssetBundleの設定に問題があると、ユーザーの貴重な通信容量や記憶領域を浪費してしまうだけでなく、快適なゲームプレイを妨げてしまうなど多くの問題が生じてしまいます。この章ではAssetBundleに関する設定や実装の方針について説明します。
依存関係の問題で、AssetBundleをどの程度細かくするかという粒度に関しては、慎重に検討する必要があります。極端に言えば、すべてのアセットを1つのAssetBundleに入れる方法と、それぞれのアセットを1つずつAssetBundleにする方法があります。どちらもシンプルな方法ですが、前者の方法は致命的な問題があります。それはアセットを追加したり、1つのアセットを更新するだけだとしても、ファイル全体を作り直して配布する必要があるためです。アセットの総量がGB単位になる場合は、更新の負荷が非常に高くなります。
そのためAssetBundleをなるべく分割する方法を選択することになりますが、細かすぎてもさまざまな部分でオーバーヘッドが生じてしまいます。そこで基本的には以下の方針でAssetBundle化することをオススメします。
完璧にコントロールするのは難しいですが、プロジェクト内で粒度に関する何かしらのルールを決めるとよいでしょう。
AssetBundleからアセットをロードする際のAPIとして3種類あります。
AssetBundle.LoadFromFileAssetBundle.LoadFromMemoryAssetBundle.LoadFromStreamStreamを指定してロードします。暗号化されたAssetBundleの復号処理をしながらロードする場合はメモリ負荷を考慮してこちらのAPIを使います。ただしStreamはシークできる必要があるため、シークに対応できない暗号アルゴリズムを使わないように注意する必要があります。AssetBundleが不要になった時点でアンロードしないとメモリを圧迫してしまいますが、その際に使用するAPIであるAssetBundle.Unload(bool unloadAllLoadedObjects)の引数unloadAllLoadedObjectsは、非常に重要なので開発の初期にどう設定するか決定する必要があります。この引数がtrueの場合、AssetBundleをアンロードする際に、そのAssetBundleからロードされたすべてのアセットもアンロードします。falseの場合はアセットはアンロードされません。
つまり、アセットを使っているあいだAssetBundleもロードし続けないといけないtrueの方はメモリ負荷が高い一方で、アセットの破棄も確実に行えるので安全性は高いです。一方でfalseの場合はアセットをロードし終わったタイミングでAssetBundleをアンロードできるのでその場のメモリ負荷は低いのですが、使い終わったアセットのアンロードを忘れるとメモリリークに繋がったり、同じアセットがメモリ上で複数ロードされたりするため、適切なメモリ管理が求められます。一般に厳密なメモリ管理はシビアになるため、メモリ負荷に余裕がある場合はAssetBundle.Unload(true)を推奨します。
AssetBundle.Unload(true)の場合はアセットを使用している間はAssetBundleをアンロードできません。そのため実際の場面では、AssetBundleの粒度次第では100以上のAssetBundleを同時にロードしている状態といった場面も出てくるかもしれません。この場合気をつけたいのは、ファイルディスクリプターの制限と、PersistentManager.Remapperのメモリ使用量です。
ファイルディスクリプターとは、ファイルを読み書きする際にOS側から割り当てられる操作用のIDです。1つのファイルを読み書きするために1つのファイルディスクリプターが必要で、ファイル操作が完了したタイミングでファイルディスクリプターを解放します。このファイルディスクリプターを1つのプロセスが持てる数の上限が決まっているので、その上限以上のファイルを同時に開くことはできません。"Too many open files" というエラーを見かけた場合は、この上限に引っかかったことを示しています。そのためAssetBundleの同時にロードできる数がこの制限に影響を受け、またUnity側もある程度はファイルを開いているので制限に対して余裕を保つ必要があります。この制限値はOSやバージョンなどによって変化するので、ターゲットとするプラットフォームの値を事前に調査する必要がありますが、一例としてiOSやmacOSでは制限値が256のバージョンもあります。また仮に上限に当たったとしても、OSによっては一時的に上限を引き上げることも可能*1なので、必要な場合は実装を検討しましょう。
[*1] Linux/Unix環境では、setrlimit関数を用いて実行時に制限値を変更することが可能
同時にロードするAssetBundleが多いことの2つ目の問題として、UnityのPersistentManager.Remapperの存在があります。PersistentManagerは簡単に言えば、Unity内部でオブジェクトとデータのマッピング関係を管理している機能です。つまり同時にロードするAssetBundleの数に応じてメモリを使うことが想像できると思いますが、問題はAssetBundleを解放しても使用したメモリ領域は解放されずにプールされるということにあります。この性質のために同時ロード数に比例してメモリを圧迫するようになるため、同時ロード数を削減することが重要となってきます。
以上のことから、AssetBundle.Unload(true)の方針で運用する場合は、同時にロードするAssetBundleを最大でも150〜200程度を目安に、AssetBundle.Unload(false)の方針で運用する場合は最大でも150以下を目安とするとよいでしょう。