UI桥接:JSON 能传消息,不等于它是一份协议
自动关联目录:UI桥接:JSON 能传消息,不等于它是一份协议
核心判断:把自定义 UI 与脚本运行时直接连通能显著降低开发成本,但原始 JSON 字符串、全局函数和一条无界消息队列只解决了“能传”,没有解决版本、所有权、流控和销毁一致性。
一、实际方案为什么有价值
创作者 UI 需要创建视图、更新状态、调用脚本并把交互送回运行时。工程中的桥接层支持视图创建、状态设置、消息投递和回调绑定;入站消息先排队,到帧更新再进入脚本,避免在 UI 回调栈里重入 VM。个别高频能力还携带序号,能够拒绝旧结果。
这条直接通路减少了中间代理和重复序列化,适合工具快速扩展。问题是通用入口仍接受原始字符串,部分路由依赖字符串特征判断,消息没有统一版本、类型白名单、大小限制和 View generation。
二、故障不是“JSON 解析失败”这么简单
- 旧视图销毁后,排队消息可能送到同名新视图。
- 大对象或深层数组能占满单帧解析时间与脚本堆。
- 未知字段被静默忽略后,两端对同一操作产生不同理解。
- 重复绑定若没有先释放旧注册引用和 UE Root,会积累回调或重复投递。
- 任意脚本全局调用把桥接层变成了第二套 Capability 漏洞面。
三、目标协议:先验证信封,再解码载荷
struct UiEnvelope {
ProtocolVersion version;
ViewId view;
Generation viewGeneration;
Sequence sequence;
MessageType type;
JsonValue payload;
};
BridgeResult Receive(Bytes bytes) {
Require(bytes.Size() <= Limits.maxMessageBytes);
UiEnvelope msg = ParseWithDepthLimit(bytes, Limits.maxJsonDepth);
Require(SchemaRegistry.Supports(msg.version, msg.type));
Require(IsCurrent(msg.view, msg.viewGeneration));
Require(QueueFor(msg.view).TryReserve(CostOf(msg)));
return QueueFor(msg.view).Push(SchemaRegistry.Decode(msg));
}消息类型显式声明方向、权限、Schema、最大大小、是否可合并和响应类型。视图销毁先提升 generation、关闭队列并释放脚本注册引用,再移除 Root 和 UE 对象;迟到消息只能得到稳定的 ViewExpired。
四、UE 线程与帧边界
Slate/UMG 更新和脚本回调通常仍应在 Game Thread,但桥接层可以在入队前做长度、JSON 深度和信封校验。状态类消息按 key 合并,输入类消息保序,日志类消息按预算丢弃;单帧 drain 使用时间预算而不是“清空为止”。
五、验证与反思
需要覆盖版本不匹配、未知类型、超大/超深 JSON、乱序、重复 sequence、销毁后迟到、重绑定和队列过载。观察指标至少包括每视图队列年龄、解析耗时、丢弃原因与未释放注册引用数。
复盘结论是:跨语言桥接一旦承载用户输入,就已经是一份网络协议,只是两端恰好在同一进程。
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/46dbe1f6.html
更新于:2026年10月14日