ET UnityBridge 深度解析:命令行桥接 Unity Editor 的架构、协议与 AI 自动化实操指南
【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET
导读
cn.etetet.unitybridge是 ET 框架中的Unity 本地文件桥接包:它通过一个纯命令行程序ET.UnityBridge.dll与 Unity Editor 内的文件宿主进程通信,让 AI Agent 或脚本以dotnet命令直接驱动 Unity——查询编译状态、进入/退出 PlayMode、操作资源、场景、GameObject、Inspector、Prefab,甚至截图和跑 Editor 测试。读完本文,你将掌握该桥接包的目录结构与通信协议、CLI 全部传输参数与命令发现方法、deferred 长时命令的底层机制,以及一套可直接上手的 AI 操作 Unity 的最小工作流。
包定位与三层结构
根据 AGENTS.md 的概述,本包围绕“Unity 本地文件桥接”提供三部分能力:
| 组成 | 职责 |
|---|---|
DotNet~ | 纯命令行程序ET.UnityBridge,对应工程 ET.UnityBridge.csproj |
Scripts/Editor | Unity Editor 文件宿主,汇入ET.Editor,负责在编辑器内轮询请求、分发执行、写回响应 |
Scripts/Model/Share | 桥接命令、错误码与共享文件协议,两端共用的数据结构 |
对应的核心目录速览(引自原文档):
| 路径 | 说明 |
|---|---|
| DotNet~ | ET.UnityBridge.csproj与命令行入口 |
| Scripts/Editor | Unity Editor 宿主、处理器与分发逻辑 |
| Scripts/Model/Share | 桥接命令、错误码、路径与文件存储协议 |
这种“命令行客户端 + Editor 宿主 + 共享协议”的三层设计,使桥接完全不依赖网络端口或 HTTP 服务:所有数据通过本地磁盘文件交换,Unity 侧无需开启任何服务器监听,天然适配编辑器安全沙箱。
通信原理:本地文件存储协议
根目录解析优先级
桥接两端通过同一个根目录交换文件,路径解析逻辑在 UnityBridgeStorage.cs 的UnityBridgePathHelper.ResolveRoot中实现,优先级如下:
- 显式传入的
--root <路径>; - 环境变量
ET_UNITY_BRIDGE_ROOT; - 默认值
Temp/UnityBridge(相对项目根目录)。
CLI 端在 Program.cs 中调用UnityBridgePathHelper.ResolveRoot(root)完成解析;Unity 侧 UnityBridgeEditorHost.cs 在静态构造时用同样的规则缓存根目录。两端必须解析到同一目录,否则就会出现wait unity bridge response timeout。
目录布局与文件流转
UnityBridgeFileStore.EnsureDirectories(见 UnityBridgeStorage.cs)会创建以下子目录:
| 目录 | 用途 |
|---|---|
requests/ | CLI 写入的待处理请求({rpcId}.json) |
processing/ | Editor 取出后正在处理的请求 |
responses/ | Editor 写回的响应({rpcId}.json),CLI 读取后删除 |
deadletter/ | 无法解析的请求被移入的“死信”区 |
state/ | 内部状态,含pending-command.json(deferred 命令)与idempotency/(幂等缓存) |
一次完整请求的流转为:CLI 通过UnityBridgeFileStore.WriteRequest原子写入requests/{rpcId}.json(UnityBridgeStorage.cs)→ Editor 轮询到后用File.Move移入processing/(保证单次只被一个进程处理)→ 执行完成后WriteResponse写回responses/{rpcId}.json并删除 processing 文件 → CLI 读到响应即删除该响应文件。所有写入都走“临时文件 + 替换”的原子写路径(WriteTextAtomic),避免半写状态被另一端读到。
RPC 信封
CLI 与 Editor 交换的不只是命令本身,而是一个信封对象UnityBridgeRequestEnvelope(见 UnityBridgeCommands.cs),包含四个字段:
RpcId:本次调用的唯一标识,由 CLI 用进程 ID、随机数与时间戳异或生成(Program.cs);IdempotencyKey:幂等键,配合响应缓存实现“同一命令重复发送只执行一次”;TimeoutMs:超时上限(0 表示不设超时);CommandJson:真正的命令 JSON,以_t字段标注命令类型。
CLI 使用入门
入口与最小流程
CLI 入口固定为:
dotnet ./Bin/ET.UnityBridge.dll如果Bin/ET.UnityBridge.dll尚不存在,需要先用et-build编译确认工具已生成。不带参数直接运行会输出unity bridge command is empty并以退出码 2 结束(见 Program.cs)。
最小可用命令是Ping,用于探测宿主是否在线:
dotnet ./Bin/ET.UnityBridge.dll '{"_t":"Ping"}'需要命令列表或详细状态时使用HostState:
dotnet ./Bin/ET.UnityBridge.dll '{"_t":"HostState"}'传输参数
CLI 支持四个传输参数(在 Program.cs 中以TransportOptions定义):
| 参数 | 作用 | 默认值 |
|---|---|---|
--root <路径> | 显式指定桥接根目录 | 环境变量ET_UNITY_BRIDGE_ROOT,否则Temp/UnityBridge |
--waitMs <毫秒> | 客户端等待最终响应的时间 | 15000 |
--timeoutMs <毫秒> | 单条命令的服务端超时上限(写入信封) | 0(不设限),服务端另有分命令默认值 |
--idempotencyKey <字符串> | 幂等键,用于去重与结果缓存 | 自动生成 GUID(N 格式) |
组合示例:
dotnet ./Bin/ET.UnityBridge.dll '{"_t":"HostState"}' --root "Temp/UnityBridge" --waitMs 15000 --timeoutMs 10000 --idempotencyKey "host-state-check"等待与自动延长机制
SendRequest(Program.cs)在写入请求后以 100ms 间隔轮询响应文件,直到超过waitMs截止时间。关键细节:当未显式指定--waitMs时,如果检测到state/pending-command.json中存在与当前 RpcId 匹配的 deferred 命令,CLI 会自动把等待期限延长到DefaultDeferredWaitMs(185 秒)。这意味着Compile、EnterPlay这类耗时长命令默认最多能等到 185 秒,无需手工加大--waitMs。
命令协议与常用命令
协议定义位置
全部命令与响应结构定义在 Proto 目录下:
- UnityBridge_C_11100.proto:状态类命令、资源类命令、场景/选择集/测试/截图/GameView 命令,以及
BridgeVector2/3、BridgeQuaternion、BridgeTransformInfo、BridgeObjectInfo、BridgeAssetInfo等共享消息结构; - UnityBridge_C_11400.proto:GameObject/Transform/菜单/Prefab/Inspector/Undo-Redo/BatchExecute 命令。
六个核心状态命令
| 命令 | 作用 | 响应特有字段 |
|---|---|---|
Ping | 连通性探测,返回编译/PlayMode/CodeMode/Unity 版本 | Time、IsCompiling、IsPlaying、IsPlayingOrWillChangePlaymode、CodeMode、UnityVersion |
HostState | 额外返回可用命令清单 | AvailableCommands |
Compile | 触发一次脚本编译 | DurationMs |
Refresh | 刷新资源数据库 | — |
RegenProject | 重新生成 VS/Rider 工程文件 | — |
EnterPlay/ExitPlay | 进入 / 退出 PlayMode | IsPlaying |
Reload | 热重载(要求已在 PlayMode) | — |
命令发现
不要手工背诵全部命令名。两条路径:
- 运行时用
HostState的AvailableCommands字段拿当前宿主支持的完整命令列表; - 静态用
rg在 Proto 目录检索命令消息:
rg -n "^message .*Request|^message (Ping|HostState|Compile|Refresh|RegenProject|EnterPlay|ExitPlay|Reload)\b" ./Packages/cn.etetet.unitybridge/Proto需要行为细节时再按需打开对应的单个 handler(Scripts/Editor/Share/下每个命令一个UnityBridgeXxxHandler.cs),不要整包读取。
推荐执行顺序
skill 文档给出的状态变化类命令有严格的先后关系:
| 目标 | 顺序 |
|---|---|
| 编译 | HostState→Compile |
| 刷新 | HostState→Refresh |
| 重建工程文件 | HostState→RegenProject |
| 进入 PlayMode | 确认IsCompiling == false且IsPlayingOrWillChangePlaymode == false→EnterPlay |
| 热重载 | 确认IsPlaying == true→Reload |
| 退出 PlayMode | HostState→ExitPlay |
状态检查:Ping 与 HostState 解读
Ping是每次操作前的必做动作,一个典型响应(对照 UnityBridge_C_11100.proto):
{"_t":"PingResponse","RpcId":1,"Error":0,"Time":1715000000000, "IsCompiling":false,"IsPlaying":false,"IsPlayingOrWillChangePlaymode":false, "CodeMode":"Code","UnityVersion":"2022.3.0f1"}各字段含义:
Error:0 表示成功,非 0 表示失败;IsCompiling:Unity 是否正在编译脚本;IsPlaying:当前是否处于 PlayMode;IsPlayingOrWillChangePlaymode:是否处于 PlayMode 或正在切换 PlayMode(EnterPlay的前置检查依据);CodeMode:当前代码模式(从Resources/GlobalConfig的CodeMode字段反射读取,见 UnityBridgeEditorStatus.cs);UnityVersion:编辑器版本。
HostState在此基础上额外携带AvailableCommands,适合在任务开始前一次性确认宿主能力与命令拼写。
事件驱动的 Unity Editor 宿主
轮询循环
Editor 侧宿主UnityBridgeEditorHost使用[InitializeOnLoad]在编辑器启动时挂载到EditorApplication.update,以0.2 秒为周期轮询(UnityBridgeEditorHost.cs):
- 先尝试
UnityBridgeDeferredRuntime.TryPump泵送已挂起的 deferred 命令; - 若无挂起命令且距上次轮询超过 0.2 秒,则取一条新请求;
ProcessOneRequestAsync中先TryTakeNextRequest(把requests/中排序后的第一个文件原子移动到processing/),再执行HandleRequest;- 执行结果若为普通响应,写回
responses/并删除 processing 文件;若为 deferred 响应则交由 Deferred 运行时接管。
服务端超时
HandleRequest会根据命令类型给出服务端侧默认超时(UnityBridgeEditorHost.cs):
| 命令 | 服务端默认超时 |
|---|---|
Compile | 180000 ms |
Refresh/RegenProject/EnterPlay/ExitPlay | 60000 ms |
| 其他命令 | 10000 ms |
若请求在processing/中停留超过TimeoutMs,宿主直接返回unity bridge request timeout。
幂等缓存
每次请求携带IdempotencyKey,宿主在执行成功后把响应写入state/idempotency/{sha256(key)}.json;后续相同幂等键的请求直接命中缓存返回(并替换为新 RpcId),避免EnterPlay、Compile这类命令被重复执行(见 UnityBridgeEditorHost.cs 与 UnityBridgeStorage.cs)。
命令分发与 Handler 体系
Dispatcher 注册机制
UnityBridgeEditorDispatcher在静态构造时通过TypeCache.GetTypesDerivedFrom<IUnityBridgeHandler>()反射收集所有 handler 实现,校验每个 handler 的RequestType/ResponseType合法且无重复后建立“请求类型 → handler”映射(UnityBridgeEditorDispatcher.cs)。扩展桥接命令只需新增一个继承AUnityBridgeHandler<TRequest, TResponse>的 handler 类,无需修改分发器。
命令 JSON 的_t判别
Dispatcher 的TryNormalizeCommandJson解析规则(UnityBridgeEditorDispatcher.cs):
- 必须包含
_t字段(支持字符串或 BSON 多态数组,取最后一个非空值); - 旧的
CommandType/Payload封装格式已废弃,会直接报错提示改用_t; - 命令名可以是短名(如
Ping)或全名(ET.Ping),分发器自动归一化为全名后反序列化。
错误码定义
错误码集中在 ErrorCode.cs:
| 常量 | 值(基于PackageType.UnityBridge推导) | 含义 |
|---|---|---|
Success | 0 | 成功 |
InvalidCommandLine | 200000000 + 包号×1000 + 1 | 命令行/命令 JSON 非法 |
Timeout | 200000000 + 包号×1000 + 2 | 等待超时 |
NotInPlayMode | 200000000 + 包号×1000 + 3 | 不在 PlayMode(ExitPlay/Reload前置不满足) |
AlreadyInPlayMode | 200000000 + 包号×1000 + 4 | 已处于 PlayMode(重复EnterPlay) |
Compiling | 200000000 + 包号×1000 + 5 | Unity 正在编译,暂不能开始新的延迟命令 |
HandlerFail | 100000000 + 包号×1000 + 1 | handler 执行失败 |
CLI 侧读到Error == 0时以退出码 0 结束,否则以退出码 1 结束(Program.cs),并原样打印响应 JSON 供调用方解读。
Deferred 命令:长时操作的实现原理
Compile、Refresh、EnterPlay、AssetImportRequest等命令会改变 Unity 编辑器状态或耗时较长,被设计为deferred(延迟)命令。其机制分为两层:
Handler 层:AUnityBridgeDeferredHandler
AUnityBridgeDeferredHandler<TRequest, TResponse>(AUnityBridgeDeferredHandler.cs)在首次Handle时:
- 调用
UnityBridgeDeferredRuntime.TryCreatePendingCurrent把命令快照(含 RpcId、幂等键、超时、开始时间)写入state/pending-command.json; - 以
UnityBridgeDeferredContext.CreateStart()执行一次Run,此时 handler 内通过deferred.Started<TResponse>()主动抛出UnityBridgeDeferredStartedException; - 分发器捕获该异常后返回
UnityBridgeDeferredResponse(UnityBridgeCommands.cs),宿主识别IsDeferredResponse后不写响应,直接返回等待后续泵送。
运行时层:轮询泵送与恢复
UnityBridgeDeferredRuntime.TryPump(UnityBridgeDeferredRuntime.cs)在每个 Editor update 帧先于普通请求执行:
- 读到
pending-command.json后,反序列化命令、查找 handler,调用deferredHandler.Deferred(command, startedAt); - handler 内部用
deferred.NotReady<TResponse>()抛出UnityBridgeDeferredNotReadyException表示“还没就绪”,运行时收到null响应则什么都不做,下一帧再试; - 条件满足(如编译完成、PlayMode 切换完成)后 handler 返回正式响应,运行时通过
Complete写缓存、写响应、清理 processing 与 pending 状态文件; - 若
now - StartedAt > TimeoutMs,直接以Timeout错误码完成。
这就是为什么 skill 文档反复强调:deferred 命令必须等待最终响应,不能看到请求被接收就结束。CLI 端的 185 秒自动延长等待正是为此设计。
AI 操作 Unity 的任务路由
来自 et-unitybridge-ai-ops.md 的任务路由表,是 AI 使用桥接包的最高频场景索引:
| 目标 | 优先命令族 | 常见前置 |
|---|---|---|
| 状态 / 连通性 | Ping、HostState、EditorGetStateRequest | 无 |
| 编译 / 刷新 | Compile、Refresh、RegenProject、AssetRefreshRequest、AssetImportRequest | IsCompiling == false |
| PlayMode / 热重载 | EnterPlay、ExitPlay、Reload、EditorPauseRequest | 检查IsPlaying/IsPlayingOrWillChangePlaymode |
| 资源 | AssetSearchRequest、AssetFindRequest、AssetLoadRequest、AssetReadTextRequest、AssetGetPathRequest | 先限定 filter/path/count |
| 场景 | SceneGetHierarchyRequest、SceneGetActiveRequest、SceneLoadRequest、SceneSaveRequest、SceneNewRequest | 写操作前确认当前场景 |
| 选择集 | SelectionGetRequest、SelectionSetRequest、SelectionAddRequest、SelectionRemoveRequest、SelectionClearRequest | 先读当前 selection |
| 对象 / Transform | GameObject*Request、Transform*Request | 先Find/GetInfo/Get |
| Inspector | InspectorGet*Request、InspectorSet*Request、InspectorAddComponentRequest、InspectorRemoveComponentRequest | 先读组件和属性名 |
| Prefab | PrefabInstantiateRequest、PrefabSaveRequest、PrefabApplyRequest、PrefabGet*Request、PrefabUnpackRequest | 先确认 asset path / instance |
| 截图 / GameView | ScreenshotCaptureRequest、GameView*Request | 先读分辨率 |
| 测试 | UnityTestRunRequest | 用精确正则 |
| 批量 | BatchExecuteRequest | 先单步验证 |
核心操作模式
读状态(先读后写):
dotnet ./Bin/ET.UnityBridge.dll '{"_t":"HostState"}'只总结关键字段:Error、Message、IsCompiling、IsPlaying、IsPlayingOrWillChangePlaymode、所需命令是否存在。
执行 deferred 命令:
dotnet ./Bin/ET.UnityBridge.dll '{"_t":"Refresh"}'若返回unity is compiling,先轮询Ping直到IsCompiling == false再重试。
查资源(小范围限定):
dotnet ./Bin/ET.UnityBridge.dll '{"_t":"AssetFindRequest","Filter":"t:Prefab","MaxResults":10}'读场景层级:
dotnet ./Bin/ET.UnityBridge.dll '{"_t":"SceneGetHierarchyRequest","Depth":2,"IncludeInactive":false}'写操作后用GameObjectGetInfoRequest或TransformGetRequest验证结果,不能只相信命令返回成功。
读 Inspector:
dotnet ./Bin/ET.UnityBridge.dll '{"_t":"InspectorGetComponentsRequest","Path":"<HierarchyPath>"}'先读组件列表和属性名,再执行 set 命令,不要猜测SerializedProperty路径。
跑 Editor 测试(精确正则,避免全量):
dotnet ./Bin/ET.UnityBridge.dll '{"_t":"UnityTestRunRequest","Name":"^Unitybridge_DeferredHandlerRunContext_Test$"}'判定标准:Error == 0、Matched > 0、Failed == 0。
输出解读与常见错误
响应通用规则
Error == 0表示成功,非 0 表示失败,需结合Message判断原因;CompileResponse额外包含DurationMs(编译耗时);EnterPlayResponse/ExitPlayResponse额外包含IsPlaying;PingResponse/HostStateResponse额外包含Time/IsCompiling/IsPlaying/IsPlayingOrWillChangePlaymode/CodeMode/UnityVersion/AvailableCommands。
常见错误排查表
| 错误信息 | 含义与处理 |
|---|---|
wait unity bridge response timeout | Unity 未打开、项目未加载完、桥接根目录不一致,或 Editor 未处理请求;先检查ET_UNITY_BRIDGE_ROOT/--root是否两端一致 |
unity is compiling | Unity 正在编译,暂时不能开始新的延迟命令;轮询Ping等待编译结束 |
unity already in playmode or changing playmode | 已处于 PlayMode,不能重复执行EnterPlay |
unity not in playmode | ExitPlay/Reload的前置条件不满足,需先EnterPlay |
handler is missing | 命令名拼写错误或宿主版本不支持;先用HostState确认AvailableCommands |
execute menu item failed | 菜单路径不存在,或 Unity 当前状态不允许执行 |
省 Token 操作规范
skill 文档明确要求 AI 端遵循“最小读取”原则:
- 不要完整读取所有 proto、handler 或
AvailableCommands; - 先用
HostState或rg发现命令名,再只打开相关 proto 小片段和对应 handler; - 先执行最小读命令确认目标,再做写操作;批量操作前先验证 1 个样本;
- 不要把完整 JSON 响应贴给用户,只总结
Error、Message和关键字段; - 优先 UnityBridge 命令而非 GUI 点击,除非命令缺失或用户明确要求。
测试与验证
本包在 Scripts/Editor/Test 目录提供了覆盖各命令族协议消息与 handler 行为的 Editor 测试,例如Unitybridge_DeferredHandlerRunContext_Test.cs(deferred 上下文)、Unitybridge_GameViewSetResolutionHandlerInvalidSize_Test.cs(非法参数)、Unitybridge_ExitPlayModeHandlerNotInPlayModeError_Test.cs(错误前置条件)等,可作为命令行为与错误码语义的权威参考。测试辅助类 UnityBridgeHandlerTestSupport.cs 与 UnityBridgeProtocolTestSupport.cs 用于构造请求与断言响应。
更多参考
- skill 入口文档:skills/et-unitybridge/SKILL.md
- CLI 传输、等待、返回值解读:references/et-unitybridge-cli.md
- AI 操作 Unity 的任务路由与操作模式:references/et-unitybridge-ai-ops.md
- 命令行入口实现:DotNet~/Program.cs
- 协议定义:Proto/UnityBridge_C_11100.proto、Proto/UnityBridge_C_11400.proto
掌握以上内容后,你即可用一句dotnet ./Bin/ET.UnityBridge.dll '{"_t":"Ping"}'建立与 Unity Editor 的桥接,并沿任务路由表逐步完成从状态查询到资源、场景、PlayMode、测试的完整 AI 自动化操作闭环。
【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考