0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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. 完成イメージ

MyUnityGame/
├─ Assets/
│  ├─ Editor/
│  │  └─ GenerateProjectStructure.cs   ←★ 今回作成
│  └─ Scripts/…
└─ project-structure.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 から NuGetForUnity.*.unitypackage をダウンロード。

  2. Unity プロジェクトにインポート
    NuGetを使用したいプロジェクトを開いた状態で .unitypackage をダブルクリック → Import
    スクリーンショット 2025-05-08 11.19.54.png

  3. メニューに NuGet が追加される (追加されない場合はプロジェクトを再起動)
    スクリーンショット 2025-05-08 11.20.54.png

  4. NuGet ▸ Manage NuGet Packages を開く。

    スクリーンショット 2025-05-08 11.21.29.png

  5. Microsoft.CodeAnalysis.CSharp と YamlDotNet を検索 → Install

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

うまくいかない場合は ここ を参照して下さい。

スクリプトを配置

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

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. 参考リンク

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?