npx claudepluginhub bigdra50/unity-cli --plugin unity-cliThis skill uses the workspace's default tool permissions.
> **PREREQUISITE:** `../unity-shared/SKILL.md`(Relay Server 経由で Unity Editor が起動/アクティブであること)
Controls Unity Play Mode, simulates keyboard/mouse inputs, automates UI via clicks/drags, captures screenshots/videos, checks console logs for test execution and visual verification.
Automates Unity UI inspection and interaction using unity-cli. Finds elements by name or type, checks button/input states, clicks elements, sets values, runs short testing sequences.
Runs Unity Test Framework tests via CLI: detects Editor, executes EditMode/PlayMode tests, parses XML results, generates failure reports. For game logic validation, debugging, CI/CD.
Share bugs, ideas, or general feedback.
PREREQUISITE:
../unity-shared/SKILL.md(Relay Server 経由で Unity Editor が起動/アクティブであること)skill 経由のコマンドは必ず
-i <instance>を付ける (unity-shared #インスタンス指定)。uitree コマンドは Play Mode 中のみ動作する。必ず
u -i <instance> playしてから実行すること。
| 種別 | 検証内容 | 手法 | スクショ |
|---|---|---|---|
| Functional | 操作→状態が正しいか | click → text で値検証 | 不要 |
| Visual Regression | 外観が変わっていないか | screenshot → 画像比較 | 必要 |
| Structural Snapshot | ツリー構造が変わっていないか | dump --json → diff | 不要 |
| Smoke | 最低限動くか | click → console にエラーなし | 任意 |
| Monkey | ランダム操作でクラッシュしないか | monkey → errors 確認 | 不要 |
| 手がかり | UI Toolkit | uGUI |
|---|---|---|
| ファイル | .uxml, .uss | .prefab に Canvas |
| コンポーネント | UIDocument | Canvas, RectTransform |
要素の指定方法は2通り: ref ID (ref_3) または -p <panel> -n <name>。
u -i <instance> uitree dump # 全パネル一覧 (contextType: Player がゲーム側)
u -i <instance> uitree dump -p "PanelSettings" # パネルのツリー (各要素の ref/name/type/classes が見える)
u -i <instance> uitree dump -p "PanelSettings" --json # JSON 出力 (snapshot用)
u -i <instance> uitree query -p "PanelSettings" -t Button # type で検索 (VisualElement ベースのボタンはヒットしない → -c で検索)
u -i <instance> uitree query -p "PanelSettings" -n "BtnStart" # name で検索
u -i <instance> uitree query -p "PanelSettings" -c "action-btn" # USS class で検索
u -i <instance> uitree query -p "PanelSettings" -t Button -c "primary" # AND 条件
u -i <instance> uitree inspect -p "PanelSettings" -n "BtnStart" # 要素詳細
u -i <instance> uitree inspect ref_5_48 # ref ID で指定
u -i <instance> uitree inspect ref_5_48 --style # resolvedStyle 含む
u -i <instance> uitree inspect ref_5_48 --children # 子要素情報含む
u -i <instance> uitree text -p "PanelSettings" -n "ToastMessage" # name で指定
u -i <instance> uitree text ref_5_12 # ref ID で指定
u -i <instance> uitree click -p "PanelSettings" -n "BtnContinue" # name で指定
u -i <instance> uitree click ref_5_48 # ref ID で指定
u -i <instance> uitree click ref_5_48 --count 2 # ダブルクリック
u -i <instance> uitree click ref_5_48 --button 1 # 右クリック
u -i <instance> uitree scroll -p "PanelSettings" -n "ScrollArea" --y 100
u -i <instance> uitree monkey -p "PanelSettings" -c "action-btn" --count 50 --seed 42
u -i <instance> uitree monkey -p "PanelSettings" --duration 30 --seed 42
u -i <instance> uitree monkey -p "PanelSettings" -c "action-btn" --stop-on-error --json
query でフィルタした要素をランダムに click し、コンソールエラーを監視する。
-c (USS class) か -t (type) のフィルタが必須(フィルタなしでは要素が見つからない)。
事前に u -i <instance> uitree query -p <panel> -c <class> で有効な class 名を確認してから monkey に渡す。
--seed で操作順を再現可能。
u -i <instance> uitree snapshot save -p "PanelSettings" --name baseline # 現在のツリーを保存
u -i <instance> uitree snapshot diff -p "PanelSettings" --name baseline # baseline との差分
u -i <instance> uitree snapshot list # 保存済み一覧
u -i <instance> uitree snapshot delete --name baseline # 削除
diff 出力: 要素の追加(+)/削除(-)/classes 変化(~) を検出。
u -i <instance> screenshot # GameView (-s game がデフォルト)
u -i <instance> screenshot -p "ui-check.png" # 保存先指定
u -i <instance> screenshot --burst -n 5 # 連続 (アニメーション確認)
UI Toolkit / uGUI (Screen Space) は -s game でのみ映る。-s camera では映らない。
GameView のキャプチャにはエディタのフォーカスが必要。タイムアウトする場合は再試行で解決することが多い。
uitree は UI Toolkit 専用。uGUI は scene/component コマンドで操作する。
u -i <instance> scene hierarchy --depth 3
u -i <instance> component list -t "Canvas"
u -i <instance> component inspect -t "MyButton" -T "UnityEngine.UI.Button"
Phase 1: 作成 & 手動確認
コード変更 → /unity-verify → play → dump → 操作 → screenshot → stop
Phase 2: pytest E2E テスト生成
dump で構造把握 → pytest テストファイルを生成 → 実行して確認
Phase 3: PlayMode テスト移植
安定したシナリオを C# に書き直す → CI で回帰テスト
u -i <instance> play
u -i <instance> uitree dump -p "PanelSettings" # 構造把握
u -i <instance> uitree click -p "PanelSettings" -n "BtnStart" # 操作
u -i <instance> uitree text -p "PanelSettings" -n "StatusLabel" # 状態確認
u -i <instance> screenshot # 結果キャプチャ
u -i <instance> stop
Phase 1 で確認した操作シナリオを、pytest テストとして永続化する。
unity-cli の Python API (UITreeAPI, ConsoleAPI, EditorAPI) を直接使う。
手順:
u -i <instance> play + u -i <instance> uitree dump で構造を把握するtests/integration/ にテストファイルを生成するuv run python -m pytest tests/integration/<file> -v で実行して確認するテンプレート:
"""E2E tests for <UI名> — pytest + unity-cli API."""
import time
import pytest
from unity_cli.api import ConsoleAPI, EditorAPI, UITreeAPI
from unity_cli.client import RelayConnection
PANEL = "<パネル名>" # u uitree dump で確認したパネル名
@pytest.fixture(scope="module")
def conn() -> RelayConnection:
conn = RelayConnection(instance="<プロジェクト名>", timeout=5.0)
try:
EditorAPI(conn).get_state()
except Exception:
pytest.skip("Relay not available")
return conn
@pytest.fixture(scope="module")
def uitree(conn: RelayConnection) -> UITreeAPI:
return UITreeAPI(conn)
@pytest.fixture(scope="module")
def console(conn: RelayConnection) -> ConsoleAPI:
return ConsoleAPI(conn)
@pytest.fixture(scope="module")
def editor(conn: RelayConnection) -> EditorAPI:
return EditorAPI(conn)
@pytest.fixture(autouse=True)
def _play_mode(editor: EditorAPI):
"""Play Mode に入る。"""
state = editor.get_state()
if not state.get("isPlaying"):
editor.play()
deadline = time.time() + 10
while time.time() < deadline:
if editor.get_state().get("isPlaying"):
break
time.sleep(0.5)
yield
@pytest.fixture(scope="module", autouse=True)
def _stop_after_all(editor: EditorAPI):
"""全テスト後に Play Mode 終了。"""
yield
try:
editor.stop()
except Exception:
pass
# --- Functional テスト ---
class TestMenuButtons:
def test_continue_shows_toast(self, uitree: UITreeAPI) -> None:
uitree.click(panel=PANEL, name="<ボタン名>")
time.sleep(0.3)
result = uitree.text(panel=PANEL, name="<ラベル名>")
assert result["text"] == "<期待値>"
# --- Smoke テスト ---
class TestSmoke:
BUTTONS = ["<ボタン1>", "<ボタン2>", ...]
def test_all_clickable_without_errors(self, uitree: UITreeAPI, console: ConsoleAPI) -> None:
console.clear()
for btn in self.BUTTONS:
uitree.click(panel=PANEL, name=btn)
time.sleep(0.2)
errors = console.get(types=["error"])
assert errors.get("entries", []) == []
# --- Structural Snapshot ---
class TestStructure:
def test_tab_switch_changes_tree(self, uitree: UITreeAPI) -> None:
before = uitree.dump(panel=PANEL)
uitree.click(panel=PANEL, name="<タブ名>")
time.sleep(0.3)
after = uitree.dump(panel=PANEL)
assert before != after
既存の conftest.py に conn, uitree, editor, console fixture がある場合はテンプレートの fixture 定義を省略し、conftest に委譲する。fixture の scope は conftest に合わせる(conftest が session なら _play_mode 等も同じ scope か互換性のある scope にする)。
動的な値(タイムスタンプ、プログレス%等)を持つラベルは等値比較ではなくフォーマット検証を使う:
assert result["text"].endswith("%") # "65%" 等
テストの種別に応じてクラスを追加する:
TestMenuButtons — Functional (click → text 検証)TestSmoke — 全ボタンクリック + エラーなしTestStructure — ツリー構造の変化検出TestMonkey — ランダム操作でエラーなしTestVisual — screenshot 撮影(画像比較は手動 or 別ツール)Monkey テスト例:
from unity_cli.api.uitree_monkey import MonkeyRunner
class TestMonkey:
def test_random_clicks_no_errors(self, uitree: UITreeAPI, console: ConsoleAPI) -> None:
# MonkeyRunner.run() は MonkeyResult を返す。errors は list[dict]
runner = MonkeyRunner(uitree, console)
result = runner.run(panel=PANEL, class_filter="<操作対象class>", count=50, seed=42, interval=0.2)
assert result.errors == [], f"Monkey errors: {result.errors}"
Snapshot テスト例:
from unity_cli.api.uitree_snapshot import SnapshotStore
class TestSnapshot:
def test_save_and_diff_no_changes(self, uitree: UITreeAPI, tmp_path) -> None:
store = SnapshotStore(snapshot_dir=tmp_path / "snapshots")
data = uitree.dump(panel=PANEL, format="json")
store.save("baseline", data)
current = uitree.dump(panel=PANEL, format="json")
# diff(name, current): name は保存済みスナップショット名、current は比較対象のツリーデータ
result = store.diff("baseline", current)
assert result["added"] == []
assert result["removed"] == []
assert result["changed"] == []
monkey テストの前にUIを初期状態に戻す(前のテストの副作用で Toast 等のクラスが変わることがある)。time.sleep(2) で Toast の hidden 復帰を待つか、テスト順序に依存しない assertion を使う。
Phase 2 の pytest で安定したシナリオを C# PlayMode テストに書き直す。
PlayMode に移植する基準:
pytest E2E のまま残す基準:
手順:
Assets/Tests/PlayMode/ に C# テストを生成するu -i <instance> tests run play で実行して確認するテンプレート:
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.UIElements;
namespace Game.Tests.PlayMode
{
[TestFixture]
public class <UI名>Tests
{
private GameObject _hudObject;
private UIDocument _uiDocument;
private VisualElement _root;
[UnitySetUp]
public IEnumerator SetUp()
{
// UIDocument を持つ GameObject を生成
// コントローラが Awake/OnEnable で初期化するのを待つ
_hudObject = new GameObject("TestHUD");
_hudObject.AddComponent<<コントローラ名>>();
_uiDocument = _hudObject.GetComponent<UIDocument>();
// コルーチン初期化 (StartCoroutine in OnEnable 等) は1フレームでは完了しない
// 2フレーム以上待つ
yield return null;
yield return null;
_root = _uiDocument.rootVisualElement;
Assert.IsNotNull(_root, "Root visual element should be initialized");
}
[TearDown]
public void TearDown()
{
Object.Destroy(_hudObject);
}
// --- Functional テスト ---
[UnityTest]
public IEnumerator <ボタン名>_Shows<期待される動作>()
{
var btn = _root.Q("<ボタン名>");
Assert.IsNotNull(btn);
Click(btn);
yield return null;
var label = _root.Q<Label>("<ラベル名>");
Assert.AreEqual("<期待値>", label.text);
}
// --- Smoke テスト ---
[UnityTest]
public IEnumerator AllButtons_ClickableWithoutErrors()
{
var buttons = new[] { "<ボタン1>", "<ボタン2>", ... };
foreach (var name in buttons)
{
var element = _root.Q(name);
Assert.IsNotNull(element, $"{name} should exist");
Click(element);
yield return null;
}
LogAssert.NoUnexpectedReceived();
}
// --- ヘルパー ---
private static void Click(VisualElement element)
{
using var evt = ClickEvent.GetPooled();
evt.target = element;
element.panel.visualTree.SendEvent(evt);
}
}
}
Click ヘルパーの注意:
ClickEvent.GetPooled() で座標計算不要のイベントを生成し、panel.visualTree.SendEvent で発火するNavigationSubmitEvent では RegisterCallback<ClickEvent> が反応しないため使わないPointerDownEvent + PointerUpEvent を worldBound.center 座標付きで送るasmdef の注意:
Assembly-CSharp(asmdef なし)にある場合、テスト asmdef からは直接参照できないreferences に追加するAssets/UI/Game.UI.asmdef を作成 → Game.Tests.PlayMode.asmdef の references に "Game.UI" を追加Phase 2 の pytest テストの各テストメソッドを1対1で C# に移植する。
移植後は u -i <instance> tests run play で実行。
unity-shared のフォールバック順に従う:
u api schema --type <Type> で対応メソッドを検索u api call で実行