AIと一緒にアプリを作るなら ― Unity × YAML でプロジェクト構成を自動生成するワークフロー


> **TL;DR**
> 
> - **AI が既存コードを壊す／重複ファイルを作る原因**は “プロジェクト全体像の誤認識”
>     
> - そこで **`project-structure.yaml`** に _最低限のメタ情報_（ファイルパス・クラス・プロパティ・メソッド…）をまとめて AI に読ませると、高確率で正しい前提でコードを生成させられる
>     
> - 本記事では **Unity** 向けに Roslyn + YamlDotNet を使い “ワンクリック自動生成” する方法を解説
>     

## 1. この記事の対象とゴール

Unity プロジェクトを **Cursor / GitHub Copilot** などと協調開発している人向けの記事です。
次のような悩みを抱えていませんか？

- AI が既存スクリプトを無視して新しく作り直してしまう
    
- 型やメンバー名を間違えてビルドが通らないコードを提案してくる
    
- 「プロジェクト全体が大きすぎてコンテキストに入りきらない」
    

本記事を読み終えると――

1. **YAML 1 枚** で AI の前提を固定し、コンテキスト上限問題を回避する方法がわかる。
    
2. Unity に **NuGetForUnity + Roslyn + YamlDotNet** を組み込んで、自動で `project-structure.yaml` を生成できるようになる。
    
3. 自分好みに拡張するポイント（コメント抽出や CI 連携 etc.）のヒントを得られる。

## 2. 完成イメージ

```plantext
MyUnityGame/
├─ Assets/
│  ├─ Editor/
│  │  └─ GenerateProjectStructure.cs   ←★ 今回作成
│  └─ Scripts/…
└─ project-structure.yaml              ←★ 自動生成
```

生成されるyamlファイルの例：

```yaml
project: MyUnityGame
version: 1.0.0
generatedAt: 2025-05-07T13:26:10.816Z
modules:
  - file: Assets/Scripts/Player/PlayerController.cs
    classes:
      PlayerController:
        desc: プレイヤーの移動・アニメーションを管理する MonoBehaviour
        props:
          "public speed: float": ""
          "[SerializeField] private jumpPower: float = 5f": ""
        methods:
          "public Move(input: Vector2): void": ""
```

この YAML を **System Prompt** に貼り付ければ、AI は既存クラスを理解したうえで安全にコードを提案してくれます。

## 3. なぜ YAML が効くのか？

1. **LLM フレンドリー**：階層構造を保ちつつテキスト量が抑えられ、モデルが高速に理解できる。
    
2. **コメントしやすい**：JSON では書きづらい行コメントを残せるので、設計意図を共有しやすい。
    
3. **差分が明快**：Git diff で設計変更をレビューしやすく、PR コメントもスムーズ。
    

要するに **“人間も AI も読める設計図”** として最適なのが YAML というわけです。

## 4. Unity で自動生成スクリプトを動かす

### 必要な環境（例）

|項目|バージョン・設定|
|---|---|
|Unity|2023 LTS 〜 6000.1.2f1|
|Microsoft.CodeAnalysis.CSharp|4.x 系|
|YamlDotNet|16.x 系|
|NuGetForUnity|4.x 系|

### NuGetForUnity で DLL を導入する手順（詳細）

