【Unity】ChatGPT によるバグ取りを効率的に行うための Editor 拡張

ChatGPT の有料版をご利用中の方であれば、質問時により多くの文字数を扱うことができます。  
これは、バグ修正や機能実装の詳細を記述する際に非常に有効です。

そんな ChatGPT を最大限活用するために、  
**`.unity` ファイルに紐づいている `.cs` スクリプトのファイル名と中身をまとめてクリップボードにコピーする**  
Editor 拡張を作成しました。

ChatGPT に質問したいシーンファイルを右クリックして  
`Tools > SephirothChatGPTPromptCreator > CSOnly`  
を選択するだけで、関連する `.cs` ファイルの中身を**一括コピー**できます。

この拡張を使うと、怪しい `.cs` ファイル 1 個 1 個に対して質問する必要がなくなるため、 ChatGPT の活用が楽になります。

ここでは、その拡張機能について紹介します。

サクっと導入したい方は
[unitypackage 版](https://drive.google.com/file/d/1wEBJEuAIgWmCLMpWcnfKLwcWe4JwqWsD/view?usp=drive_link) をご利用ください。

---

```csharp:SephirothChatGPTPromptCreator.cs
#if UNITY_EDITOR

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System.Collections.Generic;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;

public static class SephirothChatGPTPromptCreator
{
    [MenuItem("Assets/Tools/SephirothChatGPTPromptCreator/All", true)]
    private static bool ValidateSelectedUnityFile()
    {
        string path = AssetDatabase.GetAssetPath(Selection.activeObject);
        return path.EndsWith(".unity");
    }

    [MenuItem("Assets/Tools/SephirothChatGPTPromptCreator/All")]
    private static void GeneratePromptFromScene()
    {
        string scenePath = AssetDatabase.GetAssetPath(Selection.activeObject);
        if (!scenePath.EndsWith(".unity"))
        {
            Debug.LogWarning("選択されたファイルは .unity ファイルではありません。");
            return;
        }

        var builder = new StringBuilder();

        // .unity ファイルの内容を追加
        string fullScenePath = Path.Combine(Directory.GetCurrentDirectory(), scenePath);

        var scene = EditorSceneManager.OpenScene(fullScenePath, OpenSceneMode.Single);
        var rootObjects = scene.GetRootGameObjects();

        StringBuilder sb = new StringBuilder();
        sb.AppendLine("{");

        int goCount = 0;
        foreach (var go in rootObjects)
        {
            ExportGameObject(go, sb, "  ", ref goCount);
        }

        sb.AppendLine("}");

        builder.AppendLine(Path.GetFileName(scenePath));
        builder.AppendLine(sb.ToString());
        builder.AppendLine();

        // シーンを開いてすべての MonoBehaviour を取得
        var openedScene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
        var allObjects = openedScene.GetRootGameObjects();

        var uniqueScripts = new HashSet<MonoScript>();

        foreach (var go in allObjects)
        {
            foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
            {
                if (mb == null) continue; // Missing Script の場合
                var script = MonoScript.FromMonoBehaviour(mb);
                if (script != null)
                    uniqueScripts.Add(script);
            }
        }

        // 各スクリプトファイルを読み込み（Assetsフォルダ内のスクリプトのみ対象）
        foreach (var monoScript in uniqueScripts)
        {
            string scriptPath = AssetDatabase.GetAssetPath(monoScript);
            if (!scriptPath.StartsWith("Assets/")) 
                continue; // Unity内部やパッケージのスクリプトは除外

            string fullScriptPath = Path.Combine(Directory.GetCurrentDirectory(), scriptPath);
            builder.AppendLine(Path.GetFileName(scriptPath));
            builder.AppendLine(File.ReadAllText(fullScriptPath));
            builder.AppendLine();
        }

        // クリップボードにコピー
        EditorGUIUtility.systemCopyBuffer = builder.ToString();
        Debug.Log("シーンと関連付けられた .cs スクリプトがクリップボードにコピーされました。");
    }

    private static void ExportGameObject(GameObject go, StringBuilder sb, string indent, ref int count)
    {
        if (count > 0) sb.AppendLine(",");
        sb.AppendLine($"{indent}\"{go.name}\": {{");

        string subIndent = indent + "  ";

        // Transform
        var t = go.transform;
        sb.AppendLine($"{subIndent}\"Transform\": {{");
        sb.AppendLine($"{subIndent}  \"Position\": {{ \"x\": {t.position.x}, \"y\": {t.position.y}, \"z\": {t.position.z} }},");
        sb.AppendLine($"{subIndent}  \"Rotation\": {{ \"x\": {t.eulerAngles.x}, \"y\": {t.eulerAngles.y}, \"z\": {t.eulerAngles.z} }},");
        sb.AppendLine($"{subIndent}  \"Scale\": {{ \"x\": {t.localScale.x}, \"y\": {t.localScale.y}, \"z\": {t.localScale.z} }}");
        sb.AppendLine($"{subIndent}}},");

        // Components
        var components = go.GetComponents<Component>();
        sb.AppendLine($"{subIndent}\"Components\": [");
        for (int i = 0; i < components.Length; i++)
        {
            var comp = components[i];
            if (comp == null) continue;
            string name = comp.GetType().Name;
            if (comp is MonoBehaviour) name += " (script)";
            sb.Append($"{subIndent}  \"{name}\"");
            if (i < components.Length - 1) sb.Append(",");
            sb.AppendLine();
        }
        sb.AppendLine($"{subIndent}]");

        sb.Append($"{indent}}}");

        count++;

        foreach (Transform child in go.transform)
        {
            sb.AppendLine(",");
            ExportGameObject(child.gameObject, sb, indent, ref count);
        }
    }

    [MenuItem("Assets/Tools/SephirothChatGPTPromptCreator/CSOnly", true)]
    private static bool ValidateSelectedUnityFile2()
    {
        string path = AssetDatabase.GetAssetPath(Selection.activeObject);
        return path.EndsWith(".unity");
    }

    [MenuItem("Assets/Tools/SephirothChatGPTPromptCreator/CSOnly")]
    private static void GeneratePromptFromScene2()
    {
        string scenePath = AssetDatabase.GetAssetPath(Selection.activeObject);
        if (!scenePath.EndsWith(".unity"))
        {
            Debug.LogWarning("選択されたファイルは .unity ファイルではありません。");
            return;
        }

        var builder = new StringBuilder();

        // シーンを開いてすべての MonoBehaviour を取得
        var openedScene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
        var allObjects = openedScene.GetRootGameObjects();

        var uniqueScripts = new HashSet<MonoScript>();

        foreach (var go in allObjects)
        {
            foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
            {
                if (mb == null) continue; // Missing Script の場合
                var script = MonoScript.FromMonoBehaviour(mb);
                if (script != null)
                    uniqueScripts.Add(script);
            }
        }

        // 各スクリプトファイルを読み込み（Assetsフォルダ内のスクリプトのみ対象）
        foreach (var monoScript in uniqueScripts)
        {
            string scriptPath = AssetDatabase.GetAssetPath(monoScript);
            if (!scriptPath.StartsWith("Assets/")) 
                continue; // Unity内部やパッケージのスクリプトは除外

            string fullScriptPath = Path.Combine(Directory.GetCurrentDirectory(), scriptPath);
            builder.AppendLine(Path.GetFileName(scriptPath));
            builder.AppendLine(File.ReadAllText(fullScriptPath));
            builder.AppendLine();
        }

        // クリップボードにコピー
        EditorGUIUtility.systemCopyBuffer = builder.ToString();
        Debug.Log("シーンと関連付けられた .cs スクリプトがクリップボードにコピーされました。");
    }
}

#endif
```

`SephirothChatGPTPromptCreator.cs` を作成して
内容を上記の内容にして下さい。

その後、バグが出ているシーンのシーンファイルを右クリックして
`Tools > SephirothChatGPTPromptCreator > CSOnly`
を選択すると、そのシーンに紐づいた全ての `.cs` ファイルの
ファイル名と中身がクリップボードに保存されます。

その後 `ChatGPT` を開いて
「このシーンの〇〇のバグを取りたい」
「このシーンに〇〇の機能を付けたい」
という文章の後に貼り付けて使用して下さい

※文字数制限に引っかかった場合は、20万文字程度ずつ質問して下さい。

***

シーンファイルに関係なく
全 `.cs` ファイルをクリップボードにコピーしたい方は
下記のコードを活用下さい。

小規模のプロジェクトでないと
文字数が大変な事になる点に注意です。

将来的に ChatGPT の文字数制限が
更に拡張された場合に使ってみたい所です

```csharp:SephirothChatGPTPromptCreatorB.cs
#if UNITY_EDITOR

using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
using System.Collections.Generic;
using System.IO;
using System.Text;

public class SephirothChatGPTPromptCreatorB : EditorWindow
{
    [MenuItem("Tools/SephirothChatGPTPromptCreaterB/All")]
    public static void CreatePrompt()
    {
        StringBuilder builder = new StringBuilder();

        // .unityファイルを検索して追加
        string[] unityFiles = Directory.GetFiles(Application.dataPath, "*.unity", SearchOption.AllDirectories);
        foreach (string filePath in unityFiles)
        {
            var scene = EditorSceneManager.OpenScene(filePath, OpenSceneMode.Single);
            var rootObjects = scene.GetRootGameObjects();

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("{");

            int goCount = 0;
            foreach (var go in rootObjects)
            {
                ExportGameObject(go, sb, "  ", ref goCount);
            }

            sb.AppendLine("}");
            builder.AppendLine(Path.GetFileName(filePath));
            builder.AppendLine(sb.ToString());
            builder.AppendLine();
        }

        // .csファイルを検索して追加
        string[] csFiles = Directory.GetFiles(Application.dataPath, "*.cs", SearchOption.AllDirectories);
        foreach (string filePath in csFiles)
        {
            builder.AppendLine(Path.GetFileName(filePath));
            builder.AppendLine(File.ReadAllText(filePath));
            builder.AppendLine();
        }

        // クリップボードにコピー
        EditorGUIUtility.systemCopyBuffer = builder.ToString();

        Debug.Log("全 .unity と .cs ファイルがクリップボードにコピーされました。");
    }

    private static void ExportGameObject(GameObject go, StringBuilder sb, string indent, ref int count)
    {
        if (count > 0) sb.AppendLine(",");
        sb.AppendLine($"{indent}\"{go.name}\": {{");

        string subIndent = indent + "  ";

        // Transform
        var t = go.transform;
        sb.AppendLine($"{subIndent}\"Transform\": {{");
        sb.AppendLine($"{subIndent}  \"Position\": {{ \"x\": {t.position.x}, \"y\": {t.position.y}, \"z\": {t.position.z} }},");
        sb.AppendLine($"{subIndent}  \"Rotation\": {{ \"x\": {t.eulerAngles.x}, \"y\": {t.eulerAngles.y}, \"z\": {t.eulerAngles.z} }},");
        sb.AppendLine($"{subIndent}  \"Scale\": {{ \"x\": {t.localScale.x}, \"y\": {t.localScale.y}, \"z\": {t.localScale.z} }}");
        sb.AppendLine($"{subIndent}}},");

        // Components
        var components = go.GetComponents<Component>();
        sb.AppendLine($"{subIndent}\"Components\": [");
        for (int i = 0; i < components.Length; i++)
        {
            var comp = components[i];
            if (comp == null) continue;
            string name = comp.GetType().Name;
            if (comp is MonoBehaviour) name += " (script)";
            sb.Append($"{subIndent}  \"{name}\"");
            if (i < components.Length - 1) sb.Append(",");
            sb.AppendLine();
        }
        sb.AppendLine($"{subIndent}]");

        sb.Append($"{indent}}}");

        count++;

        foreach (Transform child in go.transform)
        {
            sb.AppendLine(",");
            ExportGameObject(child.gameObject, sb, indent, ref count);
        }
    }

    [MenuItem("Tools/SephirothChatGPTPromptCreaterB/CSOnly")]
    public static void CreatePrompt2()
    {
        StringBuilder builder = new StringBuilder();

        // .unityファイルを検索して追加
        // string[] unityFiles = Directory.GetFiles(Application.dataPath, "*.unity", SearchOption.AllDirectories);
        // foreach (string filePath in unityFiles)
        // {
        //     builder.AppendLine(Path.GetFileName(filePath));
        //     builder.AppendLine(File.ReadAllText(filePath));
        //     builder.AppendLine();
        // }

        // .csファイルを検索して追加
        string[] csFiles = Directory.GetFiles(Application.dataPath, "*.cs", SearchOption.AllDirectories);
        foreach (string filePath in csFiles)
        {
            builder.AppendLine(Path.GetFileName(filePath));
            builder.AppendLine(File.ReadAllText(filePath));
            builder.AppendLine();
        }

        // クリップボードにコピー
        EditorGUIUtility.systemCopyBuffer = builder.ToString();

        Debug.Log("全 .cs ファイルがクリップボードにコピーされました。");
    }
}

#endif
```

***

`.cs` ファイルを右クリックして
そのファイルに紐づく全ファイルを
クリップボードにコピーしたい場合は
下記のコードをご利用ください。

`.cs` ファイルを右クリックし、
`Tools > SephirothChatGPTPromptGeneratorFile` を選択すると、
該当の `.cs` ファイルに関連する可能性のあるファイル名とその中身を、すべてクリップボードにコピーします。

なお、関連ファイルの判定は、
「**そのファイル名が `.cs` ファイル内に記述されているかどうか**」のみを基準に行っています。
そのため、判定の精度が**十分でない可能性があります**。あらかじめご了承ください。

```cs:SephirothChatGPTPromptGeneratorFile.cs
// Assets/Editor/SephirothChatGPTPromptGenerator.cs
using UnityEditor;
using UnityEngine;
using System.IO;
using System.Collections.Generic;
using System.Text;

public static class SephirothChatGPTPromptGenerator
{
    private const string MenuPath = "Assets/Tools/SephirothChatGPTPromptGeneratorFile";

    // メニューの表示 / 非表示を制御
    [MenuItem(MenuPath, true)]
    private static bool Validate()
    {
        var path = AssetDatabase.GetAssetPath(Selection.activeObject);
        return !string.IsNullOrEmpty(path) && path.EndsWith(".cs");
    }

    // 実処理
    [MenuItem(MenuPath)]
    private static void CopyRelatedFilesToClipboard()
    {
        var rootPath = AssetDatabase.GetAssetPath(Selection.activeObject).Replace('\\', '/');
        if (string.IsNullOrEmpty(rootPath) || !rootPath.EndsWith(".cs"))
        {
            Debug.LogError("まず 1 つの .cs ファイルを選択してください。");
            return;
        }

        // Assets 以下の全 .cs を読み込み＆キャッシュ
        var allCsFiles = Directory.GetFiles("Assets", "*.cs", SearchOption.AllDirectories);
        var cache = new Dictionary<string, string>(allCsFiles.Length);
        foreach (var p in allCsFiles)
        {
            var normalizedPath = p.Replace('\\', '/'); // ← normalize!
            cache[normalizedPath] = File.ReadAllText(p);
        }
        var visited = new HashSet<string>();
        var q = new Queue<string>();
        q.Enqueue(rootPath);
        visited.Add(rootPath);

        while (q.Count > 0)
        {
            var xPath = q.Dequeue();
            xPath = xPath.Replace('\\', '/');  // Add this line
            var xName = Path.GetFileNameWithoutExtension(xPath);
            var xContent = cache[xPath];

            foreach (var candidate in allCsFiles)
            {
                var normalizedCandidate = candidate.Replace('\\', '/');
                if (visited.Contains(normalizedCandidate)) continue;

                var cName = Path.GetFileNameWithoutExtension(normalizedCandidate);
                var cContent = cache[normalizedCandidate];

                if (cContent.Contains(xName) || xContent.Contains(cName))
                {
                    visited.Add(normalizedCandidate);
                    q.Enqueue(normalizedCandidate);
                }
            }
        }

        // クリップボードへ
        var sb = new StringBuilder();
        foreach (var path in visited)
        {
            sb.AppendLine($"----- {path} -----");
            sb.AppendLine(cache[path]);
            sb.AppendLine();
        }

        GUIUtility.systemCopyBuffer = sb.ToString();
        Debug.Log($"Copied {visited.Count} file(s) to clipboard.");
    }
}
```

***

# 🎉 おわりに
以上、至極真っ当な Editor 拡張を紹介しました。
この記事が少しでも皆さんの Unity 開発の助けになれば嬉しいです。

最後までご覧いただき、ありがとうございました！

# 追記

2025 年 6 月 3 日、デバックは不十分ですが
All の文字数を抑える修正を行いました。
これにより All を実行するのが現実的になりました。