为什么要使用基于时间的动画以及如何实现它【翻译】
Published:
原文地址: http://blog.sklambert.com/using-time-based-animation-implement
当我写这篇文章的时候,我并不知道使用基于时间的动画的重要性。直到后来,有人告诉我我所使用的基于帧的动画会导致一些问题。
我决定要学会这个基于时间的动画以及它为何如此重要。我在其他的游戏中见过几次,但是我还没有理解。 不用说了,我希望尽快的补救我的知识,赶紧学习为什么基于时间的动画是制作游戏的唯一方式。希望这篇文章将帮助你了解为什么基于帧的动画会出现一些问题以及基于时间的动画如何解决上述问题。
基于帧的动画
基于帧的动画就是使用帧速率来更新动画。举个例子,如果浏览器是 60FPS(即 60帧/秒),那么每秒钟,游戏会更新 60 次。这意味着,如果每次更新,方块都移动 2px,那么 1 秒后,方块移动了 120px。
大多数的情况下,基于帧的动画运行得相当好。唯一的问题是,你必须要保证 FPS 永远不会改变(但这是完全不现实的)。如果帧率发生变化,更新的距离也会发生变化。
回到那个方块的例子,如果我们只有 10FPS 而不是 60FPS 呢?那么方块每秒只会移动 20px 而不是 120px。这就是基于帧的动画存在的问题,完全依赖于帧速率。
看到这里,我们来看看下面的例子。每个盒子都是基于帧的动画,它们的代码完全一样,唯一的区别就是它们的帧速率不一样。
黑色的方块(设置为 60FPS)运行良好,但是红色的方块(设置为 10FPS)运行得十分缓慢。这是因为红色的方块每秒只更新 10 次,黑色的方块每秒更新 60 次。
因此,如果我们想保持一致的更新而不受帧速率的影响,那我们就不能使用基于帧的动画。相反,我们必须为我们的更新使用一个帧独立的技术,这就是基于时间的动画。
基于时间的动画
基于时间的动画就是由你使用的上一帧所经过的时间量来决定当前帧的更新量。这听起来可能有点复杂,但是如果你上过一些物理课程的话,你可能就已经熟悉这种方法了。
下面这个方程用来更新实体的位置,x = x0 + vt + at2
,这是一个基于时间的方程。它计算出经过多长时间以及在这段时间内,实体移动了多少距离。我们这里,基于时间的动画也是同样的方式。
为了更新我们的方块的位置,我们将要使用方程x = x + dx * dt
(在基于帧的动画里,我们用到的方程是x = x + dx
)。在更新我们的方块之前,我们通过下面的代码来得出自上次更新到现在,经历了多少时间。
last = new Date().getTime();
function animationLoop() {
now = new Date().getTime();
dt = now - last;
last = now;
update(dt);
draw();
}
每一帧我们都会计算上一帧更新的时间last
和当前时间now
的差值dt
,然后我们使用这个差值来更新我们的方块的位置。下面是使用基于时间的动画的结果。
正如你所看到的,黑色方块(设置为 60FPS)和蓝色方块(设置为 30FPS)保持相对同步。然而,红色方块(设置为 10FPS)的完全不生效而且有时候还会完全消失。如果你观察得时间足够长,你会看到蓝色方块最后落后黑色方块越来越多,甚至会有一些很奇怪的结果比如说粘在容器的边缘上。
那么到底发生了什么?我们使用了基于时间的动画,因此无论帧速率多少,每个方块都应该移动相同的距离。原理上来看是没有问题的,但是在程序中却不是这样。
在我们的基于时间的动画的使用中,有两个问题,第一个问题是每次更新方块到底要移动多少距离;第二个问题就是所谓的“螺旋式死亡”。
第一个问题是个简单的数学问题。黑色方块每秒更新 60 次,即每次大约 0.0166 秒。然而,红色方块每秒更新 10 次,即每次大约 0.1 秒。这意味着每次更新的时候,红色方块移动的距离大约是黑色方块的六倍(大约 12px,如果我们设置每次更新方块移动 2px 的话)。这 12px 更新红色方块的位置,使得红色方块的位置穿过了容器的边,导致它后来没有反弹回来。
第二个问题是由于我们的更新花费的时间比它们应该要花费的时间稍微长一点。你可以阅读这篇文章来修复你的问题。
为了修复我们的问题,我们使用上面提到的文章的最后部分的解决方案。
基于时间的动画(改良)
为了修复我们的问题,我们将实现一个固定dt
更新(或把dt
设置为常熟传递给每个更新)。这种方式,每次更新每个方块只会移动 2px,这样,我们就解决了第一个问题。
然而,每个方块还是需要基于上一帧的时间来更新不同的次数。因此我们还是需要记录自上一帧结束到现在经历了多少时间以及计算更新每个方块多少次。
因为我们是在固定的dt
内更新的,每一帧会剩余一点点小于dt
的时间,这些剩余的时间保存起来以便于我们能准确的得出下一帧需要移动的距离,这样就解决了问题二(螺旋式死亡)。
更新的代码如下
last = new Date().getTime();
// 设置 dt 为常量
dt = 1000 / 60;
function animationLoop() {
now = new Date().getTime();
passed = now - last;
last = now;
accumulator += passed;
while (accumulator >= dt) {
update(dt);
accumulator -= dt;
}
draw();
}
下面是代码结果
正如你所看到的,无论帧速率是什么,每个方块都保持同步。唯一的区别是动画的平滑程度(FPS 越高意味着动画越平滑)。
结论
如果能确保帧速率不会变化(这是不可能的),基于帧的动画就会很好的工作。相反,应该使用基于时间的动画,来确保在不同的帧速率下体验是一致的。但是要注意,确保你的基于时间的动画使用了比较好的算法,否则,会出现一些意想不到的结果。