アセット参照関係を全部書き出す

こんにちは。技術部平山です。

今日は「あるアセットはどのアセットから参照されているか」 「このアセットをロードすると、何が一緒にロードされるのか」 といったことがわかるツールのお話をいたします。

基本「とりあえず今やりたいことができればいいや」で作っているので、 他の人が使うことは考えていません。 この手のツールは雛形だけ共有して、 製品ごとにカスタマイズする前提でいた方が良いかと思います。

使い方

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倍遅くなるので、たまにやると面白いパズルになります。