【Unity】関数名(とコンポーネント名)から参照しているオブジェクトを検索するエディタ拡張

はじめに

Unityではボタンなどのイベントに別のコンポーネントのメソッドを指定して、ボタンが押された指定の関数を呼び出すという事ができます。

これはインスペクタ上でこのように設定することで実現できますが、これを行うとコードからの逆検索が難しいという問題があります。

どういうことかというと、これを設定して3日後ぐらいに、あれ?このOnButtonClickedってどのボタンから呼んでるんだっけ?となったときに検索するすべがありません。

IDE側ではUnity上での設定など知ったことではないので参照も0個と判断されてしまいます。

このため、自分の記憶を頼りにそれっぽいオブジェクトをヒエラルキーから探すことになるのがよくあるパターンです。

あらかじめ想定してコードからコールバックを設定していれば、関数から参照の検索という手段もありますが、そのことを想定した設計等が必要なので、なりゆきで制作している場合、実施している方は少ないのではないでしょうか?

エディタ拡張で解決

というわけで、関数名とクラス名からメソッドを参照しているオブジェクトを検索するエディタ拡張を作成してみました。(長いです)

using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEditor.SceneManagement;
using System.Collections.Generic;
using System.Linq;
using System;

namespace CatHut
{
    public class EventMethodSearchWindow : EditorWindow
    {
        private string searchMethod = "";
        private string searchComponent = "";
        private List<SearchResult> foundEvents = new List<SearchResult>();
        private Vector2 scrollPosition;
        private bool includeProjectPrefabs = true;
        private bool caseSensitive = false;
        private bool searchAllScenes = false;
        private bool hasSearched = false;

        private class SearchResult
        {
            public GameObject gameObject;
            public string componentName;
            public string targetComponentName;
            public string eventName;
            public string methodName;
            public string targetObject;
            public string hierarchyPath;
            public bool isPrefab;
            public string prefabPath;
            public string scenePath;  // シーンパスを追加
            public GlobalObjectId globalObjectID;  // 追加


            public GameObject FindGameObjectInScene()
            {
                var obj = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(globalObjectID);
                return obj as GameObject;
            }
        }

        [MenuItem("Tools/CatHut/Event Method Search")]
        public static void ShowWindow()
        {
            GetWindow<EventMethodSearchWindow>("Event Method Search");
        }

