"Find References In Assets"を作る

3年ぶりです。お久しぶりです。

さて、Unityでアセットの依存関係を調査していると、「なんで"Find References In Scene"はあるのに"Find References In Assets"がないの？？」と思うことがあります…よね？。不要そうだから消したいけど本当にそのアセットが使われていないかどうか、を調べるのはそれなりに時間かかるんですよねぇ。ということで、作ろう。

```C#:FindReferencesInAssets.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using System.Collections.Generic;

// アセットが参照されているアセットをリストアップするEditorスクリプト

public class FindReferencesInAssets : EditorWindow
{
    [MenuItem("Assets/Find References In Assets", true)]
    public static bool ValidateFindReferencesInAssets()
    {
        // ProjectWindowで1つのアセットが選択された状態のみ許可(Hierarchyおよび複数アセットの検索には非対応)
        return Selection.assetGUIDs != null && Selection.assetGUIDs.Length == 1;
    }

    [MenuItem("Assets/Find References In Assets", false)]
    public static void FindReferencesInAssetsFunction()
    {
        if (Selection.assetGUIDs != null && Selection.assetGUIDs.Length == 1)
        {
            // アセット依存関係辞書作成(※数秒から数十秒かかるので、存在しない場合のみ作る)
            if (m_assetDependencies == null)
            {
                // 辞書生成
                m_assetDependencies = new Dictionary<string, string[]>();
                // 検索対象のアセットをリストアップ(本当はある程度絞るべきだがクラスが追加されたときに困るのでAssets以下を全検索 ※Packagesは除外)
                string[] targetGUIDs = AssetDatabase.FindAssets("t:Object", new string[] { "Assets" });
                m_numTargets = targetGUIDs.Length;
                // 各アセットの依存関係を全取得
                int index = 0;
                foreach (var targetGUID in targetGUIDs)
                {
                    string targetPath = AssetDatabase.GUIDToAssetPath(targetGUID);
                    EditorUtility.DisplayProgressBar("Get asset dependencies...", targetPath, (++index) / (float)m_numTargets);
                    // 参照しているアセットのリストを取得(自身は含まない)
                    string[] dependencies = AssetDatabase.GetDependencies(targetPath, false);
                    // すでに登録済みでなければ登録(スクリプトが含まれているdllファイルだとなぜか2回リストアップされるため)
                    if (!m_assetDependencies.ContainsKey(targetPath)) m_assetDependencies.Add(targetPath, dependencies);
                }
            }

            // 選択中アセットのパスをGUIDから取得(1個のみ)
            var assetGUID = Selection.assetGUIDs[0];
            string assetPath = AssetDatabase.GUIDToAssetPath(assetGUID);

            // 参照しているアセットリスト(結果)
            List<string> foundAssetPaths = new List<string>();

            // 検索
            int i = 0;
            foreach (KeyValuePair<string, string[]> pair in m_assetDependencies)
            {
                EditorUtility.DisplayProgressBar(assetPath, pair.Key, (++i) / (float)m_numTargets);
                // 含んでる？
                int index = System.Array.IndexOf(pair.Value, assetPath);
                if (index >= 0) foundAssetPaths.Add(pair.Key);
            }
            EditorUtility.ClearProgressBar();

            // 結果発表は別ウィンドウで
            Open(assetPath, foundAssetPaths.ToArray());
        }
    }

    // アセット依存関係辞書
    // ※Editorスクリプトのstatic変数はPlayとかBuildすると消えるので作り直す必要がある。
    private static Dictionary<string, string[]> m_assetDependencies;
    private static int m_numTargets = 0;

    // ここから結果ウィンドウの実装
    public static void Open(string asset, string[] assetList)
    {
        FindReferencesInAssets window = EditorWindow.GetWindow<FindReferencesInAssets>("FindReferences");
        window.m_asset = asset;
        window.m_assetList = assetList;
    }

    void OnGUI()
    {
        if (m_asset != null && m_assetList != null)
        {
            string clipboard = "";

            GUILayout.Label("Target asset:", EditorStyles.boldLabel);
            GUILayout.Label(m_asset);
            clipboard += "Target asset:\n" + m_asset + "\n\n";

            GUILayout.Label("Dependencies:", EditorStyles.boldLabel);
            clipboard += "Dependencies:\n";
            if (m_assetList != null && m_assetList.Length > 0)
            {
                m_scroll_pos = EditorGUILayout.BeginScrollView(m_scroll_pos);
                foreach (var asset in m_assetList)
                {
                    clipboard += asset + "\n";
                    if (GUILayout.Button(asset))
                    {
                        if (asset.Contains(".unity"))
                        {
                            // シーンファイルの場合はシーンを移行(変更許可ダイアログ付き)
                            if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
                            {
                                EditorSceneManager.OpenScene(asset);

                                // シーン内のオブジェクトを検索(非アクティブなものも検索したいので"FindReferencesInScene"は使えない)
#if false
                                EditorApplication.ExecuteMenuItem("Assets/Find References In Scene");
#else
                                List<GameObject> targets = new List<GameObject>();
                                var gameObjects = Resources.FindObjectsOfTypeAll(typeof(GameObject)) as GameObject[];
                                foreach (var go in gameObjects)
                                {
                                    bool found = false;
                                    var components = go.GetComponents<Component>();
                                    foreach (var c in components)
                                    {
                                        var so = new SerializedObject(c);
                                        var sp = so.GetIterator();
                                        while (sp.NextVisible(true))
                                        {
                                            if (sp.propertyType == SerializedPropertyType.ObjectReference)
                                            {
                                                if (AssetDatabase.GetAssetPath(sp.objectReferenceValue) == m_asset)
                                                {
                                                    targets.Add(go);
                                                    found = true;
                                                    break;
                                                }
                                            }
                                        }
                                        if (found) break;
                                    }
                                }
                                if (targets.Count > 0) Selection.objects = targets.ToArray();
#endif
                            }
                        }
                        else
                        {
                            // オブジェクトを生成してピックアップする
                            Object assetObject = AssetDatabase.LoadAssetAtPath<Object>(asset);
                            EditorGUIUtility.PingObject(assetObject);
                        }
                    }
                }
                GUILayout.Label("Click button to open asset.", EditorStyles.centeredGreyMiniLabel);
                EditorGUILayout.EndScrollView();
            }
            else
            {
                GUILayout.Label("It's an independent asset.", EditorStyles.centeredGreyMiniLabel);
            }

            // クリップボードにコピーするボタン
            GUILayout.Label("Tools:", EditorStyles.boldLabel);
            if (GUILayout.Button("Copy list to clipboard")) GUIUtility.systemCopyBuffer = clipboard;
        }
        else
        {
            // ウィンドウを開いたままEditorを終了すると、再起動時に引数がnullのまま開いてしまうのでCloseする
            this.Close();
        }
    }

    // 引数の保持
    private string m_asset;
    private string[] m_assetList;

    private Vector2 m_scroll_pos = Vector2.zero;
}
```

