3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】メソッド名からそれを呼んでいるGameObjectを検索するエディタ拡張

Last updated at Posted at 2025-11-30

はじめに

この記事は、Unity Advent Calendar 2025 1日目の記事です。

Unityでは、インスペクタでUnityEventで呼び出すメソッドを指定することができます。

スクリーンショット 2025-11-14 115348.png

(私はVisual StudioでResharperというプラグインを使用しているのですが、)
Visual Studioでインスペクタで指定したメソッドを見てみると、
そのメソッドがUnityエディタで指定されていることはわかるものの、いったいどこで指定されているのかはわかりません。

スクリーンショット 2025-11-14 115531.png

そのように、メソッドがどのGameObjectのどのコンポーネントから呼び出されているかを調べる方法が無くて、不便に思ったことがある方もいるのではないでしょうか。

なのでメソッド名から、そのメソッドがどこで使われているかを検索することができるようにする機能をエディタ拡張で作成してみました。

エディタ拡張のソースコード

このスクリプトはEditorフォルダを作ってその下に置いてください。

MethodUsageFinderWindow.cs

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Reflection;
using UnityEditor;
using UnityEditor.SceneManagement;    // PrefabStageUtility
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Events;

public class MethodUsageFinderWindow : EditorWindow
{
    [Serializable]
    private class SearchResult
    {
        public string sceneName;         // Scene name / "(Prefab Asset)" / "(Prefab YAML)" / "(Open Prefab Stage)"
        public GameObject gameObject;    // Hit GameObject (null for YAML fallback)
        public Component component;      // Hit Component (null for YAML fallback)
        public string componentTypeName; // Component type name or "(GameObject)" etc.
        public string matchKind;         // "GameObject Name" / "Component Type" / "Serialized String" / "UnityEvent Method" / "Prefab Text (YAML fallback)"
        public string propertyPath;      // SerializedProperty.path or UnityEvent field name
        public string valuePreview;      // Preview of value (method name etc.)
        public string prefabPath;        // Prefab asset path (if hit is from prefab)

        // For UnityEvent hits: which class the method belongs to
        public string methodOwnerTypeName;   // e.g. "Srm.SrmMoveElevator"
    }

    private string searchText = "";
    private Vector2 scrollPos;
    private readonly List<SearchResult> results = new List<SearchResult>();

    private readonly HashSet<string> prefabHasStructuredHit = new HashSet<string>();
    private readonly List<string> cachedPrefabPaths = new List<string>();
    private readonly HashSet<string> resultKeys = new HashSet<string>();

    // In normal mode, whether to also search all .prefab under Assets/
    [SerializeField] private bool includePrefabAssetsInNormalMode = false;

    [MenuItem("Tools/Method Usage Finder")]
    public static void Open()
    {
        var window = GetWindow<MethodUsageFinderWindow>("Method Usage Finder");
        window.Show();
    }

