Unity アセットバンドルのヒントや注意点

ATTILIO CAROTENUTO / UNITYTechnical Lead
Apr 16, 2024|8 分
Unity アセットバンドルのヒントや注意点

アセットバンドルとは、ゲームのアセットが含まれたアーカイブファイルのことです。これらはゲームを論理的なブロックに分割するために使用され、ゲームのビルドを小さくしつつコンテンツの配信や更新をオンデマンドで行うことを可能にします。また、ゲームのパッチや DLC の配信にもしばしば使用されます。アセットバンドルにはプレハブ、マテリアル、テクスチャ、オーディオクリップ、シーンなどのあらゆるアセットが含まれますが、スクリプトを含めることはできません。

以前は、アセットバンドルを手動でビルドし、各アセットを適切にマークし、ランタイムに自分で依存関係を追跡して解決する必要がありました。今ではこのようなことは、すべて「Addressables」システムによって処理されます。「Addressables」システムは、定義されたアセットグループに基づいてアセットバンドルを構築し、ロードや依存関係を透過的に処理します。

アセットバンドルの仕組みについてのガイドは数多くありますが、ここではゲームのパフォーマンス、メモリランタイムの使用、そして一般的な互換性に焦点を当て、あまり知られていない側面について取り上げていきます。

アセットのロードとアンロード

バンドルに含まれるアセットを使用しようとすると、Unity が対応するバンドルが確実にメモリにロードされることを確認した後で、アセットがメモリにロードされます。

アセットバンドル内の特定のアセットを部分的にロードすることは可能ですが、その逆は不可能です。つまり、アセットバンドル内のアセットがロードされてからは、アセットグループ全体が不要になった場合にのみ、そのアセットをアンロードすることができるのです。

その結果、バンドルの構造が理想的でない場合、ゲームが進むにつれてランタイムのメモリ使用量が増加し、パフォーマンスの悪化やクラッシュの可能性につながる事もよくあります。バンドルにアセットを大量に含めるのは、ランタイムメモリを大量に消費することになりゲームのボトルネックになってしまうため、避けたほうが良いでしょう。その代わりに、アセットがロードされて同時に使用される頻度に基づいてまとめ方を変えることを目指しましょう。

エンジンバージョンの互換性

アセットバンドルは一般的に前方互換性があるため、古いバージョンの Unity でビルドされたバンドルは、ほとんどの場合、新しいバージョンの Unity でビルドされたゲームでも動作します(後で説明するように、TypeTree 情報を取り除かないことを前提としています)。その逆も然りというわけではないので、ゲームビルドに使用されたものよりも新しいバージョンの Unity でビルドされたバンドルが正しくロードされる可能性は低いです。

バンドルとゲームビルドに使用されるエンジンのバージョン差が大きくなると、互換性は低くなります。また、バンドルはまだロードされるかもしれませんが、バンドルに含まれるオブジェクトが Unity の新しいバージョンで正しくロードされない場合もあります。これは多くの場合、オブジェクトのシリアライズ方法の変更によって問題が引き起こされるためです。その場合、互換性を維持するためにバンドルをリビルドする必要があります。

また、以下の TypeTree のセクションで説明するように、異なるバージョンの Unity からバンドルをロードする際にはパフォーマンスコストもかかります。

これらの理由から、ゲームビルドの Unity バージョンを更新するときは必ず既存のアセットバンドルに対して徹底的にテストし、可能な限り更新することをお勧めします。

クロスプラットフォームの互換性

アセットバンドルは、一般的に、クロスプラットフォームサポートを提供していません。エディターでは他のターゲットプラットフォーム向けのバンドルをロードすることができますが、デバイス上ではロードできません。

これは、必ずしもプラットフォーム固有ではないアセットを含むバンドルにも当てはまります。

このような制限があるのは、データがターゲットプラットフォームでのみ機能する方法で最適化されたり圧縮されたりしている可能性があるからです。また、バンドルには、異なるプラットフォーム間で共有されるべきではないプラットフォーム固有のデータを含めることができます。そのため、別のプラットフォームでの使用を想定していないコンテンツの流出を防ぐことができます。

ローディングキャッシュ

ローディングキャッシュは、Unity がアセットバンドルの最近アクセスされたデータを保存するページの共有プールです。これはグローバルで、ゲーム内のすべてのアセットバンドルの間で共有されます。

私の記憶が正しければ、ローディングキャッシュは Unity 2021.3 で導入され、その後 2019.4 にバックポートされたはずです。これ以前は、Unity はアセットバンドルごとの個別のキャッシュに依存していたため、実行時のメモリ使用量が大幅に増加していました(後述の「シリアライズされたファイルバッファ」で説明します)。

デフォルトでは 1MB に設定されていますが、「AssetBundle.memoryBudgetKB」を設定することで変更できます。

ほとんどのケースではデフォルトのキャッシュサイズだけで十分ですが、キャッシュサイズの変更によってゲームにメリットがもたらされる場合もあります。たとえば、小さなオブジェクトがたくさん含まれているバンドルがある場合、キャッシュサイズを大きくするとキャッシュヒットが増え、ゲームのパフォーマンスが向上する可能性があります。

追加の内部データ

アセットバンドルには、ゲームアセットに加え、Unity にどのアセットをどのようにロードするかを伝えるための多くの追加情報とヘッダー、および(使用している Unity のバージョンによっては)専用のキャッシュが含まれています。

講演項目

バンドル内のアセットのマップです。これが存在することにより、バンドル内の各アセットを名前で検索し、読み込むことができます。通常、メモリ上のサイズについては、アセットバンドルが何千ものオブジェクトを含む特別に大きなものでない限り気にする必要はありません。

