Skip to content

【Canvas杂谈:第一季】RAF/FPS/dt干嘛用? #1

Closed
@hongru

Description

title


写在前面的话

  1. 为什么要写【Canvas杂谈】?

    答:还是抱着对于canvas的热情和态度,即使应用场景有限,不忍放下。加之目前集团淘宝,天猫,无线都有游戏化场景的需求。有一些和技术有关无关的话想和大家分享。
  2. 【Canvas杂谈】讲什么?

    答:和canvas有关的事情,他的好,他的坏,他坑爹的地方,以及可以和他做的窝心的事情。
  3. 该用什么样的心态来阅读【Canvas杂谈】?

    答:以“玩乐技术”的态度,它讲技术,但也讲故事。

我们今天要说的故事。要从RAF开始。
首先,RAF是什么?我相信大部分正在阅读此文的同学应该正在使用chrome浏览器。打开控制台,敲出webkitRequestAnimationFrame .
小编不会赘述这个东西干嘛用的,知道的同学已经知道了,不太了解的同学随便google一下也就知道了。说的简单一点,它就是个计时器 干着和setTimeout(callback, 16) 类似的事情。

为什么偏偏是setTimeout(callback, 16)
不知道大家对上面的16ms有什么疑义没?
image

两三年前当js动画的计时器都是使用setTimeoutsetInterval来实现的时候。小编曾经有过以下的蠢事。

  • 以前在做一些带动画的组件的时候,比如幻灯片slide,或者什么淡出淡出,划入划出的时候。总是觉得这个动画的时间间隔设的越短,动画表现就会越流畅。

所以曾经做过一件事:

setInterval(function(){  
    // animation func
}, 1);

最后发现其实setInterval 这种东西是有个最小的时间间隔的。比如小编现在在用的chrome里面 setInterval 的最小时间间隔就是4ms。

但是就算给他设置4ms的interval 它也不是4ms,因为... 我们还要考虑setInterval中的function的运行时间... 说到这里,剩下的小伙伴们自己去想吧。

好吧,回到开始的 16ms的话题。为什么16ms是合适的,而不是10ms或者20ms。当然这跟我们的人体工程有关系。有医学研究发现我们人眼对于刷新率的感知。每秒60帧几乎就满足了我们的眼睛。意思就是好像你花个5k块钱买个刷新率每秒200帧的电视看起来跟别人1k块的刷新率每秒60帧的电视看起来流畅度差不多... 汗!!
image

那么16ms 和60帧又有什么关系呢? 请打开计算器 计算一下, 1秒 = 1000毫秒, 1000/60 = ??

就算群众的眼睛是雪亮的..60帧也足够了。

16ms和RAF又有什么关系呢??
image
请看图,不解释 💯

然后,我们要说的是RAF和setInterval(callback, 16)以及setTimeout(callback, 16) 的关系
在扯之前,先让大家看两个demo 【图片可点】。
1-1 2-1

这两个实现的是同一个东西,虽然不是canvas实现的东西,但是跟RAF和setInterval有关。我相信大家用浏览器打开这两个demo看到的效果是一样的。当然,浏览器需要支持RAF,然后,请按下面的操作做:

同时打开这两个demo,然后浏览器最小化,或者切到别的tab去看看新闻啊,看看美女帅哥啊,过个半分一分钟再回到这两个页面来看看。
我想会突然好像知道了什么事情。。。

setInterval 和 setTimeout 在非当前窗口,或者浏览器‘休眠’的时候,即使渲染停止了,但是计时器不会休息,仍然会顽强的跑着!
这其实是一个不好的讯息。会大大加重浏览器的负担,同时也会影响这个页面或者app本身的性能。

以上,为第一个明显的不同的地方。

第二个不同的地方,在于callback队列的不同。小编举个栗子:

我们假设这里有3个循环计数器的实现,A是利用RAF的递归调用做的,B是用setInterval + 16ms做的,C是用setTimeout + 16ms + 递归调用实现的。
然后用这3个计步器来做一个callback的循环调用。同时假设这个callback执行消耗的时间固定为100ms。
那么我们可以大致猜想一下 B在做这件事情的具体过程:
B从时间零点开始第一次执行callback,16ms之后发现应该执行第二次了,但是发现第一次还没做完,于是还是得继续第一次没做完的事情,把第二次callback排到自己的一个计划队列里面,等到第一次执行完了再执行队列里的第二次...
但是恶性循环,32ms之后,第一次的事情还没做完,第3次的任务又来了。。。
于是,这个任务队列越来越长,越来越长。。。

