Claude CodeでUnityのasmdefを整理する — 循環参照を避けた分割手順
UnityのスクリプトをasmdefでAssemblyに分けると、1ファイルの変更で再コンパイルされる範囲が狭まります。依存の読み取りをsubagentに任せ、循環参照のない分割案を作る手順です。
スクリプトが増えたUnityプロジェクトでは、1行直しただけで長い再コンパイルを待たされます。原因は、既定ではほぼ全部のスクリプトが Assembly-CSharp という1つのAssemblyにまとまっていることです。asmdef(Assembly Definition)でフォルダ単位にAssemblyを切れば、変更の影響が及ぶ範囲を絞れます。
難しいのは、どこで切るかを決める作業です。依存の向きを読み違えると循環参照でコンパイルが通らなくなります。この読み取りは、読み取り専用のsubagentに任せると本体の会話を汚さずに済みます。手順は次の流れです。
asmdef整理の流れ
- 1
依存を読む
読み取り専用のsubagentに、フォルダ間の参照関係を一覧させます。
- 2
分割案を出す
下位から上位へ一方向になる分割案を、フォルダ単位で出させます。
- 3
asmdefを置く
案を人間が確認してから、JSONを書かせます。
- 4
コンパイルで確かめる
エラーと再コンパイルの待ち時間を前後で比べます。
asmdefで再コンパイルの範囲が狭まる理由
Unityは既定で、ゲームスクリプトのほとんどを Assembly-CSharp.dll にコンパイルします。Unity 6.6のマニュアルは、この構成の欠点を3つ挙げています。
- 1つのスクリプトを変えると、他のスクリプトもすべて再コンパイルされる
- どのスクリプトからでも他のスクリプトの型に直接触れるため、リファクタリングしにくい
- すべてのスクリプトが全プラットフォーム向けにコンパイルされる
asmdefを置いたフォルダは、その配下のスクリプトを1つのAssemblyにまとめます。サブフォルダは、そこに別のasmdefかasmrefがない限り同じAssemblyに入ります。
効果の根拠は参照の向きです。たとえばMainがStuffを参照し、StuffがLibraryを参照する構成なら、Mainを変えてもStuffには影響しません。Libraryは何も参照しないので、別プロジェクトに持ち出すのも楽になります。コンパイル順はUnityが依存関係から決めるもので、指定できません。
マニュアルが約束しているのは「不要な再コンパイルを減らせる」という方向までです。何秒縮むかはプロジェクトごとに違うため、後半で前後の計測方法を扱います。
分割前に押さえるUnityの制約
AIに案を作らせる前に、Unityが禁じている参照を知っておく必要があります。案がこの制約に触れると、適用した時点でエラーになるからです。
| 制約 | 内容 | 案への影響 |
|---|---|---|
| 循環参照 | 内容2つのAssemblyが互いに参照することは不可。解消はコードの見直しか、同じAssemblyへの統合 | 案への影響参照グラフが一方向であること |
| predefinedへの参照 | 内容asmdefのAssemblyから Assembly-CSharp への参照は不可 | 案への影響既存コードが残るなら、asmdef側から呼び出せない |
| 明示参照 | 内容predefinedは明示参照を持てず、autoReferencedなAssemblyしか使えない | 案への影響分割後も既存コードが使う側はautoReferencedを維持 |
| Editorフォルダ | 内容asmdefの下にあるEditorフォルダのスクリプトは、predefinedのEditor Assemblyではなくそのasmdefに入る | 案への影響Editor専用コードは別のasmdefかasmrefに分ける |
2行目の「Assembly-CSharpへの参照は不可」は、段階移行で最初に当たる壁です。新しく切ったAssemblyの中から、まだ移していない旧コードの型は呼べません。そのため、移す順番は「他から参照されるだけの葉」からになります。
落とし穴
autoReferencedをfalseにすると、旧コードから見えなくなる
autoReferencedを外すと、predefined Assemblyはそのコードの変更で再コンパイルされなくなります。同時に、predefined側のコードはそのAssemblyの型を直接使えなくなります。旧コードが残る間は、既定のtrueのままにしておくのが安全です。
もう1点、プラグインのDLLがあるプロジェクトでは、asmdefのAssemblyがすべてのprecompiled Assemblyを自動で参照します。このためDLLを更新すると全Assemblyが再コンパイルされます。必要なDLLだけを overrideReferences と precompiledReferences で指定すると、この波及を抑えられます。
依存の読み取りをsubagentに任せる
依存関係を読む作業は、ファイルを大量に開く割に、結果は短い表で足ります。公式のsubagent解説は、出力が大量になる作業をsubagentに切り出し、要約だけを本体に戻す使い方を最も効果的な用途の1つとしています。
今回は読み取り専用にして、誤ってファイルを書き換えられないようにします。tools を Read, Grep, Glob に絞ったsubagentを、プロジェクトの .claude/agents/ に置きます。
---
name: asmdef-planner
description: Unityプロジェクトのスクリプト依存を読み取り、asmdefの分割案を出す。asmdefの整理を検討するときに使う
tools: Read, Grep, Glob
model: sonnet
---
あなたはUnityプロジェクトの依存を調べる担当です。ファイルは変更しません。
1. Assets配下のフォルダごとに、含まれる.csの数と、他フォルダの型を使っている箇所を数える
2. 「フォルダA → フォルダB」の参照を一覧にする(using句と型名の両方を見る)
3. 互いに参照し合うフォルダの組を、根拠の行つきで列挙する
4. 参照が一方向になる分割案を、下位(参照されるだけ)から順に出す
5. 案が断定できない箇所は「要確認」とし、理由を書く保存すると、Claude Codeは .claude/agents/ の変更を数秒で検出します。ただし、そのセッションを始めたとき agents ディレクトリ自体が存在しなかった場合は、再起動が要ります。
呼び出しは自然な文章で足ります。名前を挙げれば、Claudeが委譲を判断します。確実に走らせたいときは @ で選びます。
asmdef-planner subagentで、Assets/Scripts配下の参照関係を調べて。
循環している組があれば、根拠になる行と一緒に出して。内蔵のExploreも読み取り専用で、Write・Editが拒否されます。単発の調査ならそちらで足ります。専用のsubagentを作る意味は、出力の形(下位から順の分割案、要確認の印)を固定できる点にあります。
文字列検索で型名を拾う方式には限界があります。同名の型、リフレクション、Resources.Load や文字列で指定するScriptableObjectの参照は、grepでは追い切れません。subagentの結果は仮説として扱い、最終判定はUnityのコンパイルに任せます。
循環参照が見つかったときの扱い
subagentの出力に「AとBが互いに参照」という組が出たら、そのまま2つのasmdefには分けられません。マニュアルが示す解消法は2つです。コードを見直して参照を消すか、相互参照するクラスを同じAssemblyに入れます。
どちらを取るかの判断材料は、参照の中身です。次のように読み分けられます。
- 片方向がほんの数か所で、使う型が小さい: その型を下位のAssemblyに移して向きを揃える
- 相互に密に呼び合っている: 同じAssemblyに統合する。無理に切るとインターフェースだらけになる
- 設定値やデータ型だけが共有されている: データ型だけを葉のAssemblyに切り出す
どれもコードの設計判断なので、subagentには案を出させるだけにして、採否は人間が決めます。ここでメインの会話に戻って、案を一つずつ詰めていく流れが向いています。公式の使い分けの目安でも、往復しながら詰める作業はメインの会話、自己完結して要約を返せる作業はsubagentです。
asmdefのJSONを書かせる
案が決まったら、asmdefを書かせます。asmdefはJSONなので、Claude Codeがファイルとして直接作れます。フィールドの意味はマニュアルのファイル形式の節にあります。必須は name だけで、references にはAssemblyの名前かGUIDを書けます。
{
"name": "MyGame.Core",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"autoReferenced": true,
"noEngineReferences": false
}{
"name": "MyGame.Battle",
"references": ["MyGame.Core"],
"includePlatforms": [],
"excludePlatforms": [],
"autoReferenced": true
}これは形式に沿った一例で、実際の名前と参照は分割案に合わせて変えます。references を名前で書くか、GUIDで書くかは1つのファイル内で統一します。名前とGUIDを混ぜることはできません。
GUIDで参照する利点は、参照先のasmdefファイルをリネームしても壊れないことです。注意点として、メタファイルを削除したり、Unityの外でファイルだけを移動したりすると、GUIDを振り直す必要が出ます。Claude CodeにGUIDを書かせるときは、.asmdef.meta の内容を読ませて、手で作った値を入れさせないようにします。
Editor専用のスクリプトは、Editorプラットフォームだけを含めたasmdefにまとめます。別の場所にあるEditorフォルダは、asmrefでそのAssemblyに合流させられます。テスト用のAssemblyは、nunit.framework.dll への参照と、UnityEngine.TestRunner・UnityEditor.TestRunner への参照を持てば、Unityがテスト用と判定します。
手順を固定するCLAUDE.mdの断片
整理作業を何度かに分けるなら、ルールをCLAUDE.mdに書いておくと案がぶれません。次の形が出発点になります。
## asmdefの規約
- 参照は一方向。循環が出たら統合かデータ型の切り出しを案に含める
- predefined Assembly(Assembly-CSharp)に残るコードがある間、新asmdefはautoReferenced: true
- asmdefの `references` は名前で統一する(GUIDと混在させない)
- Editor専用コードは別のasmdefに分ける
- 案は必ず「下位から順」に並べ、移す単位ごとにコンパイルを確認する「移す単位ごとにコンパイルを確認する」の1行が効きます。一度に全部のasmdefを置くと、エラーがどの分割に起因するか切り分けられません。1つ置くたびにバッチモードでコンパイルを通す流れは、Claude CodeでUnityゲーム開発を進めるで扱った検証ループがそのまま使えます。
前後の効果を確かめる
縮んだかどうかは、同じ操作の待ち時間で比べます。ここは人間の側で数字を取るのが確実です。
- 分割前に、中核のスクリプトを1か所だけ変えて保存し、Editorに戻ってからコンパイル完了までの時間を測る
- 分割後に、同じファイルで同じ変更を加えて同じ時間を測る
- 葉に近いAssemblyのファイルでも測り、変更箇所によって待ち時間が変わることを見る
中核のAssemblyを触ると多くのAssemblyが再コンパイルされるため、効果は小さく出ます。逆に、末端のAssemblyだけを触る日常的な編集では差が出やすくなります。この差が出ないときは、依存の向きが分割前とほぼ変わっていないか、DLLの更新で全体が再コンパイルされている可能性があります。
測った数字はClaude Codeに渡して、どのAssemblyが重いかの相談に使えます。subagentが出した分割案どおりになっているかの確認は、asmdefのJSONを読ませるだけで済みます。Unity側の接続をさらに進めたい場合は、Unity MCPをClaude Codeで使う方法が参考になります。
subagentの権限と使い分け
今回のsubagentは読み取り専用で、tools に Read, Grep, Glob だけを渡しました。tools を省略すると利用可能なツールをすべて継承するため、調査用のsubagentには明示的に絞るほうが安全です。権限の設計をさらに詰めたいときは、subagentの定義と使い分けのガイドにまとまっています。
model は sonnet・opus・haiku・fable・inherit などを指定できます。依存の列挙は件数が多くても判断の難度は高くないため、軽いモデルを割り当てても足りる場面が多いです。ただし、循環の解消案のように判断が要る部分は、メインの会話に戻して詰めます。
複数のフォルダを同時に調べたいときは、並列で複数のsubagentを走らせる使い方もあります。独立した調査に向く一方、各subagentの結果がメインの会話に戻るため、数が多いとコンテキストを消費します。
まとめ
asmdefの整理で難しいのは、JSONを書く作業ではなく、循環を作らない切り方を決める作業です。依存の洗い出しは読み取り専用のsubagentに任せ、循環の解消方針は人間がメインの会話で決める、という分担が噛み合います。
最初の1本は、他から参照されるだけの葉(共通のデータ型やユーティリティ)にするのが安全です。旧コードがAssembly-CSharpに残る間はautoReferencedを外さず、1つ置くたびにコンパイルを通します。効果は中核を触る場合と末端を触る場合で分けて測ると、分割の成否が見えます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。