我曾经认为,Tween到达目标后瞬间移动到目标位置是无害的。然而,这并不是事实。

我一直在开发Unity动画工具,几乎有一年了,我有一段示例代码,似乎工作得很好。

这个示例包含535个环绕圆圈的平铺物体,每个物体都有一个小弧形运动 Tween,然后将 Tween 的控制权转交给下一个 Tween,循环往复。理论上讲,这应该是非常平滑的。

然而,它并不是。

每次 Tween 转交给下一个时,物体都会产生一个微小的抖动。关闭平滑效果时,它看起来像是一个突然的向后跳跃。启用弹簧平滑效果时,它变成了一个奇怪的软漂移。

同一个问题,完全不同的症状。

还有另外一个细节,我一直认为它是理所当然的:Tween必须在目标位置精确结束。如果它们不结束在目标位置,对象可能会在随机距离之外停留,并且这个距离会随着帧率的变化而变化。

所以,如果你告诉一个按钮移动到x=5和y=25,你会很失望看到它停留在x=5.03和y=25.001的位置。Tween通常不会在百分比完成达到100%时精确到达目标位置,尤其是在变速率的帧率下(像我们所有人一样)。这就是为什么明显的解决方案是让Tween在目标位置精确结束:

bool done = percentDone >= 1f;

if (done)
{
    inst.CurrentValue = inst.Target;
}
else
{
    float t = effect.Easing.Evaluate(percentDone);
    inst.CurrentValue = inst.StartValue + (t * (inst.Target - inst.StartValue));
}

基本上,如果 Tween 完成,直接将其移动到目标位置。

我认为这是一个合理的解决方案,甚至是安全的。

然而,这也隐藏了真正的问题。

实际的 bug 不是一个 bug,而是三个小的时间误差,只有在链式动画、圆形路径和平滑效果都存在时才会显现。

第一: 这个问题的根源是:当 Tween 到达目标位置时,实际上消耗了最后一帧的部分时间,但这意味着最后一帧几乎没有贡献任何实际运动。Tween已经到达目标位置,并且我强制它精确到达目标位置。因此,每个 Tween 都以死帧的部分贡献结束。

解决方案很简单:停止将完成视为一个单独的视觉案例。使用 Mathf.Clamp01 将百分比 clamped,允许同样的插值路径完成运动:

bool done = percentDone >= 1f;

float p = Mathf.Clamp01(percentDone);
float t = effect.Easing.Evaluate(p);

inst.CurrentValue = inst.StartValue + (t * (inst.Target - inst.StartValue));

如果 p 是 1,lerp 将自然地到达目标位置。没有特殊的跳跃要求。

第二: 手动转交抛弃了时间。

当效果 A 完成时,我的驱动循环将完成 A,yield,开始效果 B 的下一帧。

这意味着剩余的帧时间被丢弃。

效果 A 完成时,例如,60% 的帧时间。剩余的 40% 应该被传递给效果 B。但是,B 开始下一帧,从零开始。

这创造了一个保证的链式效果之间的缝隙。

解决方案是立即在同一帧内将剩余时间传递给下一个效果:

float residual = frameCurrentTime - crossTime;

if (residual > Epsilon)
{
    time.PreviousCurrentTime = crossTime;
    time.DeltaTime = residual;
    carrying = true;
    continue;
}

没有等待下一帧。没有丢弃的时间。

第三: 一旦两个效果可以共享一个帧,你就不能给他们两个整个帧。

这听起来现在很明显了,但它以前并不是这样。

效果 A 应该只接收到它完成时的帧时间的部分。效果 B 应该接收剩余的部分。如果两个效果都接收到整个帧时间,时间会被重复计算。这意味着处理两个帧同时,并且每个效果都操纵时间的感知。

这重复计算的时间创建了一个速度脉冲。然后弹簧平滑效果做了它应该做的事情:它将这个假的速度整合,并将其转化为一个可见的漂移。

所以,A 的最后一步必须被 clamped 到精确的交叉时间:

crossTime = startTime + duration;

time.CurrentTime = crossTime;
time.DeltaTime = crossTime - time.PreviousCurrentTime;

然后 B 只接收剩余的剩余时间。

A 消耗了帧的第一部分。B 消耗了第二部分。他们一起构成了一个帧。

想象一下。

最有趣的部分是,原始的跳跃在孤立情况下看起来是正确的。

一个单独的 Tween?没问题。它到达目标位置。没有可见的问题。正确的。

但是,一旦我将 Tween 链式起来,添加平滑效果,并且有数百个对象同时执行, “安全”的这个快捷方式开始污染整个动画流。

我对这个问题的教训是:将 Tween 到达目标位置时直接移动到目标位置并不是自动错误,但它可以隐藏时间 bug。如果动画系统支持链式动画、平滑效果、速度或手动转交,最后一帧仍然很重要。你不能简单地抛弃它,因为值足够接近。

这个 bug 现在已经被修复了。没有停顿,没有脉冲,没有跳跃,没有漂移。

这个 bug 来自于我在 Unity 动画资产 JuiceBox 上的工作中。有一个 免费版本 可用。

我不想把这篇文章变成一个特性列表的文章,所以我就留下了。主要是,我认为这个调试故事值得分享,因为这个 bug 正是在我写的代码中,因为我确信它不重要。