C来安排任务的机制会比B稍微好一点点,但终究也逃不了B的厄运,身上的负担会越来越重。

我们来看A做这件事情的过程。

每一次任务的完成仍然需要100ms,但是浏览器不会在A没有完成当前任务的时候给它下一次的任务。也就是说A总是花100ms完成一次任务,然后再花100ms完成下一次任务。

所以,A完成10次任务所花的时间应该是1000ms,但是B和C完成10次任务应该需要>=1000ms,因为他们还要分心去管理他们的任务队列。

做个更形象的比喻:

  • 你做一个任务需要一天,但是你的老板每个小时都回来给你一个新任务。
  • 你做一个任务需要一天,等你做完今天的任务,你的老板会给你下一次的任务。

结果是你都花10天做完了10个任务,请问你觉得你更喜欢哪种方式??


RAF说完了,我们来说说FPSdt
我们都知道要让canvas里面的东西动起来,本质上你在位置1画了一个矩形,然后擦掉,然后在位置二画了一个同样的矩形。所以,这个矩形从位置一运动到了位置二。
我们把这个过程叫做“一帧”。

FPS: Frame Per Second, 翻译过来是每秒的帧数。
dt: Delta Time, 翻译过来是时间差,或者每帧的时间间隔。

理论上,fps和dt的关系就是 FPS = 1s/dt 。他们都是用来表示一个系统每秒能够运行的帧数的。简单来说,这两个东西可以反应动画的流畅程度。

那么,FPS通常是怎么获取到的呢?
我们看一个demo:

这是小编闲暇时用粒子的双密度松弛算法做的简单的流体模拟实验。我们来说说右上角的fps是怎么得到的呢?
根据 FPS= 1/dt. 所以首先我们先去拿到这个帧的时间间隔dt。在你的动画循环计数器开始的时候记录下来时间,和上一次记录下来的时间相减,就能简单得到一个当前帧的时间差dt。

代码就不多说,我想大家应该都知道怎么做。拿到dt之后,换算成fps,按合适的时间抛出来就可以了。

如果你觉得麻烦,推荐大家使用 mrdoob/stats.js

最后一个问题:dt 除了可以反应动画流畅度之外,还有什么用??
小编猜测,应该好多的同学没有意识到dt这个时间差在动画,或者游戏里面还有一个莫大的用处。
举一个场景:

假设我们在性能好的机器和性能差一些的机器上同时测试一个游戏。好的机器的FPS稳定在60,差的机器FPS稳定在30。
又假设我们给游戏主角的设定为“按住方向右键,主角在游戏场景里向右走动”。在这种设定下,假设A同学的实现为

//假设person为主角的一个实例,person.x 表示主角在游戏场景的位置。在每帧的update里如果这样写。
person.x += 1

那么在好的机器上,用户按下方向键1s,游戏执行了60帧,每帧人物的位置+1,那么人物移动了60的距离。
但是在坏的机器上,用户按下方向键1s,游戏只会执行30帧,同样每帧人物位置+1,人物只会移动30的距离。

这。。。显然不是我们期望的。

接下来,每帧的时间差dt就派上用场了。如果我们在每帧的update里面这样写:

person.x += dt*60;

好的机器上,用户按下方向键1s,游戏执行60帧,每帧的间隔dt是1/60, dt_60 就是1,每帧人物移动1的距离。1s移动60的距离。
坏的机器上,游戏执行30帧,dt=1/30, dt_60=2; 也就是每帧人物会移动2的距离,那么1s下来,人物移动总距离还是60.

说到这里,小编觉得应该忽然明白了什么。。。
image


本期的【Canvas杂谈】到此为止,总结一下:

  • 如果你在做动画,不管是不是基于canvas的,不妨尝试一下requestAnimationFrame 。至于浏览器的兼容问题,小编认为你自有方法搞定。
  • 如果你在做动画,不妨把FPS 暴露出来,对性能的检测和优化一定有用的。
  • 如果你在做动画,帧的时间差dt 可以发挥更大的用处。

【完】


写在后面的话,

  • 这里是【Canvas杂谈】第一期,文字有点多,看官们不要厌烦。
  • 【Canvas杂谈】以季度为单位,每周一期,希望能对你有所帮助。 第一季话题草案 issues
  • 知之为知之,不知为不知,如果叙述有不准确,欢迎来 issues 里讨论。
  • 如果您有想听的话题,请来**New Issue** 里提。小编会有规律的随机选取有价值的话题撰文。
  • 每期【Canvas杂谈】会同步更新到ATA canvas小组圈子 http://www.atatech.org/gprofile/928

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions