30Hz 插值里那些不明显的坑
AI 摘要
文章梳理了游戏开发中插值表现层的几个常见陷阱:旋转必须用 Slerp 避免大角度路径异常;传送类突变位置需由 Sim 层打标记跳过插值;alpha 超 1 时插值会变成外插引发抖动。核心结论是表现层不能自己判断状态,插值逻辑需严格依赖 Sim 层信号驱动。
“插值只在表现层”是条好规则,但它是必要条件不是充分条件。实际写的时候还有几个具体的坑。
旋转要用 Slerp 不用 Lerp
Vector3.Lerp 对位置没问题,但旋转用 Lerp 会在大角度时出问题——两个四元数之间线性插值,中间态不是合法旋转,速度不均匀。
Quaternion.Slerp(prev.Rotation, curr.Rotation, alpha);
// 不是 Quaternion.Lerp
更麻烦的是四元数的双覆盖问题:q 和 -q 表示同一个旋转,但插值路径完全相反。如果 prev 和 curr 点积为负,要先把 curr 取反:
if (Quaternion.Dot(prev, curr) < 0f) curr = new Quaternion(-curr.x, -curr.y, -curr.z, -curr.w);
不处理的话,角色转身时偶尔会绕远路转一整圈。
传送不能插值
闪现、击退、重生这类“位置突变”的帧,插值会让实体从旧位置“滑”到新位置,看起来像瞬移拖影。
解法是给状态加一个标记:
public readonly struct SimTransform
{
public readonly Vector2 Position;
public readonly bool Teleported;
}
Teleported == true 时直接跳到目标位置,不插值。这个标记由 Sim 层在发生传送的那个 tick 置位,表现层消费后清零。
关键是这个判断必须来自 Sim 层,不能让表现层自己比较距离阈值——阈值是个魔数,迟早在某次调参后失效。
alpha 可能大于 1
累加器写法下,如果一帧内一个 tick 都没跑(_accumulator < FixedDelta),alpha 是合法的。但如果渲染帧率低于 30Hz,或者 tick 被 MaxStepsPerFrame 截断了,_accumulator 会持续累积,alpha 可能超过 1。
超过 1 再插值就是外插(extrapolation),实体会冲到当前 tick 状态之外,然后下一帧又弹回来——产生抖动。
float alpha = Mathf.Clamp01(_accumulator / FixedDelta);
钳一下最省事。更严谨的做法是超过 1 时改用外插且限制最大外插时长,但对 30Hz 的 Sim 来说,钳制够用了。
待验证
还没处理的情况:实体在插值期间被销毁。目前是下一 tick 直接移除,视觉上会“啪”地消失。考虑加一个淡出,但那需要在表现层维护死亡实体的生命周期——又是一层状态,先记着。
这是一条笔记,不是成稿文章。它可能不完整、可能有错,也可能过段时间就被我推翻。 想看成体系的文章,去 Blog。