让 AI Agent(Claude、Cursor 等)通过 MCP 协议直接控制 Unity Editor / Runtime。
必须配合 SimpleMcpServer 使用。
AI Agent (Claude Code / Cursor)
│ MCP SSE (GET /sse + POST /mcp)
▼
SimpleMcpServer (Node.js/TypeScript) ← 另一个仓库,需单独 clone
│ WebSocket (ws://)
▼
SimpleMCPBridge (C#) ← 本仓库,clone 到 Unity Assets/ 下
│
▼
Unity Editor / Runtime
支持同时连接多个 Bridge(如 Editor + Android 设备),路由规则 last-registration-wins。
SimpleMCPBridge 的设计理念是 structured memory query(结构化内存查询),而非 screenshot-based perception(基于截图的视觉感知)。
为什么这样设计?
| 截图+视觉分析 | 内存查询(本桥) | |
|---|---|---|
| 成本 | 每帧截图+传输+LLM视觉分析 = 高token消耗 | 直接读组件属性 = 几十字节JSON |
| 精度 | OCR/视觉推断有误差(血量估计、文字识别) | 精确值(hp=87, mana=45.3) |
| 速度 | 截图+编码+网络+LLM处理 = 秒级 | 读属性+返回JSON = 毫秒级 |
| 可操作性 | 看到画面但不知道对象名/组件/instanceId | 直接拿到 instanceId 可立即操控 |
具体做法:
- 血条 → 读
Slider.value(精确浮点值),不分析像素颜色 - UI 文字 → 读
Text.text/TMP_Text.text(字符串),不 OCR - 玩家状态 → 读
Transform.position+Rigidbody.velocity+Animator参数,不截图推断 - 场景对象 → 遍历
Transform层级 +GetComponent,不视觉识别 - 敌人感知 →
game.get_entities批量读 AI/Health 组件,不数像素
camera.screenshot仅作为辅助手段(调试、确认视觉布局),不是感知游戏状态的主路径。所有游戏状态都应从 Unity 对象的组件属性中直接读取。
工具类别机制:127 个工具按类别组织([Scene]/[Ui]/[Ngui]/[Input]/[Game]…),
并支持运行时按类别动态开关(tools.enable / tools.disable)——只注册当前
任务需要的工具集,减少 token 消耗。详见下文「工具类别与动态注册」章节。
- Node.js 22+ — 运行 MCP Server
- Git — 克隆仓库
- Unity 2022.3+
cd YourUnityProject/Assets/
git clone https://github.com/redcool/SimpleMCPBridge_Unity.git SimpleMCPBridge安装后目录结构:
Assets/
├── SimpleMCPBridge/ ← 本仓库(git clone)
│ ├── Runtime/ # 运行时桥接代码
│ │ ├── BridgeClient.cs # 核心桥接逻辑(连接/消息/加密)
│ │ ├── MCPBridge.cs # MonoBehaviour 组件(生命周期/重连)
│ │ ├── MessageRouter.cs # JSON-RPC 消息路由
│ │ ├── MCPToolRegistry.cs # [MCPTool] 自动发现/注册
│ │ ├── MCPToolAttribute.cs # [MCPTool] + [MCPToolClass] 特性
│ │ ├── MCPMethodConst.cs # 工具名常量
│ │ ├── EncryptionHelper.cs # AES-256-CBC 加解密
│ │ ├── WebSocketClient.cs # 零依赖 RFC 6455 WebSocket([Obsolete],参考用)
│ │ ├── NetWebSocketClient.cs # 活动传输:.NET ClientWebSocket 封装
│ │ ├── WebSocketInterfaces.cs # WebSocket 接口抽象
│ │ ├── Config/
│ │ │ └── BridgeConfig.cs # 配置加载(Editor/Player)
│ │ ├── Handlers/ # 工具处理器(17+ Handler,127 个工具)
│ │ │ └── NguiHandler.cs # NGUI 工具(#if NGUI_ON 条件编译,未装 NGUI 不注册)
│ │ ├── Tools/ # 工具辅助类
│ │ └── Models/
│ ├── Editor/
│ │ └── MCPBridgeEditor.cs # MCPBridge 组件 Inspector(CustomEditor,非窗口)
│ ├── Prefabs/
│ │ └── MCPBridge.prefab # 预置组件(默认 127.0.0.1:45678,自动重连)
│ ├── Plugins/ # 依赖 DLL(已含仓库内,clone 即可用)
│ ├── Resources/
│ │ └── bridge-config.json # 包内兜底配置(Editor/Player 首次自动拷贝的来源)
│ └── SimpleMCPBridge.asmdef # 程序集定义(含 versionDefines)
├── SimpleMCPBridge-config/ # 项目本地配置(首次启动自动生成,不在包内)
│ └── bridge-config.json # Editor 用 IP/Port/加密配置
├── ...
SimpleMCPBridge 是独立 git 仓库,不是 Unity 项目的子模块。 Plugins DLL 已包含在仓库内,clone 后无需手动下载。
# 任意目录下克隆
cd D:/dev/ # 举例,随意放哪里
git clone https://github.com/redcool/SimpleMCPServer.git
cd SimpleMCPServer
# 首次需要安装依赖
npm install
npm run build之后每次启动只需运行 start.bat(会自动 build + 启动)。
通过 Unity Package Manager 安装:
UPM Git URL:
https://github.com/CyberAgentGameEntertainment/InstantReplay.git?path=Packages/jp.co.cyberagent.instant-replay#release
或者手动编辑 Packages/manifest.json:
{
"dependencies": {
"jp.co.cyberagent.instant-replay": "https://github.com/CyberAgentGameEntertainment/InstantReplay.git?path=Packages/jp.co.cyberagent.instant-replay#release"
}
}安装后 SimpleMCPBridge.asmdef 的 versionDefines 会自动检测到该包,定义 INSTANT_REPLAY_ON 宏。
- 启动 MCP Server(双击
SimpleMcpServer/start.bat) - 用 Unity 打开项目,把
Assets/SimpleMCPBridge/Prefabs/MCPBridge.prefab拖进场景 (或给任意 GameObject 添加MCPBridge组件) - 选中该物体,Inspector 内显示连接状态/Bridge ID/错误面板,以及 Server IP/Port
字段和 Connect 按钮(默认
127.0.0.1:45678) - 点击 Connect(若勾选 Auto Reconnect,断线自动重连)
MCPBridge 是 [ExecuteAlways] 组件:Edit Mode / Play Mode / 构建后的 Player 均可用;
勾选 Dont Destroy On Load 后跨场景存活,进出 Play Mode 自动重连。
编辑 bridge-config.json:
{
"serverIp": "127.0.0.1",
"serverPort": 45678,
"encryptionKey": ""
}配置加载策略(BridgeConfig.cs,首次自动生成、已存在不覆盖):
- Editor — 项目
Assets/SimpleMCPBridge-config/bridge-config.json,首次自动从包内Resources/bridge-config.json拷贝生成;用户可改,重启生效 - Player —
Application.persistentDataPath/bridge-config.json,逻辑同上 - 兜底 — 包内
Resources/bridge-config.json内嵌默认值
scene.call_component_method 权限字段(可选,需重启生效):
methodBlocklist— 追加拦截项("MethodName"或"TypeName.MethodName",大小写不敏感);代码默认 6 项 (destroy/destroyimmediate/destroyobject/quit/quitimmediate/disconnect)始终生效,无法通过配置移除methodAllowlist— 空 = 关闭;非空 = 白名单模式,只放行命中的方法;白名单不会覆盖黑名单 (代码默认 + 配置追加的双重拦截始终优先)
替代 TLS/wss 的 AES-256-CBC 载荷加密:
| 特性 | 说明 |
|---|---|
| 算法 | AES-256-CBC, PKCS7 padding |
| 密钥派生 | SHA-256(encryptionKey) |
| 格式 | #ENC#<base64(IV+ciphertext)>(前缀格式,不再使用旧版 {"encrypted":"..."} JSON 包装) |
| 空密钥 | 透传(不加密) |
| 密钥不匹配 | 返回 {"type":"error","code":"decrypt_failed"} |
启用:Server 和 Bridge 的配置中设置相同的 encryptionKey。
选择载荷加密而非 TLS/wss 的原因:Unity Editor(Mono)TLS 兼容性差,Android IL2CPP 自签名证书部署复杂。
所有场景工具同时支持两种方式定位 GameObject:
| 参数 | 说明 |
|---|---|
instanceId |
Unity 实例 ID,精确唯一,但 domain reload 后失效 |
path |
Transform 路径(如 "Canvas/Panel/Button"),跨 domain reload 有效 |
解析优先级:instanceId > path。两个都传时先试 instanceId,找不到再 fallback 路径。
| 工具 | 平台 | 参数 | 说明 |
|---|---|---|---|
scene.get_hierarchy |
All | — | 获取完整的场景层级树(含 position、components、children 及 transform path) |
scene.get_objects |
All | nameContains |
按名称过滤查找对象。如果 nameContains 包含 /,则当作 Transform 路径处理(如 "Canvas/Button") |
scene.get_objects_by_type |
All | typeName(必填), nameContains, layer, layerName, isIncludeInvisible |
查找含有指定组件类型的所有 GameObject。typeName 支持短名称如 'Button'、'Image'、'Renderer'、'Collider'、'Selectable' |
scene.get_objects_by_tag |
All | tag(必填), nameContains, layer, layerName |
按 Tag 查找所有 GameObject |
scene.get_objects_by_path |
All | path(必填), nameContains, layer, layerName |
按 Transform 路径(如 "Canvas/Panel/Button")查找 GameObject |
scene.create_object |
All | name, position[3], rotation[3], scale[3], parentId/parentPath |
创建新的 GameObject(支持位置/旋转/缩放/父级) |
scene.delete_object |
All | instanceId/path |
按 instanceId 或 path 删除 GameObject |
scene.set_transform |
All | instanceId/path, position[3], rotation[3], scale[3], space(world|local) |
设置位置/旋转/缩放。space 参数:world(默认,transform.position)或 local(transform.localPosition) |
scene.set_component_property |
All | instanceId/path, componentType, propertyName, value |
设置组件上的可序列化字段/属性值(自动按字段类型转换) |
scene.get_components |
All | instanceId/path |
获取 GameObject 所有组件列表(类型名、完整类型名、enabled 状态) |
scene.get_component_properties |
All | instanceId/path, componentType |
获取组件所有可序列化属性名和当前值 |
scene.set_active |
All | instanceId/path, active(bool) |
启用或禁用 GameObject |
scene.duplicate_object |
All | instanceId/path |
复制 GameObject,新对象命名为 "原名 (Copy)" |
scene.rename |
All | instanceId/path, name |
重命名 GameObject |
scene.set_parent |
All | instanceId/path, parentId/parentPath |
设置父级。parentId/parentPath 留空则解除父级到根 |
scene.add_component |
All | instanceId/path, componentType |
添加组件(如 Rigidbody、BoxCollider)。自动在所有程序集中查找类型 |
scene.remove_component |
All | instanceId/path, componentType |
移除组件 |
scene.enter_play_mode |
Editor | — | 进入 Play Mode |
scene.exit_play_mode |
Editor | — | 退出 Play Mode(使用 delayCall 确保 JSON-RPC 响应先发送) |
scene.pause_play_mode |
Editor | paused(bool) |
暂停或继续 Play Mode |
scene.get_play_mode |
Editor | — | 获取当前播放模式状态。返回:isPlaying、isPaused、mode(edit|playing|paused) |
scene.save_current |
Editor | savePath(可选) |
保存当前场景。savePath 不传则覆盖保存;未命名场景 savePath 为必填 |
scene.instantiate_prefab |
Editor | assetPath(必填), position[3], rotation[3], scale[3], parentId/parentPath |
从项目 Assets 路径实例化预制体到场景 |
scene.set_material ⚠ |
Editor | instanceId/path, materialIndex(默认0), color[3或4], texturePath |
修改 Renderer 材质的颜色或主纹理。资产级材质修改建议直接改 .meta GUID |
scene.load_scene |
All | sceneName(必填), mode(single|additive), async |
加载场景。Editor 非 Play Mode 时打开场景资产;其余经 SceneManager(Play/built 均可用)。⚠ 加载后所有 instanceId 失效,需重新获取层级 |
scene.save_prefab |
Editor | instanceId/path, assetPath(必填) |
将 GameObject 保存为预制体资产(已存在则覆盖)。PrefabUtility 操作不可撤销 |
| 工具 | 参数 | 说明 |
|---|---|---|
scene_view.get_camera |
— | 获取当前 SceneView 相机状态:position、rotation、pivot、size(正交)或 fieldOfView(透视)、isOrthographic、farClip、nearClip。无 SceneView 打开时返回错误 |
scene_view.set_camera |
position[3], rotation[3], pivot[3], size, isOrthographic |
设置 SceneView 相机。所有参数可选。pivot 是相机环绕的世界坐标点。size:正交模式为尺寸,透视模式为 FOV 度数 |
| 工具 | 平台 | 参数 | 说明 |
|---|---|---|---|
recording.start |
All(Play Mode) | width(1280), height(720), fps(30), enableAudio(false), quality(50,1-100) |
开始通过 CyberAgent InstantReplay 录屏(OS 原生编码)。仅在 Play Mode 下可用。返回 success、outputPath、width、height、fps |
recording.stop |
All(Play Mode) | — | 停止录制并开始 MP4 编码。立即返回 status:"encoding"。轮询 recording.status 等待完成 |
recording.status |
All(Play Mode) | — | 查询录制/编码状态。状态流转:idle → recording → encoding → completed/error。完成后返回 filePath |
recording.reset |
All(Play Mode) | — | 强制重置录制系统。编码超时或卡死时用于恢复。清理所有会话状态 |
平台支持:
recording.*基于 CyberAgent InstantReplay,该包官方支持 Android / iOS / macOS / Windows / Linux(需 ffmpeg)/ Web(需 WebCodecs),全部走 OS 原生编码(MediaCodec / VideoToolbox / Media Foundation), 无需外部工具。桥注册平台为Android | iOS | Standalone | Editor且RequirePlayMode = true(仅 Play Mode 注册)。 Editor(Windows/macOS)侧录制——例如 AI 迭代粒子特效时的反馈回路——已默认启用(2026-08 起,Windows Editor 录制已实测通过);multi-bridge 下如 Editor 与 Android 同时在线,遵循 last-registration-wins,需要确定性目标时用bridge.call显式指定。Editor 下输出到项目VideoRecord/(与 camera.screenshot 同根目录),设备构建输出到temporaryCachePath/VideoRecord/。
录制流程:
1. scene.enter_play_mode # 进入播放模式
2. recording.start # 开始录制(默认 1280x720@30fps)
3. scene.set_transform(...) # 操作场景对象
4. recording.stop # 停止录制
5. recording.status → polling # 轮询直到 state="completed"
6. scene.exit_play_mode # 退出播放模式
Quality → Bitrate 映射:
| quality | bitrate | 级别 |
|---|---|---|
| 90-100 | 12 Mbps | 非常高 |
| 75-89 | 8 Mbps | 高 |
| 50-74 | 4 Mbps | 中等(默认) |
| 25-49 | 2 Mbps | 低 |
| 1-24 | 1 Mbps | 非常低 |
FreshFrameProvider:每帧创建独立 RenderTexture,解决 InstantReplay 默认 ScreenshotFrameProvider 的纹理重用竞态问题。
| 工具 | 参数 | 说明 |
|---|---|---|
input.click_screen |
x(必填,0-1), y(必填,0-1) |
在归一化屏幕坐标模拟用户点击。经过完整 EventSystem 事件管线:RaycastAll → PointerDown → PointerUp → PointerClick。返回 hit 列表和实际点击对象。需要场景中有活动的 EventSystem(Play Mode 或 Runtime)。不依赖 Input System,只使用 Unity 内置 UnityEngine.EventSystems |
| 工具 | 参数 | 说明 |
|---|---|---|
castle.click_building ⚠ |
x(必填,0-1), y(必填,0-1) |
在归一化屏幕坐标点击 3D 城堡建筑(Physics.Raycast 层 20+22 + Lua FakeHitResultEvent)。项目特定工具——通用工程无此需求时可 tools.disable 忽略 |
| 工具 | 参数 | 说明 |
|---|---|---|
input.mouse_click |
x(必填,0-1), y(必填,0-1), button(0左/1右/2中) |
在归一化屏幕坐标模拟鼠标点击。使用 Input System 虚拟鼠标(MouseDeviceTools)。适用于接收 Input System 事件的物体 |
input.mouse_move |
dx(必填,pixel), dy(必填,pixel) |
按像素增量移动鼠标(用于视角旋转/瞄准)。正 dx=右移,正 dy=上移。典型值:(50,0)=右转,(0,-30)=上仰。保持当前按钮状态,不中断按住。使用 InputSystem.QueueStateEvent 在物理鼠标设备上 |
input.key_press |
key(必填), action(tap|hold|release) |
模拟键盘按键。action 说明:tap=按下立即释放(跳跃/射击),hold=持续按住(WASD 移动),release=释放按键(key=* 或省略时释放全部)。使用 InputSystem.QueueStateEvent 在物理键盘设备上 |
input.touch |
action(必填,tap|start|move|end), x(0-1), y(0-1), fingerId(0) |
触屏模拟。action: tap=立即触+放,start=开始触摸,move=移动到新位置,end=抬起。滑动手势:连续调用 start → move × N → end |
input.swipe |
startX/startY(必填,0-1), endX/endY(必填,0-1), duration(0.3s), steps(15), fingerId(0) |
从一个点到另一个点平滑滑动手势。异步多帧执行,立即返回估算时长。使用 game.wait 等待完成 |
input.gamepad |
action(必填) |
虚拟手柄控制。action 操作:button→需 button(名称)+ press(tap/press/release);axis→需 axis(名称)+ value(-1..1);set→批量,需 buttons[] + axes{};reset→全部归零;state→查询当前状态;rumble→震动(lowFreq/highFreq 0-1, duration 秒,<=0 停止) |
input.get_state |
— | 查询所有当前输入状态。返回:trackedKeys(当前按下的键列表)、mousePosition(归一化鼠标位置)、mouseDelta、mouseScroll、gamepadState(连接检测+所有按钮/轴值) |
gamepad 按键名:south/a、east/b、north/x、west/y、leftShoulder/lb、rightShoulder/rb、leftStick、rightStick、start、select/back、dpadUp/down/left/right
gamepad 轴名:leftStickX/Y、rightStickX/Y、leftTrigger、rightTrigger
| 工具 | 参数 | 说明 |
|---|---|---|
ui.get_texts |
contains(可选过滤) |
从内存读取所有屏幕 UI 文本(无 OCR,零 Token 成本)。扫描活动 Canvas 中的 Text 和 TextMeshProUGUI 组件。每个元素返回:文本内容、类型、transform path、归一化屏幕矩形 [xMin,yMin,xMax,yMax]、中心点、字号、颜色。同时返回屏幕尺寸作为坐标参考 |
ui.find |
type(Button/Toggle/Slider/…), contains(文本包含), interactable(bool) |
查找可交互 UI 元素(Button、Toggle、Slider、Dropdown、InputField)及屏幕位置和状态。每个元素返回:type、path、label/text、interactable、归一化屏幕矩形、中心点。用 center 位置配合 input.click_screen 点击 |
ui.set_input_field_text |
path/instanceId, text |
设置 InputField/TMP_InputField 文本内容 |
ui.set_toggle |
path/instanceId, value(bool) |
设置 Toggle 开关状态 |
ui.set_slider |
path/instanceId, value(float), normalized(默认true) |
设置 Slider 值。normalized=true 时 0-1 映射到 [minValue,maxValue] |
ui.select_dropdown_option |
path/instanceId, option(int) 或 optionText(string) |
按索引或文本选择 Dropdown 选项 |
ui.drag |
fromPath/fromInstanceId, toPath/toInstanceId |
通过 ExecuteEvents 模拟从一个 UI 元素拖拽到另一个 |
ui.get_tooltip |
path/instanceId 或 x,y |
触发 PointerEnter 并扫描新出现的可见文本,返回 Tooltip 内容 |
针对使用 NGUI(第三方老牌 UI 插件)的旧项目。与上方 uGUI 工具互相独立、互不互斥: 可同时开启,也可
tools.disable ["Ui"]只留 NGUI(或反之,类别Ngui/Ui)。 点击模拟直接复用input.click_screen(NGUI 的 UICamera 响应屏幕坐标)。
安装 NGUI(两种方式任选):tasharen/ngui 仓库无 package.json,不能直接用 versionDefines 检测。
方式 A:包化成 UPM 包(推荐)——NGUI_ON 由 versionDefines 自动定义:
git clone https://github.com/tasharen/ngui.git <某目录>- 仓库根补
package.json:{"name": "com.tasharen.ngui", "version": "3.12.0", ...} - 建 asmdef:
Scripts/NGUI.asmdef(运行时,程序集名NGUI)+Scripts/Editor/NGUI.Editor.asmdef(includePlatforms: ["Editor"],引用NGUI) - 排除示例:
Assets/NGUI/Examples→ 改名Examples~ - 项目
Packages/manifest.json加"com.tasharen.ngui": "file:../../ngui"
方式 B:源码直接放 Assets/ + 手动符号——仅省去「package.json + manifest 引用」:
- NGUI 源码拷入
Assets/NGUI/,必须建与方式 A 相同的两个 asmdef(asmdef 程序集无法引用裸放进 Assembly-CSharp 的 NGUI 代码) - 排除示例:
Assets/NGUI/Examples→ 改名Examples~ - 手动加
NGUI_ON符号:Project Settings → Player → Scripting Define Symbols加NGUI_ON(或SimpleMCPBridge.asmdef的defineConstraints加"NGUI_ON")
⚠️ 警告:#if NGUI_ON打开但 NGUI 类型不可解析会报 CS0246——NGUI 源码必须放在有NGUI.asmdef的程序集里, 且SimpleMCPBridge.asmdef的references含"NGUI"(软引用)。两种方式此前提相同。
SimpleMCPBridge 侧已配好:asmdef versionDefines(com.tasharen.ngui → NGUI_ON)+ references 软引用 "NGUI"。未装 NGUI 时工具不注册、仅一条 warning,不影响编译。
| 工具 | 参数 | 说明 |
|---|---|---|
ngui.get_texts |
contains(可选过滤) |
从内存读取所有 NGUI UILabel 文本(无 OCR)。每个元素返回:文本内容、类型、transform path、归一化屏幕矩形 [xMin,yMin,xMax,yMax]、中心点、字号、对齐、颜色。同时返回屏幕尺寸 |
ngui.find |
type(UIButton/UIToggle/UISlider/UIInput), contains(文本包含), interactable(bool) |
查找可交互 NGUI 元素及屏幕位置和状态。每个元素返回:type、path、label/text、interactable、归一化屏幕矩形、中心点。用 center 配合 input.click_screen 点击 |
ngui.find_widgets |
type(widget 类型名如 UITexture/UISprite/UILabel), contains(名称或文本包含) |
查找所有 NGUI UIWidget(UITexture/UISprite/UILabel 等)及屏幕位置。每个元素返回:type、name、path、instanceId、归一化屏幕矩形、中心点;UILabel 额外含 text/fontSize/alignment/color。定位纯显示元素(如背景图)用此工具 |
针对使用 UI Toolkit(
UnityEngine.UIElements,UXML/USS/UIDocument)构建运行时 UI 的项目。 与 uGUI(类别Ui)和 NGUI(类别Ngui)互相独立、互不互斥,类别Uitk。 UI Toolkit 是引擎内置模块,无条件编译——无需装包、无需 versionDefines,注册即用; 面板未 attach(非运行态)时优雅返回空结果,无需 Play Mode。 不设UITK_ON条件编译(有意决策):com.unity.modules.uielements是内置模块非可选包,2022.3 必在; 客户工程 runtime 不用 UITK 时用tools.disable ["Uitk"]裁剪(临时、任务级,domain reload 自动恢复全开)。 UITK 交互事件依赖 EventSystem 桥接:面板要接收指针事件,场景必须有PanelEventHandler+PanelRaycaster(通常挂在 UIDocument 的 PanelSettings 持有者上,如EventSystem/Default Panel Settings)——没有桥接则 UITK 无事件。 注意:UITK 元素是 VisualElement(非 GameObject,无 instanceId)——用panelIndex/panelPath+ 元素path寻址 (uitk.get_elements返回的 fullPath)。input.click_screen(EventSystem)能否驱动 UITK 取决于场景是否桥接: 面板 GameObject 挂了PanelEventHandler+PanelRaycaster时(UIDocument + EventSystem 的标准集成),click_screen 可经 EventSystem 射中面板并触发 Clickable 的clicked(已实测验证);未桥接时 EventSystem 射不到 UITK 面板。 确定性点击仍用uitk.click(向 panel 注入 pooled PointerDown/Up,Clickable 自动生成 ClickEvent)。
| 工具 | 参数 | 说明 |
|---|---|---|
uitk.get_panels |
— | 列出所有 UIDocument 面板:name、gameObjectPath、enabled、activeInHierarchy、attached、sortingOrder、panelSettingsSortingOrder、屏幕尺寸。用 gameObjectPath 作为后续调用的 panelPath |
uitk.get_texts |
contains(可选过滤) |
从内存读取所有 UITK 文本(无 OCR)。扫描 Label/TextElement/TextField。每个元素返回:文本、类型、name、fullPath、归一化屏幕矩形 [xMin,yMin,xMax,yMax]、字号、颜色(Button 排除,由 uitk.find 覆盖) |
uitk.find |
type(Button/Toggle/...), contains |
查找可交互 UITK 元素(Button/Toggle/Slider/SliderInt/DropdownField/TextField/ScrollView)及屏幕位置和状态。每个元素返回:typeName、name、fullPath、interactable、text/value、choices 数、归一化屏幕矩形、中心点 |
uitk.get_elements |
panelIndex/panelPath(可选,默认全部) |
导出 UITK 视觉树。每个元素返回:name、typeName、fullPath、classes、enabled、visible、text/value、归一化矩形。单面板超 500 元素截断(truncated:true) |
uitk.click |
panelIndex/panelPath + path(元素模式)或 x,y 归一化坐标(坐标模式) |
点击 UITK 元素。元素模式:在 el.worldBound.center 派发 pooled MouseDown+MouseUp(GetPooled(Event),Clickable 生成 ClickEvent);坐标模式:归一化坐标映射到 root.worldBound(与 uitk.find/get_elements 的 normalizedRect 同原点,左上角)+ panel.Pick 命中检查。需面板已 attach 且已布局(运行态) |
uitk.set_value |
panelIndex/panelPath + path + value, silent(可选) |
设置元素值。Toggle=bool、Slider=float、SliderInt=int、DropdownField=索引或选项文本、TextField=string。默认 value=X 触发 ChangeEvent 生效;silent=true 用 SetValueWithoutNotify 静默 |
uitk.create_element |
panelIndex/panelPath + type(Button/Label/Slider/Toggle), parent(可选), name(可选), text(可选) |
运行时创建 UITK 元素并加入面板视觉树。⚠ 不持久——面板刷新/UXML 重新应用即销毁。返回新元素路径 |
uitk.remove_element |
panelIndex/panelPath + path(必填) |
从面板视觉树移除元素。⚠ 不持久——面板刷新即恢复。返回被移除路径及原父路径 |
| 工具 | 参数 | 说明 |
|---|---|---|
input.action |
keys[], mouse{}, axes{} |
单次调用组合输入。keys= {key, action}数组(action: tap/hold/release);mouse=可选 x/y(归一化)、dx/dy(像素增量)、scroll(浮动)、buttons(数组);axes=Input System 动作名→值字典(如 {"Horizontal":1.0,"Vertical":0.5,"Fire1":1.0})。轴通过 ApplyOverrideValue 设置(同时馈入 legacy Input.GetAxis)。返回执行的操作列表 |
| 工具 | 参数 | 说明 |
|---|---|---|
game.get_state |
includeUI(默认true), playerPath(可选) |
单次调用获取游戏综合快照。返回:活动场景名+buildIndex、时间(time/deltaTime/timeScale/frameCount)、屏幕尺寸、播放模式状态、所有 UI 文本、所有可交互 UI、可选玩家位置(通过 playerPath 指定)、活动 Input System 轴、相机信息(position、forward、fov、isOrthographic、nearClipPlane、farClipPlane)。设置 includeUI=false 跳过 UI 扫描加速响应。核心感知工具——一次调用提供 AI 80% 决策所需信息 |
game.get_time_scale |
— | 获取当前时间缩放。返回:timeScale(Time.timeScale)、fixedDeltaTime(Time.fixedDeltaTime)、realtimeSinceStartup、frameCount |
game.get_spatial |
origin3, playerPath(可选), radius(默认10), maxObjects(默认20), tag, layerName, typeFilter |
获取参考点周围指定半径内的 3D 物体空间信息。返回物体名、instanceId、组件列表、世界坐标、距离、方向(归一化向量)、tag、layer。自动过滤空对象。支持按 tag/layer/组件类型过滤 |
game.watch |
signals |
注册一组信号持续监测。每个 signal 含 id、type(property)、path、component、property。返回当前值作为基线。示例:[{"id":"hp","type":"property","path":"Player","component":"Health","property":"currentHP"}] |
game.get_delta |
— | 获取自上次调用以来所有 watch 信号的变化。桥侧每 ~167ms(10帧)持续检测并缓存变化,本调用纯读缓存(读后清空)——两次调用之间的短事件也不会漏。只返回有变化的值(含新旧值)。无变化时返回空 |
game.get_entities |
typeFilter(可选, AI/Health/CharacterController), maxResults(默认20) |
批量获取场景中带指定组件的实体及其关键状态。每个实体返回:name、instanceId、position、rotation、velocity(如有)、组件摘要值 |
game.get_player |
playerPath(可选), includeComponents(默认false) |
一步获取玩家完整状态。返回:position、rotation、velocity、动画状态(如有 Animator)、所有挂载组件的关键属性。不传 playerPath 时自动查找 tagged Player |
game.do_sequence |
steps, timeout(默认30s) |
在 Unity 侧一次性执行一组动作序列。返回 sequence ID 立即返回。支持 step type:wait(等待)、key(键盘)、mouse_click(鼠标点击)、mouse_move(鼠标移动)、gamepad(手柄)、click_screen(UI 点击)。key/mouse/gamepad 需 Input System,click_screen 不需要 |
game.get_animator_state |
instanceId/path(必填) |
获取 Animator 当前状态。返回:stateHash、tagHash、normalizedTime、speed、parameters(所有参数名+类型+值) |
game.sequence_status |
id(必填) |
轮询 game.do_sequence 的执行状态。返回 status(running/completed/error)、step(当前步)、total(总步数)、log(最近日志) |
game.set_time_scale |
timeScale(必填,float) |
设置 Time.timeScale。0=暂停,0.5=半速,1=正常,2=2 倍速。返回设置后的 timeScale 和 fixedDeltaTime |
game.wait |
type(必填), value, timeout(默认30s) |
启动异步等待操作。立即返回 wait ID,用 game.wait_check 轮询完成状态 |
game.wait_check |
id(必填) |
轮询 game.wait 的完成状态。返回 status:completed/waiting/timeout/error |
game.batch |
calls |
在单个 Unity 帧内批量执行多个工具调用。calls 为 {name, arguments} 数组,最多 50 个。返回 {count, results: [{name, result}]}。将 N+1 次 round trip 降为 1 次 |
| 工具 | 平台 | 参数 | 说明 |
|---|---|---|---|
asset.refresh |
All | — | 刷新 Unity 资产数据库以导入新文件或检测变更。如果导入了新脚本,将触发 domain reload 且 MCP 连接会断开。客户端应轮询 /health 直到 bridgeConnected=true 确认完成 |
asset.find_assets |
Editor | nameContains(可选), typeFilter(可选) |
按名称和/或类型搜索 Assets。typeFilter:Unity 资源类型名如 'Prefab'、'Material'、'Texture'、'Scene'。返回 {filter, count, assets: [{path, name, type, guid}]} |
asset.find_references |
All | assetPath(必填) |
查找引用指定资源的所有资源。通过扫描文件内容中的目标 GUID 实现。可靠但较慢——扫描 Assets/ 下所有文本资源文件。返回引用者数组及 GUID |
asset.create |
Editor | type(folder|material), name, path, color3或4 |
创建资源。folder:新建文件夹;material:新建材质(可选颜色)。AssetDatabase 操作不可撤销 |
asset.delete |
Editor | assetPath(必填), force(bool) |
删除资源。默认先做 find_references GUID 引用预检——有引用时返回错误(列出引用数+首个引用者),force=true 跳过预检 |
asset.rename |
Editor | assetPath(必填), newName(必填,不含扩展名) |
重命名资源,返回新路径。AssetDatabase 操作不可撤销 |
asset.move |
Editor | assetPath(必填), newPath(必填) |
移动(或改父级)资源到新路径。返回旧+新路径 |
asset.build_bundle |
Editor | shaderPaths, fileName(可选), buildTarget(可选) |
构建 Shader AssetBundle 并上传到服务器,返回手机可达的 abUrl 供 shader.hot_replace 使用 |
Unity 没有 PlayerPrefs key 枚举 API——
playerprefs.get_all只能列出本会话经 set/get 见过的 key(其他代码写入的持久 key 无法枚举,用playerprefs.get精确读取)。
| 工具 | 参数 | 说明 |
|---|---|---|
playerprefs.get_all |
— | 获取本会话已知的所有 PlayerPrefs。每条含 key、type(int/float/string 类型嗅探)、value |
playerprefs.get |
key(必填), keyType(int/float/string, 可选) |
读取单个 key。keyType 省略时按 sentinel 默认值嗅探类型(int→float→string 顺序) |
playerprefs.set |
key(必填), value(必填), valueType(可选), save(默认true) |
设置 key。valueType 省略时按 JSON 值类型推断(int/float/string);save=true 时 PlayerPrefs.Save() |
playerprefs.delete |
key(必填) |
删除单个 key 并保存 |
| 工具 | 参数 | 说明 |
|---|---|---|
physics.raycast |
origin3, direction3, maxDistance, layerMask |
从 origin 沿 direction 发射射线,返回第一个碰撞结果。返回:hit(bool)、point、normal、distance、colliderType、gameObjectName、instanceId、transform path |
physics.box_cast |
origin[3], halfExtents[3], direction[3], maxDistance(默认100), layerMask(默认-1) |
沿 direction 方向扫描盒体,返回第一个碰撞结果 |
physics.sphere_cast |
origin[3], radius, direction[3], maxDistance(默认100), layerMask(默认-1) |
沿 direction 方向扫描球体,返回第一个碰撞结果 |
physics.overlap_sphere |
center[3], radius, layerMask(默认-1) |
返回球体内所有碰撞体。每个结果包含:name、instanceId、path、position、tag、layer |
physics.overlap_box |
center[3], halfExtents[3], rotation4 |
返回盒体内所有碰撞体。rotation 为可选四元数 [x,y,z,w] |
| 工具 | 参数 | 说明 |
|---|---|---|
camera.screenshot |
savePath(可选), cameraName(默认Main Camera), width, height |
截取指定相机画面并保存为 PNG。savePath 是相对路径(相对 VideoRecord/ 根目录)或绝对路径;省略时自动生成时间戳文件名(如 screenshot_20260814_125044.png)。路径根目录:Editor → <项目根>/VideoRecord/,Runtime → <temporaryCachePath>/VideoRecord/。自动创建目录。返回绝对文件路径 |
| 工具 | 参数 | 说明 |
|---|---|---|
audio.get_sources |
maxResults(默认50,上限200) |
返回所有正在播放的 AudioSource 信息。每个结果包含:clipName、volume、isPlaying、time、loop、spatialBlend、position、distanceFromListener、path、instanceId |
创作/预览(Edit Mode 可撤销)+ 运行时播放(Play Mode 才注册)。使用模型:Editor 创作(资产)+ Runtime 播放。模块属性走 typed 代码路径(零反射,规避 CS1612 与 il2cpp 裁剪),改模块属性须先存局部变量。
| 工具 | 平台 | 参数 | 说明 |
|---|---|---|---|
particle.get_systems |
All | maxResults(默认50) |
列出场景所有 ParticleSystem + 实时状态(isPlaying/isEmitting/particleCount/time/enabledModules/renderer) |
particle.get_state |
All | instanceId(必填) |
单个系统的详细状态 + 关键模块配置(main/emission/shape 等值) |
particle.create |
All | name, position[3], parentId/parentPath, loop, playOnAwake, startSpeed, startSize, startLifetime, startColor, rateOverTime, shapeType, gravityModifier, material, materialDir, shader |
创建 GameObject + ParticleSystem(可带初始配置)。material:内置名(Default-Particle/Sprites-Default/Default-Material)或 Assets/... 路径,赋给 ParticleSystemRenderer(创建后必须赋材质粒子才可见)。材质资产化:Editor 创作路径下内置名自动落地为磁盘资产 <对象名>_<材质名>.mat(目录解析:materialDir 显式 → prefab 实例源 prefab 同目录 → 兜底 Assets/),特效引用资产不用实例;runtime(Player/Play Mode)才用材质实例。shader:创建材质时指定 shader(名或 Assets/... 路径),缺省 URP Particles/Simple Lit。Edit Mode 可撤销 |
particle.set |
All | instanceId(必填), property(必填), value, materialDir(renderer.material 时), shader(renderer.material/shader 时) |
按 typed 代码路径设置模块属性:main.*/emission.*/shape.*/velocityOverLifetime.*/forceOverLifetime.*/colorOverLifetime.*/sizeOverLifetime.*/rotationOverLifetime.*/noise.*/trails.*/textureSheetAnimation.*/lights.*/collision.*/renderer.*(含 renderer.material,Editor 下同样材质资产化;renderer.shader 换材质 shader,Editor 下持久化到材质资产)。MinMaxCurve:单数=常量,[min,max]=随机,[[t,v],...]=曲线;颜色:单色,[c1,c2]=双色渐变(colorOverLifetime 时间渐变/startColor 随机),[[t,c],...]=完整渐变;emission.burst:[count,time] 或 [[c,t],...] |
particle.simulate |
All | instanceId(必填), time(必填), restart(默认true), withChildren(默认true) |
确定性预览到 t 时刻(Edit Mode 可用,不实际播放) |
particle.play |
All (Play Mode) | instanceId, restart(默认true), withChildren |
播放(可强制重新开始) |
particle.pause |
All (Play Mode) | instanceId, withChildren |
暂停(粒子冻结) |
particle.stop |
All (Play Mode) | instanceId, clear(默认true), withChildren |
停止(可选清空粒子) |
particle.clear |
All (Play Mode) | instanceId, withChildren |
清空所有粒子但不停止系统 |
particle.emit |
All (Play Mode) | instanceId, count(默认1), position[3], velocity[3], startLifetime, startSize, startColor |
通过 EmitParams 一次性爆发(无需播放中)。粒子参数可选,缺省用系统默认 |
材质颜色/贴图用 scene.set_material(ParticleSystemRenderer 是 Renderer);保存用 scene.save_prefab(assetPath 参数)。素材创作流程:先 scene.instantiate_prefab(PrefabUtility.InstantiatePrefab,保留 prefab 关联)把特效 prefab 放进场景 → particle.set 赋内置材质自动落在 prefab 同目录 → 或 particle.create/set 显式传 materialDir。配方参考:火焰/爆炸/火花/烟雾/魔法/雨雪/光点/传送门 → 见 AGENTS.md「Particle 特效创作」配方表。
| 工具 | 参数 | 说明 |
|---|---|---|
nav.query_path |
start[3], end[3], areaMask(默认-1), snapDistance(默认2.0) |
在 NavMesh 上计算两点间路径。返回:reachable(bool)、status(PathComplete/PathPartial/PathInvalid)、waypoints[[x,y,z]...]、distance(float) |
nav.sample_position |
position[3], maxDistance(默认2.0), areaMask(默认-1) |
将世界坐标吸附到最近的 NavMesh 点 |
nav.has_navmesh |
— | 检查 NavMesh 是否存在。返回:hasNavMesh(bool)、vertexCount、triangleCount |
nav.move_to |
instanceId/path(必填), destination3, speed(可选), stopDistance(可选) |
设置 NavMeshAgent 目标点,自动寻路移动。返回 agent 状态(pathPending、remainingDistance、isStopped、velocity) |
这些工具使用 RequirePlayMode = true,仅当 Play Mode 激活时才注册。
由 SimpleMcpServer 直接提供,不在 Bridge 注册:
| 工具 | 参数 | 说明 |
|---|---|---|
bridge.list |
— | 列出所有已连接 Bridge(ID/IP/工具数) |
bridge.call |
target(bridgeId), method, params |
定向调用指定 Bridge 上的工具 |
每个工具的描述都以 [类别] 开头(如 [Scene] 获取完整的场景层级树...),
类别由工具名前缀自动派生(scene.xxx → Scene,assetbundle.xxx → AssetBundle),
方便人眼和 AI 快速扫描、分组。
工具按类别动态注册/注销,避免无关工具干扰调用方:
示例:本次任务只用场景操作,把无关的 AssetBundle/Shader/Recording 关掉:
{"name": "tools.disable", "arguments": {"categories": ["AssetBundle", "Shader", "Recording"]}}之后 tools.list_categories 会显示这三类 "enabled": false,它们也不会出现在
tools/list 结果里。任务结束后 tools.reset 一键还原。
要点:
- 默认全部启用——条件编译只决定工具"是否存在"(装包即编译注册,如 NGUI 装上后
ngui.*自动可用),tools.enable无需 AI 主动调用;tools.disable是纯 AI 侧裁剪机制,用于砍掉无关工具省 token - 类别状态是 static 的(纯内存,无 EditorPrefs/PlayerPrefs 持久化):跨 Play Mode 切换、router 重建保留; 脚本重编译 / Editor 重启(domain reload)后重置为全部启用
tools.disable all会保留Tools类别本身(4 个控制工具),保证永远能恢复mcp.list_tools/tools/list返回的就是当前启用的工具子集- 新增工具类时无需手动登记类别——注册器按名称前缀自动归类
多个 Unity Bridge 可同时连接。三层机制:
- 默认路由:
last-registration-wins— 后连接的 bridge 覆盖同名工具 - 显式路由:
bridge.call指定target= bridgeId,绕过默认路由,确定性调用 - 断开 failover:某 bridge 断开时,其路由的工具若被其他在线 bridge 注册,自动回退;否则清除
- Editor bridge(92+ tools)与 Android bridge(部分工具)共存
- Bridge 断线:该 bridge 的工具从路由表移除;有其他 bridge 注册同工具时自动回退
- 无 bridge 时待处理调用进入重试队列(30s 宽限期)
- 使用
bridge.list查看所有已连接 bridge(ID、IP、工具列表) ⚠️ BridgeId 每次连接重新生成(GUID),clientPort是随机客户端端口——不要用 ip+port 作稳定标识,精确调用一律用 bridgeIdtools/list(含 MCP ListTools)返回的每个 bridge 工具 description 带[bridge: ip:port (id前8位)]前缀 = 该工具当前路由目标(last-registration-wins 结果)。AI 可直接从工具列表得知调用会打到哪个 bridge
Bridge 可通过 Server 调用外部 LLM:
Bridge → Server: {"type":"ai_request","prompt":"...","context":{...},"requestId":"..."}
Server → Bridge: {"type":"ai_response","text":"...","requestId":"..."}
- 支持任何 OpenAI 兼容 API(OpenAI、Agnes、Ollama 等)
- API Key 支持
LLM_API_KEY环境变量 - Server
config.json中llm.enabled: false关闭
- 创建新类:
using SimpleMCPBridge.Runtime;
using static SimpleMCPBridge.Runtime.Handlers.HandlerUtils;
public class MyTools
{
[MCPTool("my_tool_name", "工具描述",
Platform = MCPToolPlatforms.All,
RequirePlayMode = true)]
public static string MyTool(string paramsJson)
{
var args = ParseJsonObject(paramsJson);
var name = GetString(args, "name", "default");
return "{\"success\":true}";
}
}RequirePlayMode = true 时该工具仅当应用处于播放模式时注册,避免 Editor Edit Mode 下误调用。
类别自动派生:工具描述会带上 [类别] 前缀,类别默认取自工具名前缀
(my_tool_name → MyTool,assetbundle.xxx → AssetBundle)。如需自定义类别:
[MCPTool("my_tool_name", "工具描述", Category = "CustomGroup")]归类后即可用 tools.enable / tools.disable 按类别动态开关。
- 自动注册 —
MessageRouter构造时扫描程序集,自动发现带[MCPTool]的方法。
- 方法签名必须为
public static string MethodName(string paramsJson) - 类需要 public 无参构造函数(static class 自动跳过)
[MCPToolClass]标记类可加速发现Platform可选,控制哪些构建目标注册该工具RequirePlayMode可选,为true时工具仅在 Play Mode 时注册Category可选,覆盖自动派生的类别名(默认取工具名前缀)- 修改 C# 后等待 Unity 编译完成
| 方法 | 说明 |
|---|---|
ParseJsonObject(json) |
解析 JSON 为 Dictionary<string, object> |
GetString(dict, key, default) |
取字符串,带默认值 |
GetRequiredString(dict, key) |
取必填字符串 |
GetRequiredInt(dict, key) |
取必填 int |
GetOptionalInt(dict, key) |
取可选 int,返回 int? |
GetRequiredBool(dict, key) |
取必填 bool |
GetOptionalFloatArray(dict, key) |
取可选 float[] |
ErrorJson(message) |
构建错误响应 JSON |
用法:using static SimpleMCPBridge.Runtime.Handlers.HandlerUtils;
录制工具依赖 CyberAgent InstantReplay(Unity Package jp.co.cyberagent.instant-replay)。
SimpleMCPBridge.asmdef 中 versionDefines 自动检测包是否存在,存在时定义 INSTANT_REPLAY_ON:
| 场景 | INSTANT_REPLAY_ON | FreshFrameProvider | RecordingHandler |
|---|---|---|---|
| 包已安装 | ✅ 自动定义 | ✅ 编译 | ✅ 编译 |
| 包未安装 | ❌ 未定义 | ❌ 跳过 | ❌ 跳过 |
editor.eval 通过 Mono.CSharp 动态编译并在内存中执行任意 C# 代码 —— 等同于完全的 Unity/机器控制(读写任意文件、删除资产、网络访问、启动进程等)。这是工具集最强的"逃生舱":当某个场景操作没有专用工具覆盖时,AI 可用 eval 即时补救。
默认 ON,以用户方便为先。开发调试、快速原型、补救缺口工具时即时可用,不必先翻配置。若环境不可信(共享机器/公网暴露的服务器),关闭它。
双重 gate(任一关闭即不可用):
- Server 侧
config.json→evalEnabled(默认true):false时tools/list不暴露editor.eval给 agent,agent 看不到也就调不到。 - Bridge 侧
EditorPrefs SimpleMCPBridge_EvalEnabled(默认true):执行前再检查一次;MCPBridge Inspector 显示为 "Editor Eval (global)" toggle,关闭后调用返回错误。
风险面:任何能调用 /rpc 的 AI 都能执行任意代码。本工具不做代码内容过滤(任意代码无法穷举拦截,黑名单无意义)。安全靠网络层 gate —— allowedIps 白名单默认仅本机(127.0.0.1/::1)。云部署(ip:0.0.0.0)前务必扩白名单到可信 IP 段,或直接 evalEnabled:false。
关闭方法(任一即可):
SimpleMcpServer/config.json设"evalEnabled": false(重启 server,对所有 agent 隐藏)- Unity Editor: MCPBridge Inspector 的 eval toggle(立即生效,单机)
- 代码:
EditorPrefs.SetBool("SimpleMCPBridge_EvalEnabled", false)
详见 AGENTS.md「editor.eval 安全说明」。
加密载荷格式从 {"encrypted":"<base64>"} JSON 包装改为 #ENC#<base64> 前缀格式。Server 端也必须使用 #ENC# 前缀,两端需同步更新。
多线程安全、资源泄漏修复、性能优化等多项改进。
根因:InstantReplay 始终创建音频轨道,即使 enableAudio=false。音频轨道从未收到帧。
影响:无害。MP4 视频轨道完整,播放正常。
BuildJsonObject(("key", "value")) 生成 "key":value(裸词,无效 JSON)。字符串值必须通过 EscapeString("value") 包装: ("key", JsonHelper.EscapeString("value")) → "key":"value"。数值和 bool 不需要包装。
同类陷阱 —— 控制字符未转义:EscapeString 原先只处理 " \ \n \r \t,漏了 JSON 规范要求转义的 U+0000–U+001F 控制字符(及 \b \f)。工具返回含控制字符的字符串(如 Vector3.ToString() 的 (0, 0, 0)、二进制数据、带格式符的文本)会产出无效 JSON → 服务器日志 Invalid JSON from bridge → 客户端表现 30s 超时(像 handler 挂起,实际是响应被丢弃)。EscapeString 已补全 \b \f 及 \uXXXX 兜底;非字符串值(float/int 的 ToString("G"))不受影响。诊断:server.log 搜 Invalid JSON,用 node -e "JSON.parse(...)" 定位裸词位置。
ws 库的 WebSocketServer 在进程退出后有时端口保持绑定:
Get-Process -Name "node" | Stop-Process -Force开启多个 Unity 进程(如多个 Editor 窗口)时各自建立独立 WebSocket 连接,每个进程一个 bridgeId。
MCPToolRegistry 在 BridgeClient 构造时扫描注册工具。由于 BridgeClient 是单例,工具注册只发生一次。进出 Play Mode 时如果 bridge 未断开,工具列表不会动态更新。但在标准工作流中(domain reload → bridge 重连),每次进入/退出 Play Mode 都会重新注册。
MCP Server(SimpleMcpServer)必须在一个可见的 cmd 窗口中运行,不能以无窗口/后台方式启动:
- 用
start.bat启动(会打开 cmd 窗口并显示日志) - 不要用
Start-Process -NoNewWindow或后台服务方式启动 - 原因:服务器日志(连接、工具调用、AB 传输)实时输出到窗口,便于排查问题;且服务器进程需要保持前台运行
当 Editor 和 Android 两个 bridge 同时连接,且 Editor 处于 Play Mode 时,last-registration-wins 规则会让 Editor 覆盖同名工具(如 shader.hot_replace、assetbundle.hot_replace)的路由。此时直接调用这些工具会路由到 Editor 而非 Android。
这是有意的默认行为(见「Multi-Bridge 路由」三层机制):无指定目标时,服务器按注册顺序取最后一个。需要确定性目标时,用 bridge.call 显式指定:
{
"name": "bridge.call",
"arguments": {
"target": "<android_bridge_id>",
"method": "assetbundle.hot_replace",
"params": { "abUrl": "http://<server_ip>:45678/ab/<bundle>" }
}
}bridge.list 可查看各 bridge 的 ID 和 IP(Android 通常为 10.0.x.x)。
Unity 的 AB 去重机制:相同内容的 AssetBundle 只能被 LoadFromMemory 加载一次。若 shader.hot_replace 已加载某 AB 且未卸载,后续 assetbundle.hot_replace 加载同内容 AB 会报:
The AssetBundle 'Memory' can't be loaded because another AssetBundle with the same files is already loaded.
解决:每次部署前先调用 assetbundle.unload_all 卸载所有已加载 bundle,或使用不同内容的 AB(不同文件名/内容)。
| 仓库 | 说明 |
|---|---|
| SimpleMCPBridge | 本仓库 — Unity 桥接包 |
| SimpleMcpServer | MCP Server — Node.js/TypeScript |
| InstantReplay | CyberAgent InstantReplay — OS 原生硬编码录屏 |