こんにちは。技術部平山です。
今日は「あるアセットはどのアセットから参照されているか」 「このアセットをロードすると、何が一緒にロードされるのか」 といったことがわかるツールのお話をいたします。
基本「とりあえず今やりたいことができればいいや」で作っているので、 他の人が使うことは考えていません。 この手のツールは雛形だけ共有して、 製品ごとにカスタマイズする前提でいた方が良いかと思います。
使い方
GitHubからスクリプト を持っていって、どこかのEditorフォルダに入れると、 メニューに「Kayac/Make Reference Graph」が出てきます。
実行すると、プロジェクトフォルダにファイルが4つ吐かれます。
- referenceGraph.txt
- それぞれのアセットが直接依存しているアセットのリスト
- referenceGraphPrefab.txt
- それぞれのプレハブが依存する全アセットのリスト。参照カウント付き。
- referenceGraphScene.txt
- それぞれのシーンが依存する全アセットのリスト。参照カウント付き。
- referenceGraphScriptable.txt
- それぞれのScriptableObjectが依存する全アセットのリスト。参照カウント付き。
上で述べたように、製品ごとに要求は異なるはずですので、 改造するなり、参考にして自作するなりした方が幸せになれると思います。
なお、参照カウントが何のためかと言うと、これは「不用意にプレハブを多数配置してないか見るため」です。 以前の記事で見ましたように、プレハブが複数個置いてあると、それに比例してデータが大きくなります。 現状の実装ではプレハブ配置数と参照カウントは一致しませんが、 少なくともこれが大きければ怪しい、ということは言えます。
何がうれしいの?
いくつか用途があります。
消したいんだけど使われてないか不安な時
あるアセットを消したいのだが、使われていないか不安な時に、 referenceGraph.txtで検索して、「参照される側」として出てこなければ、 消して大丈夫ということがわかります.
ただし若干の罠がありまして、実装にAssetDatabase.GetDependencies を使っている関係上、プロジェクト設定からの参照は取れないので、 例えばSplash Screen画像や、アプリアイコンに使われているかどうかは わかりません。また、もちろんResourcesやStreamingAssetsの下にあれば わかりません。
なお、「使われてない奴のリストが欲しい」ということもあると思いますが、 このプログラムを少し改造すればすぐ作れると思います。
不用意にシーン/プレハブが大きくなってないか確認する
あるシーン/プレハブのロードが妙に遅い、というようなことはあります。
知らないうちに大きなプレハブへの参照が入っていて、
イモヅル式にロードするデータ量が膨らんでいた、
というようなことです。
これは、referenceGraphなんとか.txt
を見ればわかります。
「重い」「デカい」といった症状が出てから調べてもいいでしょうし、 予防的に眺めておくのもいいでしょう。今回は、
- 使用メモリ量の削減
- 起動速度の改善
- スパイクの軽減あるいはタイミング移動
といった作業をする時に使いました。
妙に大きな画像がロードされていて、何かと思ったら、 「そのテクスチャはアトラスで、そのごく一部だけを使ったモデルがシーンに置いてあった」 ということがわかったりもします。
実装
以前のテクスチャリストの記事や AssetBundleとアプリ本体のアセット重複予防の記事 と似た感じです。
AssetDatabase.FindAssets() でアセットを列挙し、 AssetDatabase.GetDependencies() で依存関係を取ります。 あとはこの情報を使って関係をグラフ構造にします。 あとはそれをいい感じに使ってやりたいことをやるだけです。
今回は全アセットの関係が知りたいので、とにかく全アセットを列挙して、 全てにGetDependenciesを呼び、検索しやすいように辞書に入れて、 アセットの参照関係をグラフ構造にします。 グラフの構成要素である節(ノード)はこんな型です。
class Node { public string path; public string[] dependencies; public List<Node> children; }
- あるアセットが複数のアセットを参照する
- あるアセットは複数のアセットから参照される
- ループ(A→B→A..)がありうる
と自由度が高いので、JSONやXMLのようにシンプルな木構造として テキストに書くことはできません。
特に「ループがありうる」が重大でして、 再帰的な処理でリストを作っていく際には、 ループを検出する処理が必須です。でないと無限ループします。 今回のツールでは、アセットのパスをStack に入れて、Push/Popしながら、 「現在のスタックにあるパスが出てきたらスルー」という処理を入れています。
GUIDとパス
また注意が必要な点として、「一つのファイルに複数のアセット」 が存在することがあります。一つのpathに複数のGUIDがあるということです。 FindAssetsで得られるのはGUIDであり、 これをAssetDatabase.GUIDToAssetPath()でパスに変換すると、 同じパスが複数回出てくる可能性があります。そこで、
static IList<string> FindAssets(string filter) { var guids = AssetDatabase.FindAssets(filter); var set = new HashSet<string>(); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); set.Add(path); } var list = new List<string>(); list.AddRange(set); return list; }
一旦HashSetにつっこんで重複を除いています。 Linq等を使って華麗に書ける方はその方が良いでしょう(Distinctでしたっけ?)。
拡張/カスタマイズ
「テキストファイルに出てくるだけでは味気ない」という方は、素敵なUIをつけて、 「クリックしたらそのアセットに飛べる」みたいなことをやると喜ばれるかもしれません。 ただ、GUIの場合は描画負荷のことも考えないといけないので、 結構大変です。windowに入る範囲だけ描画、みたいなことをしないと、 数百数千の表示はできません。
「使っていないアセットのリストを作る」も良いですね。 参照されているかを調べる処理を足すだけです。 ただこれもカスタマイズが必要でして、
- 特定の種類は除外したい(コードとか)
- 特定のフォルダは除外したい(ResourcesやStreamingAssets、アプリアイコンなど)
- 選択したフォルダ以下で調べたい
といった要望があるかと思います。
また「あるアセットがどのGameObjectから参照されているかまで知りたい」 という要望もあると思いますが、これは少々大変です。 Unityが用意しているGetDependenciesでは足りず、 以前やった時は自力でyamlを読んでやっていました。 SerializedObject というものを使えばyamlを触らずに同じことができる、 という話も聞きましたが、試したことはありません。 どのような手段を使うにせよ、大きなプロジェクトであればあって損はない機能かと思います。
終わりに
Unityは使っているアセットは勝手にビルドに入りますので、 「気がついたらビルドがムチャクチャデカくなってた」 ということがよく起きます。 大規模なゲームの話だろ?と思われるかもしれませんが、 今時はそうも言っていられない事情があります。AssetStoreがあるからです。
昔は大規模なゲームを作るには大規模な開発体制でデータを量産しないといけませんでしたが、 今はAssetStoreで気軽にアセットが手に入りますので、 開発体制が小規模でもデータ規模は大きくなりうるのです。 「気がついたら起動待ちやメモリ使用量がえらいことになっていた」、 といった事故は昔よりも起こりやすいのではないでしょうか。
なお、この手のツールはアルゴリズム脳を刺激するので、 たまに書くと脳の老化が防げて都合が良いと思います。 アルゴリズムの経験が浅い方には良い練習になるでしょう。 設計が悪いと簡単に10倍、100倍遅くなるので、たまにやると面白いパズルになります。