这是《数字展厅 Unity 项目开发实战分享》系列的第三篇,聚焦展厅导视系统里的 MQTT 消息设计:从主题划分与注册上线握手,到实际运行中暴露的解析、去重、断线与资源落地问题。文中涉及的后台地址、服务凭据、设备 IP 一概不展开。
一体机要求"断网能用、联网就能同步":功能全在本地跑,后台只负责内容更新推送与在线状态接收。这种"少量、可靠、双向、面向设备"的通信,MQTT 比轮询 HTTP 合适得多——设备侧一个长连接,服务端按设备定向下发,正契合"一个后台管多台设备"的模型。
主题设计刻意极简,一共三类:
| 主题 | 方向 | 载荷 | 说明 |
|---|---|---|---|
| 设备 ID(MAC 小写) | 下行 | UpdateSignal / UpdatePoints | 设备专属主题,后台按设备定向推送 |
status | 上行 | {id, status:"online", name} | 上线通告 |
status | 下行 | status == "gistered" | 注册通过回执 |
laststatus | 上行 | {id, off:"1"} | 离线通告 |
握手流程:未注册设备启动后订阅自己的设备主题,等服务器下发注册通过回执,再把设备名与注册态写进 PlayerPrefs;已注册设备直接向 status 主题发布上线消息,再订阅自身主题。程序退出时向 laststatus 发一条离线消息并断开连接,让后台的在线状态不至于"假在线"。
下行消息只有两种结构,字段少、语义单一,这本身就降低了联调成本:
| 消息类型 | 字段 | 作用 |
|---|---|---|
UpdateSignal | table = "stb" | 待机资源更新,携带图片/视频地址列表 |
btm | 底部滚动字幕文本 | |
UpdatePoints | table | 需要更新的点位标识 |
distance / angle | 该点位的距离与机械臂指向角度 | |
res = "1" | 表示附带资源,需触发下载 |
问题:一条脏数据拖垮整批消息。
早期解析逻辑是一个大 try,只要一条 JSON 解析失败,后面的消息全部丢失。改成按消息类型分别 try-catch(UpdateSignal 与 UpdatePoints 各自独立),单条异常只丢弃自己,其余照常处理。
问题:后台连续推送导致下载堆积。
后台更新一个模块可能连发多条消息,直连下载会重复下载、并发打满带宽。三层防护:HashSet 去重(同地址只入队一次)、0.8s 去抖(一批消息合并成一次任务)、_isDownloading 锁(防两次下载重叠)。
问题:待机资源和点位资源互相覆盖。
两类资源共用一个去抖协程时,后到的消息会把前一类任务的计时重置,出现"待机素材更新了、点位图没更新"。改成两条独立的去抖通道,各自维护计时与任务。
问题:下载完成回调不在主线程。
网络回调里直接操作 UI 是典型的偶发崩溃源。统一把落地操作(改界面、写文件、触发下一步)经主线程调度器转回主线程执行。
问题:断线与重连策略。
连接参数加 60s KeepAlive 维持长连接,断线后延时 5 秒重连,避免服务端异常时疯狂重试;会话采用不保留模式(CleanSession),设备重启后通过注册/上线流程重新对齐状态——对"状态以服务端为准"的业务,不保留会话反而更干净。
问题:资源落地的原子性。
下载目录与实际使用目录分开:先下到 Temp,确认完整后再移动到 New,移动时先清目标再搬运。中途掉电最坏只是 Temp 里多个临时文件,不污染正在展示的资源;程序退出时在 OnDestroy 里再补一次移动。
问题:本地数据写回时机。
点位信息更新后写回 PointsInfo.json 目前依赖离场/退出时触发,运行中途掉电可能丢失这一批更新。这是当前工程已知的隐患,改进方向是"下载完成即写回",把数据落盘与进程生命周期解耦。
顺带一提流量与画质上的优化:图片下载后统一做质量压缩与方向修正,单张体积从 13MB 级别降到 3~5MB 级别,同时按 EXIF 修正方向,避免现场出现"图是倒的";下载设 30s 超时并失败重试 1 次,兼顾弱网与不卡死。
回头看这一方向,能沉淀成方法的其实就一条:消息设计要假设它会脏、会重、会断,去重、去抖、分通道、可重入。都不是什么高深技术,但都是被现场问题反复教育出来的。
(内容由AI生成,仅供参考)