Appearance
精模云配协议与缓存设计
全城建筑精模量太大,不可能全装进车机。这套方案的核心是:Android 按视野 1000m 以内每秒筛一次「该显示哪些楼」发给 Unity,Unity 用 Show 字典 + Hide FIFO 两级缓存复用对象,要不要销毁重建完全由 versionName 一个字段说了算。
下发节奏1000m · 1s
Hide 缓存FIFO × 16
首批地标11 座 / 3 城
背景
6.0 V5 版本要做车道级建筑物精模云配:产品在 Nova 资源平台上传和更新建筑模型资源(同一个建筑 id 可能发新版资源),车机侧只展示屏幕附近的精模。摆在前面的约束有两条:全城模型量巨大,必须按位置筛选下发;资源会更新,车机必须及时换到新版,同时不能因为频繁重建把帧率搞垮。
我负责 Unity 侧的协议接入和显示集合管理。跨部门约定好 proto 之后,先在「SR 自定义 proto 文件版本更新记录」表里登记,再进开发——这个表后来成了协议联调期的单一事实来源。
问题
- 数据量与性能的矛盾:全城建筑不可能全量下发,也不可能全量加载,必须有一套「按视野筛选 + 对象复用」的机制。
- 资源更新的正确性:Nova 侧同一个建筑 id 会发新版资源,Unity 必须可靠地识别「资源变了」,不能显示旧模型,也不能没变化就傻重建。
- 跨端协议的脆弱性:自定义 proto 走信号链路,多传字段、少传字段都不能把车机搞崩。
方案
端到端链路
mermaid
flowchart LR
subgraph Nova["Nova 资源平台"]
UP["产品上传/更新<br>建筑模型资源"]
end
subgraph Android["Android 资源管理"]
IDX["整城建筑<br>资源索引"]
FLT["按视野 1000m 以内<br>约每秒筛选"]
end
subgraph Unity["Unity 一张图"]
RPT["约每秒上报<br>屏幕位置/视野"]
LYR["LDLayerLandmark<br>Show/Hide 两级缓存"]
end
UP --> IDX --> FLT
RPT --> FLT
FLT -->|"信号 1215<br>PrecisionMoldList"| LYR- Unity → Android(约 1s 一次):上报当前屏幕位置和视野,作为筛选依据。
- Android 侧:下载并维护整城建筑资源索引(和 Nova 对齐),按 Unity 上报的位置筛出 1000m 以内的建筑集合。
- Android → Unity:下发
PrecisionMoldList,每个PrecisionMoldInfo携带 id、经纬度、本地资源路径、版本号等。
协议设计
对外信号 ID 定为 set_PrecisionMold(1215)(3D 侧接收用),Android 发送侧对应 SIGNAL_3D_PRECISION_MOLD = 105010。proto 定义落在 logic_msg.proto:
protobuf
message PrecisionMoldList {
int32 cityId = 1; // 城市id
string cityName = 2; // 城市名称
repeated PrecisionMoldInfo precisionMoldInfo = 3; // 精模数据列表
}
message PrecisionMoldInfo {
string id = 1; // 唯一标志符
LonLat points = 2; // 模型中心经纬度坐标
string localResPath = 3; // 本地资源绝对路径,到文件夹
LonLat pointsLeftBottom = 4; // 左下角经纬度
LonLat pointsRightTop = 5; // 右上角经纬度
int64 tileId = 6; // tileid
string userData = 7; // 备用
string versionName = 8; // 精模资源版本号
string rawHash = 9; // 资源文件标志符
}几个字段设计值得展开:
- 城市 ID + 名称:城市级分组,跨城切换时整组管理。
- 中心点 + 左下/右上包围盒:既给锚点定位,也给遮挡和剔除留包围盒,不用 Unity 再算一遍。
- tileId:对齐地图瓦片体系,为按瓦片增量加载留口。
- versionName(12.26 新增):整个方案最重要的字段,见下文。
- rawHash(12.26 新增):精模资源文件标志符,只与上传的模型压缩包有关,不受经纬度等参数影响——用来校验资源文件本身,支撑增量更新判断。
版本驱动的对象管理
mermaid
flowchart TD
A["收到下发列表<br>约 1s 一次"] --> B{"id 在 Show?"}
B -->|是| C{"versionName 变了?"}
C -->|变了| D["Destroy 旧对象<br>重建新对象"]
C -->|没变| E["仅更新位置"]
B -->|否| F{"Hide 里有同 id?"}
F -->|"有,版本没变"| G["复用旧对象<br>并更新位置"]
F -->|"有,版本变了"| D
F -->|没有| H["直接新建"]
I["Show 中不在本次<br>列表的 id"] --> J["HideLandmark<br>SetActive(false) 放入 Hide"]
J --> K{"Hide 超过容量 16?"}
K -->|是| L["淘汰最老<br>并 Destroy"]关键实现
手写解析器注册进分发框架。 工程里 proto 生成走的是 Unity 编辑器 Tools → Protocol → ProtoParser 流程,代码逻辑分 proto 文件、xml 文件和一个 C# 脚本三部分。但 XpPrecisionMoldListParser 这个类是手写的,注册进 X3D.Tools.Service.SystemServiceProxy 的 AddProtocolHandle,按消息 ID 1215 接收。SystemServiceProxy 里还挂着汽车场景、导航报警、NGP 许可证等一串既有信号的 handler,精模只是其中一个新条目——接入成本被分发框架压到了最低。接收侧 OnHandleHighQualityMode 做了完整判空:msg 是 PrecisionMoldList 类型、列表非空、precisionMoldInfo 计数大于 0,才 Broadcast 给业务层,否则只打提示日志。
versionName 的业务定义是整套正确性的根基。 id 是稳定的「建筑逻辑标识」,versionName 是 Nova 侧的「资源整体版本」。和对接方约死的约束是:只要任何影响渲染/加载结果的资源字段变了(模型、材质、贴图、AB、cfg、catalog 映射等),versionName 必须变。于是 Unity 侧策略变得极简单:versionName 变 = 资源变 = 必须 Destroy + 重建;没变就只更新位置,性能最优。所有复杂度被压缩进一个字符串比较。
两级缓存复用对象。 Show 字典管正在显示的建筑 id → instance;Hide 字典是 FIFO,容量 16,存最近隐藏的可复用对象,满了淘汰最老并 Destroy,把内存压住。每秒收到新列表时:在 Show 的比版本,不在 Show 的走 ShowLandmark(先查 Hide 复用),Show 里多出来的走 HideLandmark(SetActive(false) 进 Hide)。建筑进出视野是高频事件,复用命中时零创建零销毁。
异常兜底防 crash。 Protobuf 多传未知字段:忽略,不崩。少传/空字段:解析器和 Unity 侧都做了 null 保护,不会 NRE,但会记录告警日志,方便定位是哪一端漏传。这条在联调期实际抓到过对面字段漏传的问题。
日夜间四通道资源规范。 精模有日间(浅色模式)和夜间(深色模式)两套效果,夜间每座楼固定四张图:Diffuse+transparent.tga、AO.tga、Emit.exr、Lightmap+0Env.exr(增加采样),其他通道不输出。规范定死之后,视觉走「精模配置参数存档」表上传资源、用 adb 暗号命令上车调试效果,资源维护和效果验收都有固定入口。
指标
| 指标 | 数值 |
|---|---|
| 下发筛选半径 / 节奏 | 视野 1000m 以内,约每秒 1 次 |
| Hide 缓存容量 | FIFO 16,满则淘汰最老并 Destroy |
| 协议字段 | 2 个 message,PrecisionMoldInfo 9 个字段(含 12.26 新增 versionName/rawHash) |
| 首批地标 | 11 座,北京 4(CCTV总部、中国尊、鸟巢、水立方)、上海 4(东方明珠、上海中心、环球金融中心、金茂)、广州 3(广州塔、小鹏总部、猎德大桥) |
复盘
- 把「要不要重建」压缩成一个字段的比较,是这类资源云配方案最划算的设计。 versionName 的约束写在跨部门协议里,Unity 侧逻辑就只剩查表和比较,没有中间态。
- 缓存容量要拍死一个数。 Hide FIFO 定 16 是权衡结果:再小复用命中率掉、再大内存压不住。定了数,淘汰行为才可预期。
- 解析器手写不是负担是自由度。 ProtoParser 生成流程覆盖不了的自定义入口(注册进 SystemServiceProxy、判空后 Broadcast),手写一个 Parser 类就接进了既有分发框架,比改框架本身便宜得多。
- 联调期的登记表救过命。 proto 变更登记、版本号定义、资源上传存档三张表各管一段,出问题先查表再查代码,定位速度快一个量级。这套链路的信号分发基础,和 Native-Unity 信号链路 是同一套框架;渲染侧的锚点与比例尺细节见 高德车道级精模渲染。