SplineMesh:控制点必须先转换到组件局部空间
自动关联目录:SplineMesh:控制点必须先转换到组件局部空间
核心判断:
USplineMeshComponent::SetStartAndEnd接收的是组件局部空间的位置与切线。把 World 坐标直接写进去,Actor 位于原点时可能看似正确,一旦宿主平移、旋转、缩放或重新挂接,就会重复或漏掉 Transform,碰撞也会跟着错。
一、业务冲突:轨迹和空气墙都由 Spline 生成,空间归属却不同
抛射轨迹需要按曲线生成可见段,动态边界需要沿控制点生成既有网格又有碰撞的墙体。两者都适合使用 SplineMesh,但源 Spline、目标组件和宿主 Actor 可能拥有不同 Transform;编辑器 Construction、运行时 Spawn 和网络复制又会在不同阶段重建组件。
二、实际实现:两条路径形成了直接对照
一条轨迹路径从 Spline 获取 Local 位置和 Local 切线,把动态 SplineMesh 挂到同一根组件,再注册并提交起终点。只要两者共享局部基准,这条链路是闭合的。
另一条边界路径在重建前先销毁旧 SplineMesh,这一点正确;随后却从 Spline 查询 World 位置和 World 切线,直接传给新组件的 SetStartAndEnd,组件也没有先建立与源 Spline 一致的 Attachment。它实际上依赖“新组件 Transform 恰好是单位矩阵”。
这种问题最隐蔽的地方是原点测试会通过:World 与 Local 数值相同。地图把 Actor 移走、父节点旋转或美术调整缩放后,错误才显现;如果 SplineMesh 开启碰撞,错误几何还会同时进入查询和物理接触,不只是画面偏移。
三、目标方案:先选目标空间,再只转换一次
最简单的规则是:若 Spline 与所有 SplineMesh 共用同一父 Transform,就统一查询 ESplineCoordinateSpace::Local。若两者不共父级,则通过 World 中转,明确转换到目标组件局部空间:
FSegmentGeometry ConvertSegment(
const USplineComponent& Source,
const FTransform& TargetWorld,
int32 StartIndex,
int32 EndIndex)
{
const FTransform SourceWorld = Source.GetComponentTransform();
const FVector P0s = Source.GetLocationAtSplinePoint(StartIndex, Local);
const FVector T0s = Source.GetTangentAtSplinePoint(StartIndex, Local);
const FVector P1s = Source.GetLocationAtSplinePoint(EndIndex, Local);
const FVector T1s = Source.GetTangentAtSplinePoint(EndIndex, Local);
return {
TargetWorld.InverseTransformPosition(SourceWorld.TransformPosition(P0s)),
TargetWorld.InverseTransformVector(SourceWorld.TransformVector(T0s)),
TargetWorld.InverseTransformPosition(SourceWorld.TransformPosition(P1s)),
TargetWorld.InverseTransformVector(SourceWorld.TransformVector(T1s))
};
}位置使用 Position 变换,切线使用 Vector 变换,不能让平移污染切线。非单位与负缩放会改变切线长度和朝向,必须在同一变换链中处理,不能只旋转方向再沿用原长度。
动态组件的推荐顺序是:创建组件、设置稳定 Attachment 与相对 Transform、批量写入网格/材质/碰撞和所有段参数,最后只注册一次;若注册后的运行时更新必须分步完成,再用延迟更新参数并统一 UpdateMesh。视觉和碰撞必须消费同一份 FSegmentGeometry,禁止各自重新查询不同空间的 Spline。
四、重建性能:正确销毁不等于适合高频修改
重建前 DestroyComponent 能避免旧段残留,是值得保留的做法。但若控制点在运行时频繁变化,每次销毁并 NewObject 会重复支付 UObject 分配、组件注册、SceneProxy 和 BodySetup/Physics State 更新。
更稳妥的是按段数维护组件池:复用已有段,只为新增段创建组件,多余段禁用或销毁;记录脏控制点区间,只更新受相邻点影响的段。碰撞更新可降频或在编辑完成时提交,避免拖动控制点时每个采样都重建物理状态。
五、验证与指标
测试不能只把 Actor 放在原点。至少覆盖平移、任意旋转、父级旋转、均匀/非均匀缩放、负缩放、运行时重新挂接和网络端重建。每个控制点都应验证:渲染段端点、碰撞命中点和 Spline World 位置在容差内一致。
性能侧记录 SegmentsCreated、SegmentsReused、DirtySegments、SceneProxyRebuildTime、PhysicsRebuildTime。如果只看 Draw Call,很容易忽略动态组件和碰撞重建已经把成本移回 Game Thread 与 Physics Thread。
六、反思
两条实际实现的差异说明,这不是 Spline API 难用,而是没有把目标空间写成接口契约。Transform 问题最怕“数值暂时相等”:一旦输入没有携带空间语义,原点上的成功只是把错误推迟到内容变化以后。