FixedUpdate真的是固定的时间间隔执行吗?聊聊游戏定时器

0x00 前言
有时候即便是官方的文档手册也会让人产生误解,比如本文将要讨论的Unity引擎中的FixedUpdate方法。
This function is called every fixed framerate frame, if the MonoBehaviour is enabled.
而大家似乎也都认为FixedUpdate是在固定的时间间隔执行,不受游戏帧率的影响。其实这么说并不准确,甚至有些误导。因此,我就写这篇文章来聊聊Unity的FixedUpdate方法,或者说来聊聊游戏引擎的定时器实现吧。
0x01 直观印象
我相信各位对FixedUpdate的直观印象首先来自于它的名字。并且常常会和一般Update方法进行对比,以突显它的,呃,特殊性。
其次,有一些实验精神的小伙伴也许会亲自来测试一下这个方法的时间间隔。而Console输出的内容也十分符合他的预期,简直毫厘不差。

(图片来自网络)
不过,我相信很多笃信FixedUpdate的朋友一定有过类似这样的疑虑:默认情况下FixedUpdate的更新频率是50FPS(0.02s),如果当游戏的更新频率较大时——例如60FPS——那么FixedUpdate有固定的更新频率还稍微可以理解,但是如果游戏本身的更新频率就很低——例如30FPS——那么是怎么保证FixUpdate的更新频率呢?
而Unity的主要逻辑是单线程的(参见Aras的回答),所以也不存在有不同的逻辑循环可能,换句话说Update和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,而不要滥用。而由于FixedUpdate主要是用于物理逻辑的,因此下文的讨论也主要围绕物理逻辑。
0x02 可变增量时间
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)
{
//...休眠后计算新的增量时间
}
所以看上去整个游戏保证一个大致的更新频率似乎不难。但是现在问题的关键在于每次更新时的时间增量无法保证相同。而在物理模拟中,保证一个固定的增量时间是十分重要的。这是因为在游戏引擎进行物理模拟时要使用数值积分,而作为最简单的数值积分方法——欧拉法在游戏引擎中大量使用。

上面就是一个欧拉法的简单例子。可以看到增量时间是很重要的。而在游戏引擎的物理模拟中,一个不稳定的增量时间可能导致很多和预期相悖的结果。
由于此时计算的是真实的时间,而真实的增量时间无法保证固定,那换一个思路,我们把参与物理模拟的增量时间当做一个常量可以吗?换句话说,不论游戏的更新频率如何,参与物理模拟的增量时间是一个常数。
0x03 固定增量时间
一个最简单的固定增量时间的实现,显然就是将固定的增量时间作为一个常量参数传递给物理模拟模块,这样我们就能够保证物理模拟的增量时间固定,同时还能将物理模拟的更新频率和游戏引擎的更新频率进行解耦——物理的模拟不受引擎的更新频率影响,无论游戏的更新频率是多少,传递给物理模拟的增量时间都是一个常量。
这里还拿Unity引擎来举例子,默认情况下项目的Fixed Timestep的值为0.02s。也就是说物理模拟的频率是50FPS,假设我们的游戏的更新频率是25FPS,那么会发生什么呢?没错,游戏每1次Update时,物理模拟都要推进2次,也就是之前我们看到的在Update之前多次调用了FixedUpdate。那么如果我们的游戏更新频率是100FPS呢?这次就变成了每2次Update调用1次FixedUpdate。
这也就解释了为何有的朋友在做相关的小测试的时候,在每次FixedUpdate内打印Time.deltaTime时输出的都是0.02了,因为Time.deltaTime并非两次调用FixedUpdate之间真实的时间间隔,而是来自我们在项目的Time设置内设置的值——它是一个与真实时间无关的常量。
When called from inside MonoBehaviour's FixedUpdate, returns the fixed framerate delta time.
那么这种固定增量时间的逻辑如何用代码表示呢?很简单,只需要在主循环的内部,再来一个二级循环。
double simulationTime = 0;
double fixedTime = 20;
while (!quit()){
double realTime = Timer.GetTime();
while (simulationTime < realTime){
simulationTime += fixedTime;
UpdateWorld(fixedTime);
}
RenderWorld();
}
而Unity的实现显然也是类似的,这个在Unity手册中关于执行顺序的相关章节内可以看到。

上图标明了FixedUpdate是属于物理模拟模块的,同时在主循环的内部,物理模拟的部分还有一个二级循环。
0x04 总结及建议
好了,到了需要总结一下的时候了。
经过上文的分析,我想各位应该都能了解一个游戏引擎是如何实现定时器的了吧,而且在物理模拟时使用一个固定的增量时间也并不神秘,只需要让物理模拟和真实时间解耦,使用一个常量作为其增量时间即可。
同时还要提醒各位,不要滥用Unity的FixedUpdate方法,因为它是用来处理物理模拟的,更重要的是它并非根据真实时间的间隔执行。
-EOF-
最后打个广告,欢迎支持我的书《Unity 3D脚本编程》

欢迎大家关注我的公众号慕容的游戏编程:chenjd01