    private void OnGUI()
    {
        EditorGUILayout.LabelField("Method Usage Finder", EditorStyles.boldLabel);
        EditorGUILayout.Space();

        // Input label
        EditorGUILayout.LabelField("Method Name:");
        searchText = EditorGUILayout.TextField(searchText);

        EditorGUILayout.Space();

        // Detect Prefab Mode
        var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
        bool isPrefabMode = prefabStage != null;

        // In Prefab Mode, force toggle OFF and disable it
        if (isPrefabMode)
        {
            includePrefabAssetsInNormalMode = false;
        }

        using (new EditorGUI.DisabledScope(isPrefabMode))
        {
            includePrefabAssetsInNormalMode = EditorGUILayout.ToggleLeft(
                "Also search all Prefab assets under \"Assets/\" (normal mode only)",
                includePrefabAssetsInNormalMode
            );
        }

        EditorGUILayout.Space();

        using (new EditorGUI.DisabledScope(string.IsNullOrEmpty(searchText)))
        {
            if (GUILayout.Button("Search"))
            {
                SearchAll();
            }
        }

        EditorGUILayout.Space();
        EditorGUILayout.LabelField($"Results: {results.Count}", EditorStyles.boldLabel);
        EditorGUILayout.Space();

        using (var scroll = new EditorGUILayout.ScrollViewScope(scrollPos))
        {
            scrollPos = scroll.scrollPosition;

            foreach (var r in results)
            {
                EditorGUILayout.BeginVertical("box");

                EditorGUILayout.LabelField($"Source : {r.sceneName}");
                if (!string.IsNullOrEmpty(r.prefabPath))
                {
                    EditorGUILayout.LabelField($"Prefab : {r.prefabPath}");
                }

                if (r.gameObject != null)
                {
                    EditorGUILayout.LabelField($"GameObj: {r.gameObject.name}");
                }

                if (!string.IsNullOrEmpty(r.componentTypeName))
                {
                    EditorGUILayout.LabelField($"Component: {r.componentTypeName}  ({r.matchKind})");
                }
                else
                {
                    EditorGUILayout.LabelField($"Match   : {r.matchKind}");
                }

                if (!string.IsNullOrEmpty(r.propertyPath))
                {
                    EditorGUILayout.LabelField($"Property: {r.propertyPath}");
                }

                if (!string.IsNullOrEmpty(r.valuePreview))
                {
                    EditorGUILayout.LabelField($"Value  : {r.valuePreview}");
                }

                if (!string.IsNullOrEmpty(r.methodOwnerTypeName))
                {
                    EditorGUILayout.LabelField($"Method Class: {r.methodOwnerTypeName}");
                }

                EditorGUILayout.Space();

                EditorGUILayout.BeginHorizontal();
                if (r.gameObject != null)
                {
                    if (GUILayout.Button("Ping GameObject", GUILayout.Width(130)))
                    {
                        Selection.activeGameObject = r.gameObject;
                        EditorGUIUtility.PingObject(r.gameObject);
                    }
                }

                if (!string.IsNullOrEmpty(r.prefabPath))
                {
                    if (GUILayout.Button("Ping Prefab", GUILayout.Width(130)))
                    {
                        var prefabObj = AssetDatabase.LoadMainAssetAtPath(r.prefabPath);
                        if (prefabObj != null)
                        {
                            Selection.activeObject = prefabObj;
                            EditorGUIUtility.PingObject(prefabObj);
                        }
                    }
                }

                EditorGUILayout.EndHorizontal();

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

    // ==============================
    // Entry point
    // ==============================
    private void SearchAll()
    {
        results.Clear();
        prefabHasStructuredHit.Clear();
        cachedPrefabPaths.Clear();
        resultKeys.Clear();

        if (string.IsNullOrEmpty(searchText))
        {
            Debug.LogWarning("MethodUsageFinder: search text is empty.");
            return;
        }

        string searchLower = searchText.ToLowerInvariant();
        string searchExact = searchText;

        try
        {
            var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();

            if (prefabStage != null)
            {
                SearchInPrefabMode(prefabStage, searchLower, searchExact);
            }
            else
            {
                SearchInNormalMode(searchLower, searchExact);
            }
        }
        finally
        {
            EditorUtility.ClearProgressBar();
        }

        Debug.Log($"MethodUsageFinder: Found {results.Count} result(s) for \"{searchText}\".");
        Repaint();
    }

    // ==============================
    // Result de-duplication
    // ==============================
    private bool RegisterResultKey(
        string sceneName,
        string prefabPath,
        GameObject go,
        string componentTypeName,
        string matchKind,
        string propertyPath,
        string valuePreview
    )
    {
        string sceneOrPrefabId = !string.IsNullOrEmpty(prefabPath) ? prefabPath : sceneName;
        string goPath = GetGameObjectPath(go);

        string key = $"{sceneOrPrefabId}|{goPath}|{componentTypeName}|{matchKind}|{propertyPath}|{valuePreview}";
        return resultKeys.Add(key);
    }

    private static string GetGameObjectPath(GameObject go)
    {
        if (go == null)
            return "";

        string path = go.name;
        Transform t = go.transform.parent;
        while (t != null)
        {
            path = t.name + "/" + path;
            t = t.parent;
        }
        return path;
    }

    // ==============================
    // Normal mode: open scenes + optional all prefabs
    // ==============================
    private void SearchInNormalMode(string searchLower, string searchExact)
    {
        SearchInOpenScenesSerialized(searchLower, searchExact);

        if (includePrefabAssetsInNormalMode)
        {
            SearchInAllPrefabsSerialized(searchLower, searchExact);
            SearchInAllPrefabsYamlFallback(searchExact);
        }
    }

    // ==============================
    // Prefab mode: only open prefab hierarchy
    // ==============================
    private void SearchInPrefabMode(PrefabStage stage, string searchLower, string searchExact)
    {
        var root = stage.prefabContentsRoot;
        if (root == null)
            return;

        bool cancel = EditorUtility.DisplayCancelableProgressBar(
            "Searching in open prefab...",
            $"Prefab Stage: {stage.assetPath}",
            0.1f
        );

        if (cancel)
        {
            Debug.Log("MethodUsageFinder: Prefab stage search cancelled.");
            return;
        }

        SearchInGameObjectHierarchy(
            sceneName: "(Open Prefab Stage)",
            root: root,
            searchLower: searchLower,
            searchExact: searchExact,
            isPrefabAsset: false,
            prefabPath: stage.assetPath
        );
    }

    // ==============================
    // Open scenes search
    // ==============================
    private void SearchInOpenScenesSerialized(string searchLower, string searchExact)
    {
        int sceneCount = SceneManager.sceneCount;
        int processedScenes = 0;

        for (int i = 0; i < sceneCount; i++)
        {
            Scene scene = SceneManager.GetSceneAt(i);
            if (!scene.isLoaded)
                continue;

            processedScenes++;

            bool cancel = EditorUtility.DisplayCancelableProgressBar(
                "Searching in open scenes...",
                $"Scene: {scene.name} ({processedScenes}/{sceneCount})",
                (float)processedScenes / Math.Max(1, sceneCount)
            );

            if (cancel)
            {
                Debug.Log("MethodUsageFinder: Scene search cancelled.");
                break;
            }

            SearchInScene(scene, searchLower, searchExact);
        }
    }

    private void SearchInScene(Scene scene, string searchLower, string searchExact)
    {
        GameObject[] roots = scene.GetRootGameObjects();
        foreach (var root in roots)
        {
            SearchInGameObjectHierarchy(
                sceneName: scene.name,
                root: root,
                searchLower: searchLower,
                searchExact: searchExact,
                isPrefabAsset: false,
                prefabPath: ""
            );
        }
    }

    // ==============================
    // Common: recursive hierarchy search
    // ==============================
    private void SearchInGameObjectHierarchy(
        string sceneName,
        GameObject root,
        string searchLower,
        string searchExact,
        bool isPrefabAsset,
        string prefabPath
    )
    {
        foreach (var t in root.GetComponentsInChildren<Transform>(true))
        {
            GameObject gameObject = t.gameObject;

            // GameObject name (partial match)
            if (!string.IsNullOrEmpty(gameObject.name) &&
                gameObject.name.ToLowerInvariant().Contains(searchLower))
            {
                if (RegisterResultKey(sceneName, prefabPath, gameObject,
                                      isPrefabAsset ? "(Prefab GameObject)" : "(GameObject)",
                                      "GameObject Name", "", gameObject.name))
                {
                    results.Add(new SearchResult
                    {
                        sceneName = sceneName,
                        gameObject = gameObject,
                        component = null,
                        componentTypeName = isPrefabAsset ? "(Prefab GameObject)" : "(GameObject)",
                        matchKind = "GameObject Name",
                        propertyPath = "",
                        valuePreview = gameObject.name,
                        prefabPath = prefabPath
                    });

                    if (isPrefabAsset && !string.IsNullOrEmpty(prefabPath))
                        prefabHasStructuredHit.Add(prefabPath);
                }
            }

            // Components
            Component[] components = gameObject.GetComponents<Component>();
            foreach (var comp in components)
            {
                if (comp == null)
                    continue; // Missing Script

                string typeName = comp.GetType().Name;

                // Component type name (partial match)
                if (!string.IsNullOrEmpty(typeName) &&
                    typeName.ToLowerInvariant().Contains(searchLower))
                {
                    if (RegisterResultKey(sceneName, prefabPath, gameObject, typeName,
                                          "Component Type", "", typeName))
                    {
                        results.Add(new SearchResult
                        {
                            sceneName = sceneName,
                            gameObject = gameObject,
                            component = comp,
                            componentTypeName = typeName,
                            matchKind = "Component Type",
                            propertyPath = "",
                            valuePreview = typeName,
                            prefabPath = prefabPath
                        });

                        if (isPrefabAsset && !string.IsNullOrEmpty(prefabPath))
                            prefabHasStructuredHit.Add(prefabPath);
                    }
                }

                // Serialized string properties (token-level exact match)
                try
                {
                    SerializedObject so = new SerializedObject(comp);
                    SerializedProperty prop = so.GetIterator();

                    if (prop.NextVisible(true))
                    {
                        do
                        {
                            if (prop.propertyType == SerializedPropertyType.String)
                            {
                                // Skip UnityEvent's m_MethodName to avoid duplicate with UnityEvent Method result
                                if (prop.propertyPath.EndsWith(".m_MethodName", StringComparison.Ordinal))
                                    continue;

                                string value = prop.stringValue;
                                if (StringHasToken(value, searchExact))
                                {
                                    string preview = value;
                                    const int maxLen = 60;
                                    if (preview.Length > maxLen)
                                    {
                                        preview = preview.Substring(0, maxLen) + "...";
                                    }

                                    if (RegisterResultKey(sceneName, prefabPath, gameObject, typeName,
                                                          "Serialized String", prop.propertyPath, preview))
                                    {
                                        results.Add(new SearchResult
                                        {
                                            sceneName = sceneName,
                                            gameObject = gameObject,
                                            component = comp,
                                            componentTypeName = typeName,
                                            matchKind = "Serialized String",
                                            propertyPath = prop.propertyPath,
                                            valuePreview = preview,
                                            prefabPath = prefabPath
                                        });

                                        if (isPrefabAsset && !string.IsNullOrEmpty(prefabPath))
                                            prefabHasStructuredHit.Add(prefabPath);
                                    }
                                }
                            }
                        } while (prop.NextVisible(true));
                    }
                }
                catch (Exception e)
                {
                    Debug.LogWarning(
                        $"MethodUsageFinder: Failed to inspect component {typeName} on {gameObject.name}: {e.Message}"
                    );
                }

                // UnityEvent method names
                TryMatchUnityEvents(
                    sceneName: sceneName,
                    gameObject: gameObject,
                    component: comp,
                    typeName: typeName,
                    prefabPath: prefabPath,
                    searchExact: searchExact,
                    isPrefabAsset: isPrefabAsset
                );
            }
        }
    }

    // ==============================
    // UnityEvent method matching (with owning class)
    // ==============================
    private void TryMatchUnityEvents(
        string sceneName,
        GameObject gameObject,
        Component component,
        string typeName,
        string prefabPath,
        string searchExact,
        bool isPrefabAsset
    )
    {
        if (component == null)
            return;

        var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
        var fields = component.GetType().GetFields(flags);

        foreach (var field in fields)
        {
            if (!typeof(UnityEventBase).IsAssignableFrom(field.FieldType))
                continue;

            var evt = field.GetValue(component) as UnityEventBase;
            if (evt == null)
                continue;

            int count = evt.GetPersistentEventCount();
            for (int i = 0; i < count; i++)
            {
                string methodName = evt.GetPersistentMethodName(i);
                if (string.IsNullOrEmpty(methodName))
                    continue;

                if (!methodName.Equals(searchExact, StringComparison.OrdinalIgnoreCase))
                    continue;

                // Determine which class owns the method
                UnityEngine.Object targetObj = evt.GetPersistentTarget(i);
                string ownerTypeName = null;
                if (targetObj != null)
                {
                    var targetType = targetObj.GetType();
                    ownerTypeName = !string.IsNullOrEmpty(targetType.FullName)
                        ? targetType.FullName
                        : targetType.Name;
                }

                string propLabel = $"{field.Name}[{i}]";

                if (RegisterResultKey(sceneName, prefabPath, gameObject, typeName,
                                      "UnityEvent Method", propLabel, methodName))
                {
                    results.Add(new SearchResult
                    {
                        sceneName = sceneName,
                        gameObject = gameObject,
                        component = component,
                        componentTypeName = typeName,
                        matchKind = "UnityEvent Method",
                        propertyPath = propLabel,
                        valuePreview = methodName,
                        prefabPath = prefabPath,
                        methodOwnerTypeName = ownerTypeName
                    });

                    if (isPrefabAsset && !string.IsNullOrEmpty(prefabPath))
                        prefabHasStructuredHit.Add(prefabPath);
                }
            }
        }
    }

    // ==============================
    // All prefab search (normal mode)
    // ==============================
    private void SearchInAllPrefabsSerialized(string searchLower, string searchExact)
    {
        string[] allAssetPaths = AssetDatabase.GetAllAssetPaths();

        foreach (var path in allAssetPaths)
        {
            if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
                continue;

            if (!path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
                continue;

            cachedPrefabPaths.Add(path);
        }

        int total = cachedPrefabPaths.Count;
        for (int i = 0; i < total; i++)
        {
            string path = cachedPrefabPaths[i];

            bool cancel = EditorUtility.DisplayCancelableProgressBar(
                "Searching in prefabs (structured)...",
                $"Prefab: {path} ({i + 1}/{total})",
                (float)(i + 1) / Math.Max(1, total)
            );

            if (cancel)
            {
                Debug.Log("MethodUsageFinder: Prefab structured search cancelled.");
                break;
            }

            var prefabRoot = AssetDatabase.LoadAssetAtPath<GameObject>(path);
            if (prefabRoot == null)
                continue;

            string sceneName = "(Prefab Asset)";
            SearchInGameObjectHierarchy(
                sceneName: sceneName,
                root: prefabRoot,
                searchLower: searchLower,
                searchExact: searchExact,
                isPrefabAsset: true,
                prefabPath: path
            );
        }
    }

    private void SearchInAllPrefabsYamlFallback(string searchExact)
    {
        SearchPrefabListYamlFallback(cachedPrefabPaths, searchExact, "Searching in prefabs (YAML fallback)...");
    }

    private void CollectPrefabAssetPathsFromHierarchy(GameObject root, HashSet<string> paths)
    {
        foreach (var t in root.GetComponentsInChildren<Transform>(true))
        {
            GameObject go = t.gameObject;

            var status = PrefabUtility.GetPrefabInstanceStatus(go);
            if (status == PrefabInstanceStatus.NotAPrefab)
                continue;

            var prefabRoot = PrefabUtility.GetCorrespondingObjectFromOriginalSource(go);
            if (prefabRoot == null)
                continue;

            string path = AssetDatabase.GetAssetPath(prefabRoot);
            if (string.IsNullOrEmpty(path))
                continue;

            // Exclude .fbx etc., only .prefab
            if (!path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
                continue;

            paths.Add(path);
        }
    }

    private void SearchPrefabListYamlFallback(IEnumerable<string> prefabPaths, string searchExact, string progressTitle)
    {
        var list = new List<string>(prefabPaths);
        int total = list.Count;

        for (int i = 0; i < total; i++)
        {
            string path = list[i];

            if (prefabHasStructuredHit.Contains(path))
                continue;

            bool cancel = EditorUtility.DisplayCancelableProgressBar(
                progressTitle,
                $"Prefab: {path} ({i + 1}/{total})",
                (float)(i + 1) / Math.Max(1, total)
            );

            if (cancel)
            {
                Debug.Log("MethodUsageFinder: Prefab YAML fallback cancelled.");
                break;
            }

            try
            {
                string text = File.ReadAllText(path);

                if (StringHasToken(text, searchExact))
                {
                    if (RegisterResultKey("(Prefab YAML)", path, null, "", "Prefab Text (YAML fallback)", "", ""))
                    {
                        results.Add(new SearchResult
                        {
                            sceneName = "(Prefab YAML)",
                            gameObject = null,
                            component = null,
                            componentTypeName = "",
                            matchKind = "Prefab Text (YAML fallback)",
                            propertyPath = "",
                            valuePreview = "",
                            prefabPath = path
                        });
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogWarning($"MethodUsageFinder: Failed to read prefab {path}: {e.Message}");
            }
        }
    }

    // ==============================
    // Token-level exact match utility
    // ==============================
    private static bool StringHasToken(string value, string searchExact)
    {
        if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(searchExact))
            return false;

        var tokens = Regex.Split(value, @"\W+");
        foreach (var token in tokens)
        {
            if (token.Length == 0) continue;
            if (token.Equals(searchExact, StringComparison.OrdinalIgnoreCase))
                return true;
        }

        return false;
    }
}
#endif

使い方

メニューバーの
Tools > Method Usage Finder
で、検索ウィンドウが開きます。

スクリーンショット 2025-11-14 113649.png

Unityエディタでシーンが開かれているときはシーンの中を、
プレハブが開かれているときはプレハブの中を検索します。

スクリーンショット 2025-11-14 120413.png

Unityエディタでシーンが開かれているときだけ、
Also search all Prefab assets under Assets/ (normal mode only)
と書かれているスイッチをONにすると、
プロジェクトのAssetsフォルダ以下にある全てのプレハブの中も検索します。

スクリーンショット 2025-11-14 120542.png

おわりに

UnityEvent は便利な反面、「どこから呼ばれているのか」を後から追いかけるのが意外と大変です。今回紹介したエディタ拡張が、プロジェクトの整理やデバッグの手助けになれば幸いです。

もし改良案や「こういう検索にも対応してほしい」などの要望があれば、ぜひ気軽にコメントしてください。引き続き、Unityの開発がより快適になるようなツールを作っていければと思います。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?