        void OnGUI()
        {
            EditorGUILayout.BeginVertical();

            EditorGUILayout.BeginHorizontal();
            searchMethod = EditorGUILayout.TextField("Search Method:", searchMethod);
            if (GUILayout.Button("Search", GUILayout.Width(100)))
            {
                ClearResult();

                hasSearched = true;
                SearchEvents();
            }
            EditorGUILayout.EndHorizontal();

            searchComponent = EditorGUILayout.TextField("Component Name:", searchComponent);

            includeProjectPrefabs = EditorGUILayout.Toggle("Include Project Prefabs", includeProjectPrefabs);
            searchAllScenes = EditorGUILayout.Toggle("Search All Scenes", searchAllScenes);
            caseSensitive = EditorGUILayout.Toggle("Case Sensitive", caseSensitive);

            EditorGUILayout.Space();

            if (hasSearched)
            {
                if (foundEvents.Any())
                {
                    EditorGUILayout.LabelField($"Found {foundEvents.Count} results", EditorStyles.boldLabel);
                }
                else
                {
                    EditorGUILayout.HelpBox("No results found.", MessageType.Info);
                }
            }

            scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

            foreach (var result in foundEvents)
            {
                EditorGUILayout.BeginVertical(EditorStyles.helpBox);

                EditorGUILayout.BeginHorizontal();
                GUI.enabled = false;
                var displayObject = result.gameObject;
                if (displayObject == null && EditorSceneManager.GetActiveScene().path == result.scenePath)
                {
                    // 現在のシーンが検索結果のシーンと同じ場合は、参照を更新試行
                    displayObject = result.FindGameObjectInScene();
                    result.gameObject = displayObject; // 参照を更新
                }
                EditorGUILayout.ObjectField(displayObject, typeof(GameObject), true);
                GUI.enabled = true;

                // OnGUI内のSelect処理を修正
                if (GUILayout.Button("Select", GUILayout.Width(100)))
                {
                    if (result.isPrefab)
                    {
                        // Prefabアセットを開く
                        AssetDatabase.OpenAsset(AssetDatabase.LoadAssetAtPath<GameObject>(result.prefabPath));

                        // 少し待ってからオブジェクトを選択(Prefabが開かれるのを待つため)
                        EditorApplication.delayCall += () =>
                        {
                            // Prefabステージ内の該当オブジェクトを見つける
                            var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
                            if (prefabStage != null)
                            {
                                var targetPath = result.hierarchyPath;
                                var targetObject = prefabStage.prefabContentsRoot;

                                // パスに基づいてオブジェクトを検索
                                var pathParts = targetPath.Split('/');
                                for (int i = 1; i < pathParts.Length; i++) // 最初のパーツはルートなのでスキップ
                                {
                                    var child = targetObject.transform.Find(string.Join("/", pathParts.Skip(i)));
                                    if (child != null)
                                    {
                                        targetObject = child.gameObject;
                                        break;
                                    }
                                }

                                Selection.activeGameObject = targetObject;
                                EditorGUIUtility.PingObject(targetObject);
                            }
                        };
                    }
                    else
                    {
                        // Prefabステージが開いていれば閉じる
                        var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
                        if (prefabStage != null)
                        {
                            if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
                            {
                                StageUtility.GoToMainStage();
                            }
                            else
                            {
                                return; // キャンセルされた場合は処理を中断
                            }
                        }

                        if (!string.IsNullOrEmpty(result.scenePath) && result.scenePath != EditorSceneManager.GetActiveScene().path)
                        {

                            if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
                            {
                                EditorSceneManager.OpenScene(result.scenePath);
                            }
                        }
                    }


                    if (result.gameObject == null
                        && !string.IsNullOrEmpty(result.scenePath)
                        && result.scenePath == EditorSceneManager.GetActiveScene().path)
                    {
                        result.gameObject = result.FindGameObjectInScene();
                    }

                    if (result.gameObject != null)
                    {
                        Selection.activeGameObject = result.gameObject;
                        EditorGUIUtility.PingObject(result.gameObject);
                    }
                }

                EditorGUILayout.EndHorizontal();

                EditorGUILayout.LabelField($"Component: {result.componentName}");
                EditorGUILayout.LabelField($"Target Component: {result.targetComponentName}");
                EditorGUILayout.LabelField($"Event: {result.eventName}");
                EditorGUILayout.LabelField($"Method: {result.methodName}");
                if (!string.IsNullOrEmpty(result.targetObject))
                {
                    EditorGUILayout.LabelField($"Target: {result.targetObject}");
                }
                EditorGUILayout.LabelField($"Path: {result.hierarchyPath}");

                if (!string.IsNullOrEmpty(result.scenePath))
                {
                    EditorGUILayout.LabelField($"Scene: {System.IO.Path.GetFileNameWithoutExtension(result.scenePath)}");
                }

                if (result.isPrefab)
                {
                    EditorGUILayout.LabelField($"Prefab: {result.prefabPath}");
                }

                EditorGUILayout.EndVertical();
                EditorGUILayout.Space();
            }

            EditorGUILayout.EndScrollView();
            EditorGUILayout.EndVertical();
        }

        private void SearchEvents()
        {
            foundEvents.Clear();

            if (string.IsNullOrEmpty(searchMethod) && string.IsNullOrEmpty(searchComponent))
            {
                return;
            }

            EditorUtility.DisplayProgressBar("Searching Events", "Searching in scenes...", 0f);

            try
            {
                if (searchAllScenes)
                {
                    SearchInAllScenes();
                }
                else
                {
                    SearchInScene();
                }

                if (!EditorApplication.isPlaying)
                {
                    SearchInPrefabs();
                }
            }
            finally
            {
                EditorUtility.ClearProgressBar();
            }
        }