FixedUpdate真的是固定的时间间隔执行吗?聊聊游戏定时器的更多相关文章
- 利用 BackgroundService 固定时间间隔执行某动作
继承 BackgroundService 类: 为什么会写这个东西呢?本人在写消息队列的时候思考过一个问题——比如,每5秒从队列里面取一条消息(一条消息里面又包含了1000条数据),要把这1000条数 ...
- Unity 脚本中update,fixedupdate,lateupdate等函数的执行顺序
结论 通过一个例子得出的结论是:从先到后被执行的函数是 Awake->Start->FixedUpdate->Update->LateUpdate->OnGUI. 示例 ...
- 灵魂拷问:你真的理解System.out.println()执行原理吗?
原创/朱季谦 灵魂拷问,这位独秀同学,你会这道题吗? 请说说,"System.out.println()"原理...... 这应该是刚开始学习Java时用到最多一段代码,迄今为止 ...
- 分库分表真的适合你的系统吗?聊聊分库分表和NewSQL如何选择
曾几何时,"并发高就分库,数据大就分表"已经成了处理 MySQL 数据增长问题的圣经. 面试官喜欢问,博主喜欢写,候选人也喜欢背,似乎已经形成了一个闭环. 但你有没有思考过,分库分 ...
- Unity游戏暂停之Update与FixedUpdate区别
游戏暂停 示例程序 下面这段代码演示游戏暂停 using UnityEngine; using System.Collections; public class GamePauseTest : Mon ...
- Unity之Update与FixedUpdate区别
下面这段代码演示游戏暂停 using UnityEngine; using System.Collections; public class GamePauseTest : MonoBehaviour ...
- Physics(物理系统)
物理: Physics Box2d Unity 内置NVDIA PhysX物理引擎 刚体:要使一个物体在物理控制下,简单添加一个刚体给它.这时,物体将受重力影响,并可以与其他 ...
- Unity学习疑问记录之脚本生命周期
总的来说unity的脚本生命周期分几个部分:编辑→初始化→游戏逻辑→渲染→GUI→Teardown首先是Reset,顾名思义:重置.在什么情况下调用呢?1.用户第一次添加组件时.2用户点击见组件面板上 ...
- Unity3d 协程
参考文章: http://blog.csdn.net/onafioo/article/details/48979939 http://www.cnblogs.com/zhaoqingqing/p/37 ...
随机推荐
- 201521123104 《Java程序设计》第8周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 2. 书面作业 1. List中指定元素的删除(题目4-1) 1.1 实验总结 这道题的关键是如何删除元素.一 ...
- 《Java程序设计》第5周学习总结
1. 本章学习总结 1.1 尝试使用思维导图总结有关多态与接口的知识点. 1.2 可选:使用常规方法总结其他上课内容. 2. 书面作业 1. 代码阅读:Child压缩包内源代码 1.1 com.par ...
- 201521123111《Java程序设计》第4周学习总结
1. 本章学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 1.2 使用常规方法总结其他上课内容. Answer: - 上课还讲了tostring的使用,般toString用于返回表示对象值的 ...
- 201521123011 《Java程序设计》第1周学习总结
1. 本章学习总结 通过这章的学习让我初步认识了Java,认识到了java和之前c语言的异同点. 对java是面向对象的这句话有了一定了解 认识了jdk,jre,jvm的区别. 2.书面作业 1.为什 ...
- 201521123017 《Java程序设计》第13周学习总结
1. 本周学习总结 2. 书面作业 Q1.网络基础 1.1 比较ping www.baidu.com与ping cec.jmu.edu.cn,分析返回结果有何不同?为什么会有这样的不同? 1.2 te ...
- Android SDK Manager 闪退的解决办法
(一)方案一 原理: SDK Manager.exe 通过调用 android-sdk-windows\tools\lib\find_java.bat 确认 java.exe 的路径 启用 cmd ...
- Shiro第五篇【授权过滤、注解、JSP标签方式、与ehcache整合】
授权过滤器测试 我们的授权过滤器使用的是permissionsAuthorizationFilter来进行拦截.我们可以在application-shiro中配置filter规则 <!--商品查 ...
- 为bookStore添加权限【动态代理和注解】
前言 目前为止,我们已经学习了动态代理技术和注解技术了.于是我们想要为之前的bookStore项目添加权限控制-.. 只有用户有权限的时候,后台管理才可以进行相对应的操作-.. 实现思路 之前我们做权 ...
- linux(3)磁盘与文件系统管理/查看硬盘、内存空间/文件系统的操作/ 文件的压缩和打包
一.磁盘与文件系统管理 1.分区与文件系统分区:记录每一个分区的开始柱面和结束柱面主引导区(master boot recorder):记录分区的数据,记录硬盘里所有的分区信息分区划分好后,要将分区格 ...
- jpa 多对多关系的实现注解形式
1, 表结构 1)设备表 VTM_DEVICE_INFO create table VTM_DEVICE_INFO ( ID INTEGER not null, SN ) not null, STAT ...