前言
我相信大家在学习Unity的时候,Update是每一帧调用,而FixedUpdate是固定时间调用一次。
一开始我们对这个知识深信不疑(楼主也是 =.=| )
不过当我们学的更深入时,发现Unity其实是单线程的,所以它的生命周期肯定也是同步的
引出问题
那么疑问就来了,既然是单线程的,那假设我现在机器卡了,可能1秒才调用一次Update,也就是1帧,那么假设FixedUpdate的更新频率是50FPS(0.02s),那么FixedUpdate还会每隔0.02时间调用一次吗?
猜测
由于我们知道Unity是单线程的,那么肯定不会单独给FixedUpdate开一个线程,然后调用它。
所以我们猜测FixedUpdate其实不是每隔固定一个真实世界的时间调用一次
推测
如果我们来重复一下各位测试FixedUpdate时经常采用的方式:打印两次调用之间的时间间隔,或者是计算每次调用的累积时间,但区别在于我直接使用了真实的时间:
void FixedUpdate()
{
Debug.Log("FixedUpdate realTime: " + Time.realtimeSinceStartup);
}
void Update()
{
Debug.LogError("Update realTime: " + Time.realtimeSinceStartup);
}
为了以示区别,正常的Log是FixedUpdate,LogError是Update,并且设定整个游戏的更新频率为30FPS。
(图片来自网络)
通过上图可以发现,FixedUpdate并不是间隔0.02s才调用一次,相反,它有可能在Update之前调用多次。
例如刚启动时,在某次Update之前连续调用了11次FixedUpdate,而之后每次Update调用之前都会调用1~2次FixedUpdate方法,这也很好理解,因为FixedUpdate的频率是50FPS,而我们设定的更新频率为30FPS,FixedUpdate的调用次数势必要多于Update。
所以,FixedUpdate除了用来处理物理逻辑之外并不适合处理其他模块的逻辑,尤其是当大家的潜意识里都笃定它的更新频率是固定的时候。因为这很危险,比如一个需要状态同步的游戏要求按照固定的频率向目标同步状态,如果贸然采用FixedUpdate方法,就会出现上图那样可能在短时间内多次调用的问题。所以最好只在物理逻辑的处理中使用FixedUpdate,而不要滥用。
可变增量时间
ok,那我们来看看游戏引擎的定时器是如何来实现的吧。假设我们手头没有一个现成的游戏引擎,一切都需要自己来实现,那么一个最简单的游戏循环大概就是下面这个样子的。
double lastTime = Timer.GetTime();
...
while (!quit()){
double currentTime = Timer.GetTime();
double frameTime = currentTime - lastTime ;
UpdateWorld(frameTime);
RenderWorld();
lastTime = currentTime;
}
这种实现方式使得增量时间和具体的机器设备相关,并且它每一次的时间增量都不一定相同。
当机器十分快时,引擎可能通过线程休眠来保证固定的FPS。
while(timeout > 0.001 && deltaTime<timeout)
{
//...休眠后计算新的增量时间
}
所以看上去整个游戏保证一个大致的更新频率似乎不难。但是现在问题的关键在于每次更新时的时间增量无法保证相同。而在物理模拟中,保证一个固定的增量时间是十分重要的。这是因为在游戏引擎进行物理模拟时要使用数值积分,而作为最简单的数值积分方法——欧拉法在游戏引擎中大量使用。
由于此时计算的是真实的时间,而真实的增量时间无法保证固定,那换一个思路,我们把参与物理模拟的增量时间当做一个常量可以吗?换句话说,不论游戏的更新频率如何,参与物理模拟的增量时间是一个常数。
固定增量时间
一个最简单的固定增量时间的实现,显然就是将固定的增量时间作为一个常量参数传递给物理模拟模块,这样我们就能够保证物理模拟的增量时间固定,同时还能将物理模拟的更新频率和游戏引擎的更新频率进行解耦——物理的模拟不受引擎的更新频率影响,无论游戏的更新频率是多少,传递给物理模拟的增量时间都是一个常量。
这里还拿Unity引擎来举例子,默认情况下项目的Fixed Timestep的值为0.02s。也就是说物理模拟的频率是50FPS,假设我们的游戏的更新频率是25FPS,那么会发生什么呢?没错,游戏每1次Update时,物理模拟都要推进2次,也就是之前我们看到的在Update之前多次调用了FixedUpdate。那么如果我们的游戏更新频率是100FPS呢?这次就变成了每2次Update调用1次FixedUpdate。
这也就解释了为何有的朋友在做相关的小测试的时候,在每次FixedUpdate内打印Time.deltaTime时输出的都是0.02了,因为在FixedUpdate方法内获取Time.deltaTime并非两次调用FixedUpdate之间真实的时间间隔,而是来自我们在项目的Time设置内设置的值——它是一个与真实时间无关的常量。
那么这种固定增量时间的逻辑如何用代码表示呢?很简单,只需要在主循环的内部,再来一个二级循环。
double simulationTime = 0;
double fixedTime = 20;
while (!quit()){
double realTime = Timer.GetTime();
while (simulationTime < realTime){
simulationTime += fixedTime;
UpdateWorld(fixedTime);
}
RenderWorld();
}
而Unity的实现显然也是类似的,这个在Unity手册中关于执行顺序的相关章节内可以看到。
上图标明了FixedUpdate是属于物理模拟模块的,同时在主循环的内部,物理模拟的部分还有一个二级循环。
总结
FixedUpdate其实按照严格意义上来说,不会是每隔一个固定时间调用一次。注意:我说的是调用一次。
例如现在游戏1秒内调用10次Update,也就是0.1秒调用一次,假设FixedUpdate的更新频率是50FPS(0.02s),那么Update执行一次,FixedUpdate应该执行5次
那么就会出现下面的情况
while(!quit())
{
// 由于Unity的生命周期,所以FixedUpdate会比Update先执行
FixedUpdate();//第一次
FixedUpdate();//第二次
FixedUpdate();//第三次
FixedUpdate();//第四次
FixedUpdate();//第五次
Update();
}
其实可以看做Unity自己计算了距离上一次循环隔了多久,然后计算出FixedUpdate实际应该执行多少次。
参考资料:FixedUpdate真的是固定的时间间隔执行吗?聊聊游戏定时器