アセットデータベースをより効果的に活用するためのヒント

アセットインポートパイプライン v2 が Unity の新しいデフォルトのアセットパイプラインとして使用されます。これまでのアセットインポートパイプライン v1 は新しいデータベースに書き換えられ、プラットフォームの切り替えが高速化しました。 これは、依存関係を追跡する堅牢な機能を備えたスケーラブルなアセットインポートの基礎を築きます。今回は、アセットデータベースの仕組み、また、時間を節約するためのヒントをご紹介します。
Unity 2019.3 のリリース以降、新規プロジェクトにおいて新しいアセットインポートパイプラインがデフォルトで使用されています。他の多くの改善と組み合わさり、これは高い信頼性と高度な性能を兼ね備えたスケーラブルなアセットインポートパイプラインの土台となっています。今回の書き換えでは、新しいワークフローをサポートするために Library フォルダーの動作方法が変更されました。
予想される様々な状況と、それらを効率的に管理する方法について具体的に検証していきましょう。時間を消費しプロジェクトのパフォーマンスを劣化させているいくつかのボトルネックを特定し、その対処法を学びます。
ここで紹介しているヒントは、Unity 2019.3 以降のバージョンで適用できます。 プロジェクトがプロトタイピングの段階を過ぎてすでに本稼働に入っている場合は、最大の安定性を保つために、Unity の最新の長期サポート(LTS)バージョンである Unity 2019 LTS を使用することをお勧めします。
まず、新規プロジェクトの作成や、アセットライブラリが存在しないプロジェクトを開く際に発生していることについて説明します。
Editor.log ファイルをご覧になると、以下のような行がたくさん並んでいることに気づくでしょう。
- Start Importing …
- Done Importing …
アセットインポートパイプラインはその運用の痕跡をこのように残しておくことで、後から調査できるようにしています。
この情報を利用すればボトルネックとなっている部分、つまりアセットインポートにかかる時間を把握することができます。
各行の出力を見ると、以下のようなデータが抽出できます。
- アセットパス
- インポート時間
- ファイル拡張子
これらのデータを解析すると、拡張子ごとに分類できます。どのインポーターがどのアセットをインポートしたかが明らかになり、インポートに最も時間がかかるアセットタイプを円グラフで集計することができます。例えば、以下のようになります。

この図では、プロジェクトのボトルネックがどこにあるかを明示しています。
この例では、テクスチャ、モデル、およびプレハブのインポートに最大の時間が消費されており、どのアセットを最適化できるかを調査する出発点となります。
SimpleEditorLogParser サンプルプロジェクトをダウンロードして、ご自身のパーサーの基礎としてご使用ください。
インポートに最も時間が消費されているアセットカテゴリが分かれば、最適化の努力をどこに注力すべきか計画できます。「テクスチャインポート」が最も時間がかかるカテゴリである場合、そのなかでも最もインポート時間がかかっているテクスチャを調べて、最終ビルドには含まれないテクスチャを削除することを検討してください。
これはインポート時間の短縮だけに終わらず、クリーンなナイトリービルドなどを行っている場合には、継続的インテグレーションパイプラインのパフォーマンスを向上します。
プロジェクトを素早く開けることは重要です。エディターの再起動や、一日中様々なプロジェクトを開くのに費やされる時間が積み重なれば、貴重なプロジェクト時間が失われます。
プロジェクトが複雑になり、より多くの機能を使用する場合はなおさらです。これには多くの要因が考えられますが、Unity 2019.3 以前では、エディターが起動している間にプロファイラーを起動させる方法はありませんでした。
Unity を開く時に指定できるいくつかの コマンドライン引数の中のひとつ、-profiler-enableコマンドライン引数は起動時にエディターをプロファイルすることができます。
このコマンドは、エディターにアプリケーションの最初のフレーム(エディターが表示されるまでに実行されるすべてのコード)のプロファイリングデータの記録を開始するように指示します。起動時に動作しているもの、そしてどれくらい時間が消費されているのかを示してくれます。それが Unity 内のシステムなのか、アセットストアのパッケージなのか、あるいはプロジェクト固有のコードなのかが分かります。これなら、次に取るべきステップを把握するのに役立ちます。