使い方は以下の通りです。

- スクリプトファイルを適当なEditorフォルダに入れる。
- アセットをひとつだけ選択した状態で右クリックメニューかメニューバーのAssetsから実行。
- ちょっと時間かかる。(数秒から数十秒？)
- 参照しているアセットがリストアップされる。アセットのボタンを押すとProjectウィンドウ内の当該アセットが選択状態になる。
- 検索対象のアセットがシーンから参照されている場合、ボタンを押すとシーンを開いてHierarchy内のゲームオブジェクトが選択状態になる。
- 一番下のボタンを押すと、アセットの依存関係リストをクリップボードにコピーできる。

キモはここらへんでございます。

- 全アセットを取得する方法(Type指定をObjectにする)
  - string[] targetGUIDs = AssetDatabase.FindAssets("t:Object")
- 最近のUnityではPackagesフォルダがあるので、それは除外する(Assetsフォルダだけにする)。
- 全検索はさすがに重いので2回目以降のためにキャッシュする。
- Editorスクリプトのstatic変数はPlayとかBuildすると消えるので、必要な場合に作り直す。
- "Find References In Scene"は非アクティブのものを検索しないので、個別に検索している。
  - EditorApplication.ExecuteMenuItem("Assets/Find References In Scene") でメニューが呼べるが、代用にはならない。
- ウィンドウを開いたままEditorを終了すると再起動時に未選択状態で開いてしまうので、閉じちゃう。

問題点は

- 大きなプロジェクトになると、全アセットの検索にそれなりの時間がかかる。
  - タイプを制限したり、フォルダを制限する必要がありそう。(プロジェクトの性質次第)
- 大昔に書いたものなのでC#のお作法的に古い。

というところでしょうか。

世界には同じような車輪がいくつもあるんだろうな…と思いつつ、自分なりの車輪を再発明してみました。