        private void SearchInAllScenes()
        {
            if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
            {
                return;
            }

            string currentScenePath = EditorSceneManager.GetActiveScene().path;

            string[] allScenePaths = AssetDatabase.FindAssets("t:Scene")
                .Select(guid => AssetDatabase.GUIDToAssetPath(guid))
                .Where(path =>
                    !path.StartsWith("Packages/") &&
                    !path.StartsWith("ProjectSettings/") &&
                    !path.StartsWith("Assets/Plugins/"))
                .ToArray();

            float progress = 0;
            float total = allScenePaths.Length;

            foreach (string scenePath in allScenePaths)
            {
                try
                {
                    EditorUtility.DisplayProgressBar("Searching Events", $"Searching in scene: {scenePath}", progress / total);

                    var scene = EditorSceneManager.OpenScene(scenePath);

                    if (!scene.isLoaded)
                    {
                        Debug.LogWarning($"Scene {scenePath} failed to load completely");
                        continue;
                    }

                    // シーン内のオブジェクトを一度だけ収集
                    var allObjects = new HashSet<GameObject>();  // HashSetを使用して重複を防ぐ

                    // シーンのルートオブジェクトを取得
                    foreach (var root in scene.GetRootGameObjects())
                    {
                        // GetComponentsInChildrenを使用せず、Transform.hierarchyで走査
                        var transform = root.transform;
                        var queue = new Queue<Transform>();
                        queue.Enqueue(transform);

                        while (queue.Count > 0)
                        {
                            var current = queue.Dequeue();
                            allObjects.Add(current.gameObject);

                            for (int i = 0; i < current.childCount; i++)
                            {
                                queue.Enqueue(current.GetChild(i));
                            }
                        }
                    }

                    Debug.Log($"Scene {scenePath}: Found {allObjects.Count} objects");

                    foreach (var go in allObjects)
                    {
                        if (go != null)
                        {
                            Debug.Log($"Processing {go.name} - Active: {go.activeInHierarchy}, IsSceneObject: {!EditorUtility.IsPersistent(go)}, Path: {GetHierarchyPath(go)}");

                            // SearchInGameObjectを呼び出す前にシーンオブジェクトであることを確認
                            if (!EditorUtility.IsPersistent(go))
                            {
                                SearchInGameObject(go, false, "", scenePath);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.LogError($"Failed to process scene {scenePath}: {ex}");
                }

                progress++;
            }

            if (!string.IsNullOrEmpty(currentScenePath))
            {
                EditorSceneManager.OpenScene(currentScenePath);
            }
        }

        private void SearchInScene()
        {
            var allObjects = new List<GameObject>();
            foreach (var obj in Resources.FindObjectsOfTypeAll<GameObject>())
            {
                if (obj.scene.isLoaded)
                {
                    allObjects.Add(obj);
                }
            }

            float progress = 0;
            float total = allObjects.Count;

            string currentScenePath = EditorSceneManager.GetActiveScene().path;

            foreach (var go in allObjects)
            {
                EditorUtility.DisplayProgressBar("Searching Events", $"Searching in {go.name}...", progress / total);
                SearchInGameObject(go, false, "", currentScenePath);
                progress++;
            }
        }

        private void SearchInPrefabs()
        {
            string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab");
            float progressStep = 1f / prefabGuids.Length;
            float currentProgress = 0f;

            foreach (string guid in prefabGuids)
            {
                string prefabPath = AssetDatabase.GUIDToAssetPath(guid);
                EditorUtility.DisplayProgressBar("Searching Events", $"Searching in prefab: {prefabPath}", currentProgress);

                GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
                if (prefab != null && includeProjectPrefabs)
                {
                    // Prefabのルートオブジェクト自身を検索
                    SearchInGameObject(prefab, true, prefabPath, "");

                    // 子オブジェクトを検索
                    foreach (Transform child in prefab.GetComponentsInChildren<Transform>(true))
                    {
                        if (child.gameObject != prefab) // ルートオブジェクトは既に検索済みなのでスキップ
                        {
                            SearchInGameObject(child.gameObject, true, prefabPath, "");
                        }
                    }
                }

                currentProgress += progressStep;
            }
        }

        private void SearchInGameObject(GameObject go, bool isPrefab, string prefabPath, string scenePath)
        {
            var components = go.GetComponents<Component>();
            foreach (var component in components)
            {
                if (component == null) continue;

                using (SerializedObject serializedComponent = new SerializedObject(component))
                {
                    SerializedProperty iterator = serializedComponent.GetIterator();
                    string eventName = "";

                    while (iterator.NextVisible(true))
                    {
                        if (iterator.name == "m_MethodName")
                        {
                            string methodName = iterator.stringValue;
                            bool methodMatches = string.IsNullOrEmpty(searchMethod) ||
                                (caseSensitive
                                    ? methodName.Equals(searchMethod)
                                    : methodName.Equals(searchMethod, StringComparison.OrdinalIgnoreCase));

                            if (methodMatches)
                            {
                                // イベント関連の情報も詳細に記録
                                Debug.Log($"Found event on {go.name} - Method: {methodName}");
                                string methodPath = iterator.propertyPath;
                                eventName = methodPath.Split('.')[0];
                                string targetPath = methodPath.Replace("m_MethodName", "m_Target");
                                var targetProp = serializedComponent.FindProperty(targetPath);
                                string targetComponentName = "";

                                if (targetProp != null && targetProp.objectReferenceValue != null)
                                {
                                    var targetComponent = targetProp.objectReferenceValue as Component;
                                    if (targetComponent != null)
                                    {
                                        targetComponentName = targetComponent.GetType().Name;
                                        Debug.Log($"Target Component: {targetComponentName} on {targetComponent.gameObject.name}");
                                    }
                                }

                                bool componentMatches = string.IsNullOrEmpty(searchComponent) ||
                                    (caseSensitive
                                    ? targetComponentName.Equals(searchComponent)
                                    : targetComponentName.Equals(searchComponent, StringComparison.OrdinalIgnoreCase));

                                if (componentMatches)
                                {
                                    // 検索結果を格納する前の状態を記録
                                    Debug.Log($"Adding to results - GameObject: {go.name}, Path: {GetHierarchyPath(go)}, Scene: {scenePath}");

                                    foundEvents.Add(new SearchResult
                                    {
                                        gameObject = go,
                                        componentName = component.GetType().Name,
                                        methodName = methodName,
                                        hierarchyPath = GetHierarchyPath(go),
                                        eventName = eventName,
                                        isPrefab = isPrefab,
                                        prefabPath = prefabPath,
                                        scenePath = scenePath,
                                        globalObjectID = GlobalObjectId.GetGlobalObjectIdSlow(go)
                                    });
                                }
                            }
                        }
                    }
                }
            }
        }

        private string GetHierarchyPath(GameObject obj)
        {
            string path = obj.name;
            Transform parent = obj.transform.parent;

            while (parent != null)
            {
                path = parent.name + "/" + path;
                parent = parent.parent;
            }

            return path;
        }

        private void ClearResult()
        {
            foundEvents.Clear();
            hasSearched = false;
        }
    }
}

使い方

適当なEditorフォルダ配下に保存すると上部メニューにTools > CatHut > Event Method Search が表示されるのでクリック

下記ウィンドウが表示されます。

SearchMethod:検索したいメソッド名(要:完全一致)
ComponentName:メソッドが属するコンポーネント名(指定がない場合メソッド名の一致で検索)
Include Project Prefabs:プロジェクト内のprefabを検索対象に含めるか
Search All Scenes:全てのシーンを検索するか(チェックしない場合今開いているシーンを検索)
Case Sensitive:大文字小文字の区別ON/OFF

上記を指定してSearchを押すと検索結果が表示されます。

結果表示例)

Selectというボタンを押すと、該当のGameObjectが選択されます。
Prefab内の場合はPrefabを開いて選択し、別シーンの場合はそのシーンを開いて選択する挙動としています。

注意点

探索先のコンポーネントがm_MethodNameというプロパティ名でイベント関数を保持していることが前提となります。
独自実装したコンポーネント等でプロパティ名が異なる場合には検索されませんのでご注意ください。

その場合にはSearchInGameObjectメソッド内をいじって検索対象を広げるなどしてください。

GlobalObjectIdが使えるのが2019.4以降のようなので、それ以前のバージョンでは使用できません。ご注意ください。

さいごに

作ってみた結果地味に便利そうな気がしているので、ご興味ある方に使用していただければ幸いです。

コメント

タイトルとURLをコピーしました