このキャプチャでは、このプロジェクトを開くのに約 50 秒かかることがわかります。
一見、アセットデータベースを読み込むには 43 秒かかっています。そして、OnPostProcessAllAsset を呼び出すのに 14 秒費やされていることが分かります。 さらに、RegisterScriptsAndTryLoadingExistingUserAssemblies 中に実行するコードが最大 10 秒追加され、その 5.7 秒ではドメインを読み込んでいます。この読み込みは [InitializeOnLoad] 属性を持つスクリプトへの呼び出しでさらに遅くなっています。
この起動データは、パフォーマンスのボトルネックを追跡したり、コードがプロジェクト内または Unity 自体にあるのかを確認する一助となります。
Library フォルダーに同じアセットの複数のインポート結果を含めることができるようになりました。このため、新しいアセットインポートパイプラインを使用するプロジェクトでは、Library フォルダーにマッピングしているフォルダーに対して「単純な」GUID(グローバル一意識別子)を持たなくなります。
Library フォルダー内のファイルは、そのコンテンツのハッシュに多くの静的および動的依存関係を加えたものをベースとしています。これにより、すでに Library フォルダーにアセットのハッシュが存在する場合、Unity は高速なプラットフォーム切り替え、アセットの重複除去、インポートのスキップなどの機能を使うことができます。
つまり、Library フォルダー内のインポート結果を探すのはもはやどうでもよい作業ではないということです。以下の例では、「Assets/Prefabs/MyPrefab.prefab」のインポート結果を検索できる場所を示しています。
public class LibraryPathsForAsset
{
[MenuItem("AssetDatabase/OutputLibraryPathsForAsset")]
public static void OutputLibraryPathsForAsset()
{
var assetPath = "Assets/Prefabs/MyPrefab.prefab";
StringBuilder assetPathInfo = new StringBuilder();
var guidString = AssetDatabase.AssetPathToGUID(assetPath);
// プラットフォームを切り替えることなく異なるプラットフォームでも
// インポートできるようにしようとしているため、ArtifactKey はここで必要です
// 将来的には ArtifactKeys がパラメーター化される予定です
var artifactKey = new ArtifactKey(new GUID(guidString));
var artifactID = AssetDatabaseExperimental.LookupArtifact(artifactKey);
// アセットが複数のインポート結果を持つ可能性があるため
// 例えばサブアセットが存在する場合には
// 全てのアーティファクトパスを反復処理する必要があります
AssetDatabaseExperimental.GetArtifactPaths(artifactID, out var paths);
assetPathInfo.Append($"Files associated with {assetPath}");
assetPathInfo.AppendLine();
foreach (var curVirtualPath in paths)
{
// 仮想パスはどこかにリダイレクトされるので
// ディスク上の実際のパス(またはメモリ内のデータベース上のパス)を取得します
var curPath = Path.GetFullPath(curVirtualPath);
assetPathInfo.Append(" " + curPath);
assetPathInfo.AppendLine();
}
Debug.Log("Path info for asset:\n"+assetPathInfo.ToString());
}
}
ここでは、異なるバージョンの Unity に対応した 2 つの gist をご紹介します。
サンプルが異なるのは、アセットインポートパイプラインの実装が進むにつれ、多くの API が Experimental ネームスペースからアセットデータベース独自の名前空間に移動されたためです。
プロジェクトの中から特定のアセットを探し、そのアセットに何か作業をしたいと思うことが多くあるでしょう。エディターコードを実行しているときには、これを何回も繰り返したいこともあるでしょう。
AssetDatabase.FindAssets を呼び出すとプロジェクト全体をスキャンして、与えたクエリに一致するものを見つけようとします。プロジェクトの規模が拡大するにつれ、さまざまなサイズのプロジェクトを検索するのにかかる時間が増えてパフォーマンスのボトルネックとなる可能性があります。

予想した通り、プロジェクトのアセット数が多ければ多いほど、それらのアセットを検索する時間は長くなります。幸いなことに、各アセットの検索時間は時間の経過とともにある程度安定しています。