プリロードテーブル

プリロードテーブルには、バンドルに含まれる各アセットの依存関係がリストアップされます。Unity がアセットのロードや構築を正しく行うために使用されます。

バンドルに含まれるアセットに明示的または暗黙的な依存関係や、他のバンドルから流入する依存関係が多く含まれる場合、プリロードテーブルは非常に大きくなる可能性があります。このような理由から(そして他の多くの理由からも)、依存関係の連鎖を最小限にするようにバンドルを設計するのが望ましいでしょう。

TypeTree

TypeTree は、アセットバンドルに含まれるオブジェクトのシリアライズされたレイアウトを定義するものです。

その大きさは、バンドルに含まれるオブジェクトの種類の多さによって変動します。このため、多くの異なる種類のオブジェクトが混在するような大きなバンドルは作らないようにするのが得策です。

TypeTree は、ゲームビルドの Unity バージョンをアップグレードしつつ、古いバージョンのエンジンでビルドされたアセットバンドルをロードできるように互換性を維持するのに必要不可欠です。例えば、オブジェクトの形式や構造が変更された場合、セーフバイナリでの読み取りを実行することで、Unity はそれに関係なくロードを試みることができます。これにはパフォーマンスコストがあるため、一般的には、エンジンをアップデートする際にはバンドルも可能な限りアップデートすることが推奨されています。

また、バンドルのビルド時に「BuildAssetBundleOptions.DisableWriteTypeTree」のフラグを設定することで、任意で無効にすることも可能です。これによりバンドルおよび関連するメモリのオーバーヘッドは小さくなりますが、ゲームビルドのエンジンバージョンを更新するたびに、すべてのバンドルを再構築する必要があることも意味します。これはユーザー生成コンテンツ用にプレイヤーが構築したバンドルに依存している場合は特に大変になるので、よほど強い理由がない限りは TypeTree を有効にしておく事をおすすめします。

通常、TypeTree を安全に無効化できるケースとして、ゲームビルドに直接含まれるバンドルが挙げられます。この場合、どちらにせよエンジンをアップグレードする際に新しいゲームビルドと新しいアセットバンドルを作る必要があるので、後方互換性についての心配は必要ありません。

それぞれのバンドルは固有の TypeTree を持つため、同じ種類のオブジェクトを含む複数の小さなバンドルがあると、ディスク上の合計サイズがわずかに増加します。一方、ロード時に TypeTree はメモリ上のグローバルキャッシュに格納されるため、複数のアセットバンドルに同じタイプのオブジェクトが格納されていてもランタイムのメモリコストが高くなることはありません。

シリアライズされたファイルバッファ

注意:前述の通り、Unity 2019.4 以降のバージョンでは、こちらはグローバルな共有ローディングキャッシュに置き換えられています。

アセットバンドルがロードされると、Unity はシリアライズされたファイルをメモリに格納するための内部バッファを割り当てます。

通常のアセットバンドルには 1 つのシリアライズされたファイルが含まれますが、ストリーミングシーンアセットバンドルには、そのバンドルに含まれる各シーンごとに最大 2 つのファイルが含まれます。これらのバッファのサイズは、プラットフォームによって異なります。Switch、PlayStation、Windows RT では 128KB、その他のプラットフォームでは 14KB のバッファとなります。

非常に小さなアセットバンドルを大量に作成すると、これらのバッファが占有するメモリが実際に提供するアセットに比べて非常に大きくなる可能性があるので、避けた方がよいでしょう。

CRC 整合性チェック

CRC(Cyclic Redundancy Check)は、アセットバンドルのチェックサム検証に使用され、ゲームに配信されるコンテンツが想定通りのものであることを保証します。CRC はバンドル内の非圧縮コンテンツに基づいて計算されます。

コンソールの場合、アセットバンドルは通常、タイトルがインストールされる際にローカルストレージに含まれるか、DLC としてダウンロードされるので、CRC チェックは不要になります。PC やモバイルなどの他のプラットフォームの場合、CDN からダウンロードしたバンドルの CRC チェックが重要になります。これは、破損したファイルや不完全なファイルによるクラッシュの発生を避けるためであり、改ざんの可能性を避けるためでもあります。

CRC チェックは、特にコンソールやモバイルでは、CPU の使用率が高くなります。このような理由から、多くの場合、ローカルおよびキャッシュされたバンドルでは CRC チェックを無効にし、キャッシュされていないリモートバンドルでのみ有効にするのがおすすめです。

アセット検索のオーバーヘッドを削減

Unity はデフォルトで、バンドル内のアセットを検索する方法を 3 つ提供しています。

  • プロジェクトの相対パス(Assets/Prefabs/Characters/Hero.prefab)
  • アセットのファイルネーム(Hero)
  • アセットの拡張子つきファイルネーム(Hero.prefab)

これは便利ですが、コストがかかります。Unity は最後の 2 つのメソッドをサポートするためにルックアップテーブルを構築する必要がありますが、これは大規模なバンドルではかなりの量のメモリを消費する可能性があります。

さらに、「プロジェクトの相対パス」とは異なる方法でアセットをロードすると、やはりテーブル検索が必要になるため、パフォーマンスコストが発生します。

これらの理由により、それらのメソッドの使用は避けることをおすすめします。アセットバンドルのビルド時にこれらを無効にし、アセットバンドルのロードパフォーマンスとランタイムメモリ使用量を向上させることもできます。

そのためには、バンドルのビルド時に以下の 2 つのフラグをセットしましょう。

アセット管理フォーラムでは、アセット管理についてもっと学び、フィードバックを共有し、コミュニティや Unity スタッフと交流することができます。