本業の合間に、スマートフォン向けのカードゲーム『ソリティア世界旅行』(英語名 Solitaire World Tour)を Unity で作り、2026 年 9 月に iOS と Android で公開しました。コードは Claude Code が書いています。
作り方の全体は Zenn に書きました。この記事では、そのうち「Unity Editor の操作を AI に任せる」部分だけを、設定と、実際につまずいたところまで詳しく書きます。
なぜ Editor の操作まで任せたか
Unity のゲームを AI に書かせると、コードを書いたあとに次の確認が残ります。
- コンパイルが通るか
- テストが通るか
- 画面がどう見えるか、ボタンを押すとどうなるか
これを毎回人が Editor に切り替えて確かめていると、作業効率が悪いです。そこで、Editor を外から操作できる MCP を 2 つ入れ、AI が自分で「書く → コンパイル → エラーを読む → 直す → 画面を見る」まで回せるようにしました。
使っている 2 つの MCP
| MCP for Unity(UnityMCP) | uloop(Unity CLI Loop) | |
|---|---|---|
| 入手先 | https://github.com/CoplayDev/unity-mcp | https://github.com/hatayama/unity-cli-loop |
| Claude Code からの呼び方 | MCP ツール(HTTP、http://127.0.0.1:8080/mcp) |
uloop コマンド(Claude Code のスキルから呼ぶ) |
| 主に使っていること | シーン・GameObject・アセットの操作、Editor の中で C# を実行、コンソールの読み取り | コンパイル、テストの実行、ログの取得、Game View のスクリーンショット、Play Mode の開始と停止、クリック・キー入力の再現 |
どちらも同じ Editor につながります。はっきりした決まりではありませんが、次のように分けています。
- コンパイル・テスト・スクリーンショット・Play Mode での操作 → uloop
- シーンやオブジェクトを構造的にいじる、実行中の値を C# で読む → UnityMCP
2 つ入れておくと、片方が止まったときにもう片方で作業を続けられます(後述)。
入れ方
どちらも Unity のパッケージとして入れます。Packages/manifest.json はこうなっています。
{
"dependencies": {
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#main",
"io.github.hatayama.uloopmcp": "https://github.com/hatayama/unity-cli-loop.git?path=/Packages/src"
}
}
UnityMCP は Editor の起動と同時にサーバーが立ち上がるので、Claude Code 側には HTTP の MCP サーバーとして登録します。
{
"mcpServers": {
"UnityMCP": {
"type": "http",
"url": "http://127.0.0.1:8080/mcp"
}
}
}
uloop は uloop コマンドで操作します。uloop compile や uloop run-tests のように、操作ごとにサブコマンドがあり、Claude Code にはそれぞれの使い方を書いたスキル(.claude/skills/uloop-compile/SKILL.md など)を置いています。
CLAUDE.md に書いている決まり
AI に毎回同じことを説明しなくて済むよう、プロジェクト直下の CLAUDE.md に決まりを書いています。Editor まわりで効いたのは次のあたりです。
## Unity Editor automation (MCP — use proactively)
- A live Unity Editor is normally running. Use UnityMCP / uloop on your own
whenever a task touches the Editor, scenes, scripts, assets, tests, or runtime.
Do not wait to be told, and do not guess at state you can read live.
- Rough split: uloop for compile / console / tests / Game View screenshots /
Play Mode + input simulation; UnityMCP for structured scene/GameObject/asset
edits and live C# execution.
## Avoid by default
- Library/, Temp/, obj/, Logs/, Build/ ... (generated files)
## Large files
- Do not read large YAML / scene / prefab files whole. Write a small read-only
script that extracts only the fields you need.
- Never call SaveAssets / asset delete/move or import-triggering APIs while extracting.
「Editor が動いているなら、状態は推測せずに読みに行け」と書いておくのが大事でした。書いていないと、AI はコードだけ読んで「たぶんこう表示されます」と答えがちです。
生成物のフォルダは、CLAUDE.md に書くだけでなく、.claude/settings.json の permissions.deny でも読めないようにしています。
{
"permissions": {
"deny": [
"Read(./Library/**)",
"Read(./Temp/**)",
"Read(./Logs/**)",
"Read(./obj/**)"
]
}
}
CLAUDE.md は守られないことがある「お願い」で、permissions.deny は実際に止める設定、という分け方です。
UI はコードで組んでいる
このゲームの UI は uGUI ですが、Prefab を 1 つも使わず、すべてコードで組み立てています(シーンは 2 つだけ)。画面の構成がすべて C# にあるので、AI はファイルを読むだけで画面の中身を把握でき、直すのもコードの差分で済みます。Prefab やシーンの YAML を AI に読ませたり書かせたりしなくてよいのは、思った以上に楽でした。
1 回の修正の流れ
たとえば「このボタンの位置をずらして」と頼むと、AI は次のように進めます。
- C# を直す
-
uloop compileでコンパイルし、エラーがあれば読んで直す -
uloop run-testsで EditMode テストを回す -
uloop control-play-mode --action Playで Play Mode に入る -
uloop simulate-mouse-uiでその画面までボタンを押して進む -
uloop screenshotで撮って、画像を見て位置を確かめる - ずれていれば 1 に戻る
人がやるのは、最後に実機で触って確かめることです。
つまずいたところ
ここからが本題です。動くようになるまでに引っかかったところを、症状と対処の形で書きます。
1. コンパイル結果の代わりに「Domain Reload in progress」が返ってくる
uloop compile を呼ぶと、結果ではなく「Unity is reloading (Domain Reload in progress)」や「wait for compilation to finish」が返ってくることがよくあります。1 回呼んで終わりにすると、AI は「コンパイルできなかった」と勘違いします。
リトライのループで包むように、スキルとメモに書きました。
for i in $(seq 1 25); do
out=$(uloop compile --wait-for-domain-reload true)
echo "$out" | grep -qi 'reloading\|compilation to finish' && sleep 8 || break
done
echo "$out"
2. ドメインリロードのあと UnityMCP だけがつながらなくなる
スクリプトを直してドメインリロードが走ると、UnityMCP のセッションが切れて、そのまま戻らないことがありました(no_unity_session のエラー、refresh_unity のタイムアウト)。
このとき Editor 自体は正常で、uloop は生きています。2 つは別々の仕組みで Editor につながっているためです。なので、UnityMCP が応答しなくても Editor が落ちたとは判断せず、uloop に切り替えて続けるよう決めています。2 つ入れておいてよかったところです。
3. Editor が裏にあると、Play Mode やコンパイルが進まない
Editor が他のウィンドウの後ろにあると、uloop control-play-mode --action Play が「Play mode started」を返したのに、実際には Play Mode に入らないことがあります。インポートやコンパイルも止まったままになることがあります。
先に uloop focus-window で Editor を前に出し、10 秒ほど待ってから確かめると進みます。
4. スクリーンショットがいつも同じ古い画面になる
Editor が裏にあると、Game View が描画されず、ゲームのループも回りません。この状態で uloop screenshot --capture-mode rendering を撮ると、シーンをどう変えても同じ古い画面が返ってきます。時間で進むアニメーションも止まったままです。
一度だけ次を実行しておくと、裏にあってもループが回り、毎回新しい画面が撮れるようになりました。
// uloop execute-dynamic-code で実行
UnityEngine.Application.runInBackground = true;
5. スクリーンショットの色が実際より暗く、濃く写る
プロジェクトはリニアカラースペースです。--capture-mode rendering の画像は、画面に出すときの sRGB への変換がかかる前のものなので、中間の色が暗く、濃く写ります。白と黒は変わりません。
たとえば朱色 #E15A46(225, 90, 70)は、撮った画像では (192, 26, 16) くらいになります。これを見た AI が「色が違う」と判断してコードの色を直すと、実機の色が逆に狂います。
撮った色が、指定した色をリニアに変換した値と一致するなら、アプリは正しく、ずれているのは画像のほうです。
def srgb_to_linear(c): # c: 0-255
return ((c / 255 + 0.055) / 1.055) ** 2.4 * 255
「スクリーンショットで見てよいのは位置・大きさ・折り返しまで。色では判断しない」と決めて、メモに残しました。
6. 時間で消える吹き出しが、撮る前に消えている
runInBackground を有効にしたあとは、ツールを呼ぶあいだにも数秒が過ぎます。数秒で自動的に消える吹き出しは、次の呼び出しでスクリーンショットを撮るころには消えています。
吹き出しを出すのと同じ呼び出しの中で、「20 フレーム後に一時停止する」処理を仕込んでおき、止まった画面を撮るようにしました。
int f = 0;
UnityEditor.EditorApplication.CallbackFunction cb = null;
cb = () => {
f++;
if (f > 20) {
UnityEditor.EditorApplication.isPaused = true;
UnityEditor.EditorApplication.update -= cb;
}
};
UnityEditor.EditorApplication.update += cb;
Time.timeScale = 0 で止める方法は使えません。Time.deltaTime で進むフェードインが始まらなくなるためです。
7. 広告の仮表示を閉じたあと、アニメーションが止まる
AdMob(Google Mobile Ads)は、Editor ではダミーの広告を uGUI で表示します。全画面広告のダミーは、開いているあいだ Time.timeScale を 0 にします。
これを AI が C# から Button.onClick.Invoke() で閉じると、SDK が timeScale を戻す処理を通らず、0 のまま残ることがありました。症状は「Time.frameCount は増えているのに、Time.deltaTime で動くアニメーションだけ止まる」です。
uloop simulate-mouse-ui で本当にクリックして閉じれば、戻す処理が走ります。アニメーションが止まったら、コードを疑う前に Time.timeScale を確かめるようにしています。
8. Editor を開いたままではバッチモードでビルドできない
Editor を開いたまま Unity -batchmode を動かすと、「このプロジェクトは別の Unity で開かれている」と止まります。Editor を閉じずにコンパイルが通るかだけを確かめたいときは、Unity に同梱されている C# コンパイラ(Roslyn)を直接呼んでいます。
Editor はアセンブリごとのコンパイル引数を .rsp ファイルとして Library/Bee/ の下に書き出しています。これを別の場所に写し、出力先だけ書き換えて、同梱の dotnet で csc.dll に渡します。
# パスは Unity 6000.5.0f1 のもの
UNITY=/Applications/6000.5.0f1/Unity.app/Contents/Resources/Scripting/DotNetSdk
"$UNITY/dotnet" "$UNITY/sdk/8.0.318/Roslyn/bincore/csc.dll" @scratch/SolitaireWorld.App.rsp
注意点が 1 つあります。**.rsp の -out: を書き換え忘れると、Editor が使っているビルド結果を上書きしてしまいます。**実際に一度やってしまい、Editor の次の起動で作り直させました。書き換えたあと、実行する前に -out: の行を確かめる手順にしています。
#if UNITY_ANDROID のように Editor ではコンパイルされない部分も、定義シンボルを差し替えればこの方法で確かめられます。
9. 定義シンボルを git で戻しても、Editor の中に古い値が残る
撮影用に足した定義シンボルを、ProjectSettings.asset を git checkout して戻したことがあります。ファイルは戻っていたのに、PlayerSettings.GetScriptingDefineSymbols() は古い値を返しました。起動中の Editor はメモリに設定を持っていて、そのままだと次に設定を保存したときにファイルへ書き戻されます。
定義シンボルを触ったら、ファイルを見るだけでなく、Editor の中の実際の値を C# で読んで確かめるようにしています。
10. 急に Newtonsoft が見つからないエラーが 100 件以上出る
何も触っていないのに、CS0246: 'Newtonsoft' could not be found が 118 件出たことがあります。自分のコードだけでなく、Library/PackageCache の中のパッケージ(UnityMCP 自身も)からも出ていました。
原因はコードではなく、パッケージの解決が途中で壊れたことでした。次の 1 行でパッケージを解決し直し、ドメインリロードを何回か待つと、コードを変えずに元に戻りました。
// uloop execute-dynamic-code で実行
UnityEditor.PackageManager.Client.Resolve();
パッケージの名前空間で急にエラーが出たら、asmdef やパッケージを入れ直す前にこれを試すよう、メモに残しています。
つまずきはメモに残して、次から AI が自分で避ける
上の 10 個は、どれも一度は AI が引っかかったものです。Claude Code のメモリ(プロジェクトごとのメモ)に「症状・原因・対処」の形で残しておくと、次からは AI が自分で避けるようになりました。人が覚えておく必要がないのが、いちばん助かったところです。
おわりに
Editor の操作まで任せると、AI は「コードを書いた」ではなく「画面で確かめた」ところまで進めてくれるようになります。つまずくところはありますが、ほとんどは一度メモに書けば繰り返しません。
作ったゲームはこちらです。よければ遊んでみてください。