碰撞表示:可共享的是形状数据,不是运行时 Body
自动关联目录:碰撞表示:可共享的是形状数据,不是运行时 Body
核心判断:编辑形状、Cooked 碰撞数据和物理场景中的 Body 是三种生命周期。前两者可以不可变共享,Body 必须按实例持有并以真实 shape/pose 校验;只比较
BodyInstance里的目标 Scale,会把“请求值正确”误当成“物理场景已正确”。
一、业务冲突:视觉资产可以晚到,碰撞不能晚到
可编辑场景中的物体既可能是规则 Box、Sphere、Capsule,也可能来自凸包分解;它们还会在运行时缩放、镜像、组合或拆分。客户端希望相同资源复用 Cook 结果,DS 又不能依赖渲染 Mesh 才获得碰撞。若直接把 UStaticMeshComponent 当作唯一物理真相,会同时遇到三个问题:渲染资产异步时 Body 不存在,重复网格反复 Cook,组合对象的子 Mesh 与根对象重复参与碰撞。
工程中的实际故障更隐蔽:运行时属性已经记录了新 Scale,BodyInstance 查询也返回该 Scale,但底层物理 shape 的几何或局部姿势仍然是旧值。只做 Scale3D == RequestedScale 的短路会跳过修复,射线查询、接触和质量计算继续使用旧形状。换言之,组件状态只能证明曾经提出过请求,不能证明物理后端已经兑现。
这也是碰撞方案不能只讨论“简单碰撞还是复杂碰撞”的原因。业务真正需要的是一条即使视觉缺席、对象合并、负缩放和 Body 重建都发生,查询与模拟仍有明确真相来源的链路。
二、实际实现:不可变源、共享 Cook、实例 Body 三层分离
代码先把 UE 聚合几何复制为不含 UObject、VM 指针和 Cook 缓存的值类型快照。每个 shape 保存局部 Transform、尺寸或凸点、稳定 Hash;快照带 Generation 和整体 Hash,并拒绝 NaN、非正尺寸、退化四元数及顶点不足的凸体。这样 DS 可以只从发布数据恢复碰撞,不必加载 Render Asset。
第二层按 World 维护共享 Cook 注册表。Key 不只包含顶点,还包含有序 shape、局部 Transform、物理材质、BuildScale、斜坡标记和项目物理设置;Hash 命中后仍做精确比较,避免哈希碰撞错误复用。共享对象创建后不可变,并提前生成普通与镜像凸体,使符号 Scale 变化不必再次 Cook。不支持的形状、非法数据或 Cook 失败会退回实例私有路径,而不是污染共享缓存。
第三层由每个对象自己的碰撞代理持有运行时 Body。代理可以先于视觉 Mesh 成为 Actor Root,视觉到达后只附着到代理。组合对象把子 Mesh 设为无碰撞,在根代理上按槽位重建 Compound;缺失或非法主槽位有显式回退。Scale 更新优先验证底层 shape 类型、尺寸、局部 pose 和镜像方向,验证通过才原地更新并重算质量,否则销毁并重建 Body,同时推进 Body/Geometry Generation。
flowchart LR
E[可编辑碰撞描述] -->|校验/冻结| S[不可变 Shape 快照]
S -->|平台与后端 Cook| C[共享 Cooked Artifact]
C -->|每实例物化| B[运行时 Body + Generation]
B --> Q[Query / Contact / Simulation]
R[视觉 Mesh] -.仅表现附着.-> B
V[真实 shape/pose 校验] --> B三、值得保留:碰撞已经从渲染附属物变成独立制品
值类型快照是最关键的正确方向。它切断了碰撞与 UObject 生命周期、脚本 VM 和渲染加载的绑定,也让输入校验、Hash、序列化和 DS 恢复有了统一入口。共享注册表同时使用完整 Cook 输入和精确比较,避免“同一 Mesh 路径”这种过弱身份。
共享边界也基本正确:共享的是不可变 BodySetup/Cooked Geometry,实例仍拥有自己的 BodyInstance、Transform、速度和休眠状态。代理被 UPROPERTY 持有,因此即使注册表清理,共享数据也不会早于使用者释放。对普通与镜像凸体同时准备,则把负 Scale 从动态 Cook 问题降成实例选择问题。
测试没有停在“Scale 字段改了”。它会故意制造底层 shape 尺寸或局部 pose 过期、而上层仍报告目标 Scale 正确的状态,要求系统检测并重建;同时覆盖缺失 Body、近零 Scale、局部 Transform、镜像和恢复后的查询。这类测试验证的是物理事实,而不是 API 调用次数,值得延续。
四、结构性缺陷:运行时补救仍然承担了太多发布期责任
当前存在多种形状表示:编辑布局、规范快照、不可变源、UE 聚合几何和运行时 shape。它们各有必要,但字段转换分散后,新增 shape 属性很容易只进入部分 Hash 或只在某条物化路径生效。共享 Key 中若混入进程内对象指针,也只能作为本次运行的缓存身份,不能成为跨进程或持久化 Artifact 身份。
首次遇到凸体仍可能同步 Cook。共享只减少第二次成本,无法消除第一个对象造成的帧尖峰;DS 启动时批量物化尤其明显。更根本的问题是发布数据还没有把“源 Hash、Cook 参数、物理后端、目标平台、Cook 版本和 Payload 校验”固化成一份可验证制品,运行时仍要猜该复用还是重建。
组合碰撞也需要警惕。把所有子形状都塞进一个 Compound 可以减少 Component,但 shape 数、宽相位 AABB、接触对和重建范围并未消失。若拓扑变化就全量重建,编辑频繁或大组合物会把节省的 Actor 成本转移为 Body 重建尖峰。碰撞代理解决了所有权,不自动解决规模。
五、目标设计:让 Collision Artifact 成为跨端契约
目标制品应由构建期生成并签名,至少包含逻辑形状 ID、Schema、SourceHash、CookKey、物理后端与版本、目标平台、Bounds、简单形状和 Cooked Payload。客户端与 DS 使用同一逻辑描述,但按各自后端选择 Payload;找不到匹配 Payload 时只能使用声明过的简单回退或明确拒绝,不能静默等待视觉 Mesh。
运行时通过 BodyLease 持有 Artifact 与 Generation。任何 Scale、材质、合并拓扑或碰撞模式变更先生成 Candidate,再决定原地 Patch 还是重建;提交后读取底层 shape 形成 Observation,只有 Observation 与 Candidate 一致才发布新 Generation。这样属性系统、查询系统和网络同步看到的是一次原子提交,而不是中间态。
CommitResult CommitCollision(const Candidate& next, BodyLease& lease)
{
Validate(next.description);
ArtifactRef artifact = registry.Acquire(next.cookKey);
if (CanPatchInPlace(lease.body, artifact, next.transform) &&
LiveShapesMatch(lease.body, lease.committed)) {
PatchShapes(lease.body, next.transform);
RecomputeMassAndFilters(lease.body);
} else {
lease.body = RecreateBody(artifact, next.instanceState);
}
if (!LiveShapesMatch(lease.body, next))
return RollbackOrDisableCollision(lease);
return CommitGeneration(lease, next);
}六、决策规则:先问碰撞真相在哪,再问能否优化
| 场景 | 决策 | 必须验证 |
|---|---|---|
| DS 无视觉资源 | 从发布期碰撞制品物化;缺失时使用声明回退或拒绝 | 查询可用、制品版本、Bounds 与源 Hash |
| 大量相同凸体 | 共享不可变 Cooked Geometry,Body 按实例创建 | 首次 Cook、缓存命中、内存与 Body 数 |
| 规则简单形状 | 直接参数化创建,不为“统一”强制 Cook | shape 数、查询误差、更新成本 |
| 运行时正负缩放 | 校验真实 shape/pose 后原地 Patch;失败重建 | Body/Geometry Generation、质量、查询结果 |
| 组合或嵌套对象 | 子表现禁碰撞,根代理拥有 Compound;按脏槽位更新 | 重复碰撞、shape 数、局部重建范围 |
| Cook 版本或平台不匹配 | 不复用缓存,选择匹配 Artifact 或显式降级 | 后端、平台、参数、Payload 校验 |
这段实践最值得保留的不是某个缓存类,而是边界判断:Render Mesh 不是碰撞真相,BodySetup 不是实例状态,BodyInstance 的请求字段也不是底层物理事实。只有把源描述、Cooked Artifact、实例 Body 和真实 Observation 分开,碰撞才能同时支撑异步资产、DS、运行时编辑与规模化复用。