TL;DR
AI が既存コードを壊す/重複ファイルを作る原因は “プロジェクト全体像の誤認識”
そこで
project-structure.yaml
に 最低限のメタ情報(ファイルパス・クラス・プロパティ・メソッド…)をまとめて AI に読ませると、高確率で正しい前提でコードを生成させられる本記事では Unity 向けに Roslyn + YamlDotNet を使い “ワンクリック自動生成” する方法を解説
1. この記事の対象とゴール
Unity プロジェクトを Cursor / GitHub Copilot などと協調開発している人向けの記事です。
次のような悩みを抱えていませんか?
-
AI が既存スクリプトを無視して新しく作り直してしまう
-
型やメンバー名を間違えてビルドが通らないコードを提案してくる
-
「プロジェクト全体が大きすぎてコンテキストに入りきらない」
本記事を読み終えると――
-
YAML 1 枚 で AI の前提を固定し、コンテキスト上限問題を回避する方法がわかる。
-
Unity に NuGetForUnity + Roslyn + YamlDotNet を組み込んで、自動で
project-structure.yaml
を生成できるようになる。 -
自分好みに拡張するポイント(コメント抽出や 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 が効くのか?
-
LLM フレンドリー:階層構造を保ちつつテキスト量が抑えられ、モデルが高速に理解できる。
-
コメントしやすい:JSON では書きづらい行コメントを残せるので、設計意図を共有しやすい。
-
差分が明快: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 を導入する手順(詳細)
-
NuGetForUnity を取得
Releases · GlitchEnzo/NuGetForUnity からNuGetForUnity.*.unitypackage
をダウンロード。 -
Unity プロジェクトにインポート
NuGetを使用したいプロジェクトを開いた状態で.unitypackage
をダブルクリック → Import
-
NuGet ▸ Manage NuGet Packages を開く。
-
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. 運用フロー
-
スクリプトを保存 すると Unity が再ビルドし、
project-structure.yaml
がプロジェクトルートに生成/上書き。 -
コンソールに
[ProjectStructure] Generated → …/project-structure.yaml
が表示されれば成功。 -
<summary>
コメントを更新して再保存すれば YAML も即反映。 -
コミットタイミング: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 -
Microsoft.CodeAnalysis (Roslyn)
NuGet Gallery | Microsoft.CodeAnalysis.CSharp 4.13.0 -
YamlDotNet
NuGet Gallery | YamlDotNet 16.3.0