ご覧のように、プロジェクトに何十万ものアセットがある場合、アセットを検索すると開発が著しく遅くなる可能性があります。アセットの数が 20 万の時点で、単純な検索クエリですでに 200 ミリ秒の遅延が発生しています。
ブルートフォースアプローチを使用することで、以下のような一般的な使用パターンが作成されます。
string[] assets = AssetDatabase.FindAssets ("t:texture2D");
if(assets.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath (guids[0]);
if (!string.IsNullOrEmpty (path))
{
Texture tex = AssetDatabase.LoadAssetAtPath<Texture>(path);
// このテクスチャを使って行う処理をここに書く
}
}
基本的にこのコードでは、プロジェクト全体をスキャンして 1 つのテクスチャを検索し、それで何か行おうとします。
アセットデータベースは、 アセットのパスを GUID で検索してくれます。これは、配列を反復して一致するものを探すのではなく、辞書をキーで検索していると解釈していただいてもよいでしょう。
ブルートフォースと比較したこのアプローチの利点は、アセットデータベースがプロジェクト全体を検索してアセットを見つける必要がないことです。GUID をデータベースのインデックスとして使用し、そのパスに従ってアセットをメモリに読み込むことができます。
GUID はアセットに対応する .meta ファイルに存在します。以下の例をご覧ください。
fileFormatVersion: 2
guid: 9fc0d4010bbf28b4594072e72b8655ab
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
この場合、アセットの GUID は 9fc0d4010bbf28b4594072e72b8655abとなります。
その情報があれば、次のコードを実行できます。
var path = AssetDatabase.GUIDToAssetPath("9fc0d4010bbf28b4594072e72b8655ab");
var asset = AssetDatabase.LoadAssetAtPath(path);
これで、アセットを使用する準備ができました。
さらに、エディター内での検索を高速化したい場合は以下のツールもお試しください。
Quick Search パッケージは、Unity の複数の領域を検索することができます。
TypeCache は、既知の型から派生したスクリプトを検索します。
通常、アセットの GUID は一定です。
特定の状況において、アセットとその .meta ファイルが重複し衝突が発生しますが、以下の方法でアセットデータベースが解決します。
プロジェクト内のフォルダーを同じプロジェクト内の別の場所に複製する場合
アセットストアパッケージを複数回インポートしたり、別のプロジェクトから自分のプロジェクトにフォルダーを複数回コピーしたりする場合
AssetDatabase.Refresh 実行時に、プロジェクトを有効な状態で表示するには多くのシステムを連携させる必要があります。筆者の Unite Copenhagen の講演では、Refresh が呼び出された時に発生するさまざまなステップを詳しく説明しています。
Refresh の中で実行される特定のコールバックはコードと相互作用します。これは、Refresh 操作が完了するまでの時間に影響します。
ドメインリロード中に実行されるコードが増えるほど、エディターの動作は遅くなります。コードの反復処理が潤滑に行われるには、コードを実行する時期やプロジェクトにおいて延期可能かを慎重に検討してください。
ドメインリロード中に、以下のメソッドのいずれかが含まれている場合、コードが実行されます。
Awake
OnEnable
OnValidate
これらのメソッドのコードは、理想的には非常に高速であるか、別の時期に実行されるように遅延されるべきです(例えば、リフレッシュの間ではなく)。これは、コールバックは特定の状態を復元するのに役立つと考えられているのが理由ですが、コールバック内でできることに制限がないため、スケールしないコード(プロジェクト全体を走査するコード)は、エディターが開いている間のスクリプトに対して反復修正を行う速度を遅らせてしまいます。
別のアプローチとして EditorApplication.delayCall の使用があります。このアプローチでは、アセットデータベースがディスク上のすべての変更を検出してインポートした後、次のエディターティックでコードが実行されます。
このスレッドをフォローして、この分野における改善に関する最新情報をご覧ください。
以上のヒントがお役に立つことを願っています。アセットインポートパイプラインについてお知りになりたいこと、またお困りの点がありましたらお知らせください。私たちはアセットインポートパイプラインの改善に積極的に取り組んでいます。反復処理がほぼ瞬間的に行われることにより、皆さまのエディターでの作業やアセット/スクリプトの変更の生産性をさらに向上させるお手伝いしたいと思っています。