1. **NuGetForUnity を取得**  
    [Releases · GlitchEnzo/NuGetForUnity](https://github.com/GlitchEnzo/NuGetForUnity/releases) から `NuGetForUnity.*.unitypackage` をダウンロード。
    
2. **Unity プロジェクトにインポート**  
    NuGetを使用したいプロジェクトを開いた状態で `.unitypackage` をダブルクリック → _Import_
    ![スクリーンショット 2025-05-08 11.19.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638255/2ea00d91-2dd0-45bb-ad91-80582b034cb7.png)

    
3. メニューに **NuGet** が追加される (追加されない場合はプロジェクトを再起動）
    ![スクリーンショット 2025-05-08 11.20.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638255/f331e512-65fc-460b-83b7-bf938f7220b4.png)


    
4. **NuGet ▸ Manage NuGet Packages** を開く。

    ![スクリーンショット 2025-05-08 11.21.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638255/6f7fa0d6-c7e1-4948-9b45-0a2155e09cd0.png)

    
5. `Microsoft.CodeAnalysis.CSharp` と `YamlDotNet` を検索 → _Install_。
    

> 💡 _Unity 2020 以前_ は `Packages/manifest.json` の競合に注意。CI で差分が出たら手動解決しましょう。

> うまくいかない場合は [ここ](https://github.com/GlitchEnzo/NuGetForUnity) を参照して下さい。

### スクリプトを配置

`Assets/Editor/GenerateProjectStructure.cs` に以下を貼り付けて保存（※全文は記事末尾の Gist）。

```csharp
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

[InitializeOnLoad]
public static class GenerateProjectStructure
{
    private static readonly string OutFile =
        Path.Combine(Application.dataPath, "../project-structure.yaml");

    static GenerateProjectStructure()
    {
        // スクリプトリビルド完了後に自動実行
        CompilationPipeline.compilationFinished += _ => Generate();
    }

    private static void Generate()
    {
        try
        {
            BuildYaml();
            Debug.Log($"[ProjectStructure] Generated → {OutFile}");
        }
        catch (Exception ex)
        {
            Debug.LogError("[ProjectStructure] Generation failed:\n" + ex);
        }
    }

    private static void BuildYaml()
    {
        var modules = new List<object>();

        // Assets 配下の .cs をすべて収集
        var guids = AssetDatabase.FindAssets("t:Script", new[] { "Assets" });
        foreach (var guid in guids)
        {
            var assetPath = AssetDatabase.GUIDToAssetPath(guid);
            if (!assetPath.EndsWith(".cs")) continue;

            var absPath = Path.Combine(Directory.GetCurrentDirectory(), assetPath);
            var tree    = CSharpSyntaxTree.ParseText(File.ReadAllText(absPath));
            var root    = tree.GetRoot();

            var mod = new Dictionary<string, object> { ["file"] = assetPath };

            // ---------- Classes ----------
            var classes = new Dictionary<string, object>();
            foreach (var cls in root.DescendantNodes().OfType<ClassDeclarationSyntax>())
            {
                var clsNode = new Dictionary<string, object>();

                // <summary> コメント
                var summary = cls.GetLeadingTrivia()
                    .Select(t => t.GetStructure()).OfType<DocumentationCommentTriviaSyntax>()
                    .SelectMany(d => d.Content.OfType<XmlElementSyntax>())
                    .Where(x => x.StartTag.Name.LocalName.Text == "summary")
                    .Select(x => x.Content.ToString().Trim())
                    .FirstOrDefault();
                if (!string.IsNullOrEmpty(summary)) clsNode["desc"] = summary;

                // Properties & Fields
                var props = new Dictionary<string, string>();

                // Properties
                foreach (var prop in cls.Members.OfType<PropertyDeclarationSyntax>())
                {
                    var key = $"{prop.Modifiers} {prop.Identifier}: {prop.Type}";
                    props[key] = "";
                }
                // Fields
                foreach (var field in cls.Members.OfType<FieldDeclarationSyntax>())
                {
                    var modifiers  = field.Modifiers.ToString();
                    var attributes = string.Join(" ", field.AttributeLists.Select(a => a.ToString()));
                    var type       = field.Declaration.Type;
                    foreach (var v in field.Declaration.Variables)
                    {
                        var init = v.Initializer != null ? " = " + v.Initializer.Value : "";
                        var key  = $"{attributes} {modifiers} {v.Identifier}: {type}{init}".Trim();
                        props[key] = "";
                    }
                }
                if (props.Count > 0) clsNode["props"] = props;

                // Methods
                var methods = new Dictionary<string, string>();
                foreach (var m in cls.Members.OfType<MethodDeclarationSyntax>())
                {
                    var paramList = string.Join(", ",
                        m.ParameterList.Parameters.Select(p => $"{p.Identifier}: {p.Type}"));
                    var key = $"{m.Modifiers} {m.Identifier}({paramList}): {m.ReturnType}";
                    methods[key] = "";
                }
                if (methods.Count > 0) clsNode["methods"] = methods;

                classes[cls.Identifier.Text] = clsNode;
            }
            if (classes.Count > 0) mod["classes"] = classes;

            // ---------- Interfaces ----------
            var interfaces = new Dictionary<string, object>();
            foreach (var iface in root.DescendantNodes().OfType<InterfaceDeclarationSyntax>())
            {
                var ifaceNode = new Dictionary<string, object>();
                var meths     = new Dictionary<string, string>();
                foreach (var sig in iface.Members.OfType<MethodDeclarationSyntax>())
                {
                    var paramList = string.Join(", ",
                        sig.ParameterList.Parameters.Select(p => $"{p.Identifier}: {p.Type}"));
                    var key = $"{sig.Identifier}({paramList}): {sig.ReturnType}";
                    meths[key] = "";
                }
                if (meths.Count > 0) ifaceNode["methods"] = meths;
                interfaces[iface.Identifier.Text] = ifaceNode;
            }
            if (interfaces.Count > 0) mod["interfaces"] = interfaces;

            // ---------- Enums ----------
            var enums = new Dictionary<string, object>();
            foreach (var en in root.DescendantNodes().OfType<EnumDeclarationSyntax>())
            {
                var values = new Dictionary<string, string>();
                foreach (var mem in en.Members)
                {
                    var val = mem.EqualsValue != null ? mem.EqualsValue.Value.ToString() : null;
                    var key = val != null ? $"{mem.Identifier} = {val}" : mem.Identifier.ToString();
                    values[key] = "";
                }
                enums[en.Identifier.Text] = new Dictionary<string, object> { ["values"] = values };
            }
            if (enums.Count > 0) mod["enums"] = enums;

            modules.Add(mod);
        }

        // ---------- Root YAML ----------
        var rootYaml = new Dictionary<string, object>
        {
            ["project"]     = Path.GetFileName(Directory.GetCurrentDirectory()),
            ["version"]     = "1.0.0",
            ["generatedAt"] = DateTime.UtcNow.ToString("o"),
            ["modules"]     = modules
        };

        var yaml = new SerializerBuilder()
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
            .Build()
            .Serialize(rootYaml);

        File.WriteAllText(OutFile, yaml);
    }
}
```

## 5. 運用フロー

1. **スクリプトを保存** すると Unity が再ビルドし、`project-structure.yaml` がプロジェクトルートに生成／上書き。
    
2. コンソールに `[ProjectStructure] Generated → …/project-structure.yaml` が表示されれば成功。
    
3. `<summary>` コメントを更新して再保存すれば YAML も即反映。
    
4. **コミットタイミング**：YAML が変わったら通常のコード差分と一緒にコミットすれば OK。Pull Request で設計変更もレビューできます。
    
## 6. YAML をもっと活用する Tips

|ツール|用途|
|---|---|
|**Yaml2Table (VS Code)**|YAML → Markdown Table に変換して俯瞰レビュー|
|**Mermaid**|YAML からクラス図を自動生成してドキュメント化|


## 7. まとめ

- **YAML 1 枚** で AI の前提を固定し、重複ファイルや型不一致を防止
    
- Unity では _インポート → コピペ_ だけで導入完了
    
- 他言語のプロジェクトでも同じ原理で応用可能。**この記事のスクリプトとやりたいこと**を AI にそのまま質問すれば、あなたのスタック向けのジェネレータをきっと書いてくれるはずです。ぜひ試してみてください！

## 8. 参考リンク

- NuGet for Unity
	[GitHub - GlitchEnzo/NuGetForUnity: A NuGet Package Manager for Unity](https://github.com/GlitchEnzo/NuGetForUnity)

- Microsoft.CodeAnalysis (Roslyn)
    [NuGet Gallery \| Microsoft.CodeAnalysis.CSharp 4.13.0](https://www.nuget.org/packages/microsoft.codeanalysis.csharp/)
    
- YamlDotNet
    [NuGet Gallery \| YamlDotNet 16.3.0](https://www.nuget.org/packages/YamlDotNet)