如果说场景、模型、材质和光照构成了游戏世界的“身体”,那么时间系统就是这个世界的心跳。
没有时间,角色不会奔跑,子弹不会飞行,昼夜不会交替,技能冷却不会减少,动画不会播放,粒子不会飘散。一个游戏画面即使再精美,也只是一张静止的图片。正是Unity中的时间系统,让一切从“摆在场景里”变成“活在世界中”。
Unity的时间系统主要围绕Time类展开。它看起来只是一些简单的变量,例如:
Time.deltaTime Time.time Time.fixedDeltaTime Time.timeScale但这些变量背后,连接着帧率、物理模拟、动画播放、暂停机制、网络同步、技能计时和游戏节奏控制等大量核心系统。
理解Unity时间系统,不只是会写:
transform.Translate(Vector3.forward*speed*Time.deltaTime);更重要的是理解:Unity中的“时间”并不只有一种。
一、现实时间、游戏时间与帧时间:Unity里的时间不止一条河
想象你坐在电脑前玩游戏。
现实世界中的钟表一直在走:一秒、两秒、三秒。这叫做真实时间,或者说墙上时钟的时间。
但游戏中的时间未必和现实完全一致。
例如你按下暂停键,现实世界过去了10秒,但游戏中的角色应该保持不动;又比如游戏进入慢动作,现实中过去1秒,游戏里可能只推进0.2秒;再比如网络游戏中,服务器有自己的统一时钟,本地客户端还需要和它同步。
因此,Unity中至少存在几种不同概念的时间:
真实时间:不受暂停影响,现实中过了多久就是多久; 缩放后的游戏时间:会受到Time.timeScale影响; 每帧间隔时间:当前帧和上一帧之间隔了多久; 固定物理时间:专门用于物理模拟的固定节奏; 启动后的累计时间:游戏运行至今经过了多久。它们看似相近,却适合完全不同的用途。
如果使用错误,就会出现很多经典问题:
- 高帧率电脑角色跑得更快;
- 暂停后UI倒计时也停住;
- 慢动作时物理系统异常;
- 使用
Update移动刚体导致穿墙和抖动; - 技能CD在卡顿时跳动;
- 动画和物理不同步。
所以,Unity时间系统真正的意义,是帮助开发者区分“什么事情应该跟随游戏时间走,什么事情应该跟随现实时间走”。
二、最常用的Time.deltaTime:每一帧过去了多久
Unity开发中最常见的一行代码大概是:
Time.deltaTime它表示:
从上一帧到当前帧,游戏时间过去了多少秒。
如果游戏运行在60帧,那么理论上每帧大约是:
1 / 60 = 0.01667秒此时:
Time.deltaTime大约等于:
0.01667f如果游戏只有30帧,那么每帧间隔大约是:
1 / 30 = 0.03333秒此时deltaTime约为:
0.03333f这就是为什么移动物体时不能简单写:
transform.position+=Vector3.forward*speed;假设:
floatspeed=5f;在60FPS下,上面代码每秒执行60次,物体每秒移动:
5 × 60 = 300单位而在30FPS下,每秒只执行30次,则移动:
5 × 30 = 150单位同一个角色,在不同电脑上移动速度相差一倍,这显然不可接受。
正确写法应该是:
transform.position+=Vector3.forward*speed*Time.deltaTime;现在speed的单位不再是“每帧移动多少”,而变成:
每秒移动多少单位在60FPS下:
5 × 0.01667 × 60 ≈ 5单位/秒在30FPS下:
5 × 0.03333 × 30 ≈ 5单位/秒无论帧率高低,角色每秒都接近移动5个Unity单位。
这就是deltaTime最重要的作用:
让基于帧执行的逻辑,转换成基于时间执行的逻辑。
三、Update:跟着画面帧率跳动的时间
Unity中的Update()通常每帧调用一次:
privatevoidUpdate(){// 每帧执行}但“每帧一次”并不等于“每隔固定时间一次”。
如果电脑性能好,可能一秒执行144次;如果性能一般,可能一秒执行60次;如果场景复杂或设备卡顿,可能一秒只执行30次甚至更低。
因此,Update像一个跟着画面节奏跳舞的鼓点:画面刷新一次,它就敲一次;画面刷新快,它敲得密;画面刷新慢,它敲得稀。
适合放在Update中的逻辑包括:
- 玩家输入读取;
- 普通角色位移;
- 摄像机跟随;
- UI刷新;
- 技能冷却显示;
- 非物理动画;
- 插值移动;
- 鼠标交互;
- 基于
deltaTime的倒计时。
例如让物体旋转:
usingUnityEngine;publicclassRotateObject:MonoBehaviour{publicfloatrotateSpeed=90f;privatevoidUpdate(){transform.Rotate(Vector3.up,rotateSpeed*Time.deltaTime,Space.World);}}这里rotateSpeed = 90表示每秒旋转90度,而不是每帧旋转90度。
四、Time.time:游戏世界已经走了多久
Time.time表示从游戏开始运行到当前时刻,经过了多少“游戏时间”。
例如:
Debug.Log(Time.time);可能输出:
0.0 1.2 15.8 123.45它是一个不断累积的时间值。
可以把它想象成游戏世界启动后的一只秒表。
常见用途包括:
- 周期性运动;
- 昼夜循环;
- 正弦波动画;
- 定时触发;
- 闪烁效果;
- 按时间生成敌人;
- 判断技能是否冷却结束。
例如让物体上下浮动:
usingUnityEngine;publicclassFloatObject:MonoBehaviour{publicfloatamplitude=0.5f;publicfloatfrequency=2f;privateVector3startPosition;privatevoidStart(){startPosition=transform.position;}privatevoidUpdate(){floatoffsetY=Mathf.Sin(Time.time*frequency)*amplitude;transform.position=startPosition+Vector3.up*offsetY;}}这里:
Mathf.Sin(Time.time*frequency)会在-1到1之间平滑变化,形成自然的上下浮动。
如果把Time.time看成一条持续流动的河流,那么Sin函数就像在河面上不断起伏的波浪。时间往前流,物体便不断上下摆动。
不过需要注意:
Time.time会受到:
Time.timeScale影响。
也就是说,如果游戏暂停或慢动作,Time.time会变慢,甚至停止。
五、Time.timeScale:控制整个游戏世界的“时间流速”
Time.timeScale是Unity时间系统中最有趣、也最强大的变量之一。
默认情况下:
Time.timeScale=1f;这表示游戏以正常速度运行。
如果设置为:
Time.timeScale=0.5f;游戏时间会变成半速。
效果通常包括:
- 使用
Time.deltaTime的角色移动变慢; - 大多数动画变慢;
- 基于游戏时间的倒计时变慢;
- 物理模拟变慢;
- 粒子和部分特效可能变慢;
Time.time增长变慢。
这就是慢动作效果的基础。
例如子弹时间:
usingUnityEngine;publicclassSlowMotionController:MonoBehaviour{publicfloatslowMotionScale=0.2f;privatevoidUpdate(){if(Input.GetKeyDown(KeyCode.Space)){Time.timeScale=slowMotionScale;}if(Input.GetKeyUp(KeyCode.Space)){Time.timeScale=1f;}}}按住空格时,世界进入慢动作;松开后恢复正常。
如果设置:
Time.timeScale=0f;游戏时间将停止推进。
这常用于暂停游戏:
publicvoidPauseGame(){Time.timeScale=0f;}publicvoidResumeGame(){Time.timeScale=1f;}但这里有一个非常重要的细节:
Time.timeScale = 0并不意味着Unity完全停止运行。
当时间缩放为0时:
Update()通常仍然会被调用;- UI仍然可以响应;
- 鼠标和键盘输入仍然可以读取;
- 脚本逻辑仍然可以执行;
- 只是
Time.deltaTime会变成0; Time.time不再增长;FixedUpdate()通常不再正常推进。
这意味着暂停菜单仍可以正常点击,因为UI脚本的Update还在运行;但角色移动代码由于乘了:
Time.deltaTime所以移动距离变成0。
这正是Unity暂停机制灵活的地方。
六、暂停时为什么UI动画和倒计时也停了?
假设你有一个暂停界面,希望它弹出来时缓慢放大:
panel.localScale=Vector3.Lerp(panel.localScale,Vector3.one,Time.deltaTime*5f);当你设置:
Time.timeScale=0f;时,Time.deltaTime也变成0,于是:
Time.deltaTime*5f=0UI完全不会继续缩放。
这时应该使用:
Time.unscaledDeltaTime它表示:
不受
Time.timeScale影响的真实帧间隔。
修改后:
panel.localScale=Vector3.Lerp(panel.localScale,Vector3.one,Time.unscaledDeltaTime*5f);即使游戏暂停,UI仍可以播放动画。
同理,暂停菜单中的倒计时、网络连接超时、广告计时、Loading界面动画等,很多都应该使用不受缩放影响的时间。
Unity中常见的非缩放时间包括:
Time.unscaledDeltaTime Time.unscaledTime Time.realtimeSinceStartup它们可以理解为“现实世界的时间”。
例如:
floatelapsed=Time.realtimeSinceStartup;即使游戏处于暂停、慢动作状态,这个时间仍然继续增长。
七、FixedUpdate与fixedDeltaTime:物理世界有自己的节拍器
如果说Update是跟随画面帧率变化的节奏,那么FixedUpdate就是一台固定节拍的节拍器。
Unity物理系统,例如:
- Rigidbody移动;
- 重力;
- 碰撞;
- 摩擦;
- 弹跳;
- 力的施加;
- Joint关节;
- 车辆轮子;
- 角色物理交互;
通常依赖FixedUpdate()。
默认情况下:
Time.fixedDeltaTime=0.02f;这意味着物理系统默认每隔:
0.02秒模拟一次,即每秒50次。
privatevoidFixedUpdate(){// 固定时间间隔执行}和Update不同,FixedUpdate不一定每帧只执行一次。
例如画面运行在120FPS时,渲染帧很多,但物理每秒仍然只模拟50次,因此某些画面帧中可能没有FixedUpdate。
反过来,如果画面卡顿,一帧用了很久,Unity为了追赶物理时间,可能在一帧内连续调用多次FixedUpdate。
例如某一帧卡了0.1秒,而物理间隔为0.02秒,那么Unity可能需要补做约5次物理更新。
这也是为什么在FixedUpdate中不能把它简单理解为“一帧一次”。
八、为什么刚体要在FixedUpdate中移动
假设有一个带Rigidbody的球体。
不推荐这样写:
privatevoidUpdate(){transform.position+=Vector3.forward*speed*Time.deltaTime;}因为这样是直接修改Transform位置,物理系统来不及正确参与碰撞计算。高速移动时,物体可能直接穿过墙壁,或者产生抖动、碰撞不稳定等问题。
更推荐:
usingUnityEngine;publicclassRigidbodyMove:MonoBehaviour{publicfloatspeed=5f;privateRigidbodyrb;privatevoidAwake(){rb=GetComponent<Rigidbody>();}privatevoidFixedUpdate(){Vector3moveDistance=Vector3.forward*speed*Time.fixedDeltaTime;rb.MovePosition(rb.position+moveDistance);}}或者通过施加力:
privatevoidFixedUpdate(){rb.AddForce(Vector3.forward*force);}这里需要注意:在FixedUpdate中,通常使用:
Time.fixedDeltaTime而不是:
Time.deltaTime因为物理逻辑对应的是固定步长。
可以这样理解:
Update处理“玩家看到的每一帧”;FixedUpdate处理“物理世界推进的一小步”。
画面可能忽快忽慢,但物理世界尽量保持稳定节奏。
九、Time.maximumDeltaTime:防止卡顿让游戏“一步跨太远”
现实中,游戏不可能永远稳定运行。
当玩家切出窗口、电脑突然卡顿、加载资源、GC回收、场景中对象过多时,一帧可能从正常的:
0.016秒突然变成:
0.5秒甚至更久。
如果这时直接使用:
transform.position+=velocity*Time.deltaTime;那么物体会在一帧中移动非常远。
例如速度是每秒10米:
10 × 0.5 = 5米角色可能瞬移5米,子弹可能跨过墙壁,摄像机可能猛地跳动。
Unity通过:
Time.maximumDeltaTime限制单帧可用于模拟的最大时间跨度,避免某一次卡顿造成过于严重的逻辑跳跃。
当然,这不是解决性能问题的根本办法,但它像汽车的安全限速装置:在异常情况下,尽量避免游戏逻辑因为一个巨大的时间步长而失控。
十、协程中的时间:WaitForSeconds会受到暂停影响吗?
Unity协程经常用于延迟执行:
IEnumeratorAttackCooldown(){yieldreturnnewWaitForSeconds(2f);Debug.Log("两秒后执行");}这里的:
WaitForSeconds(2f)使用的是受Time.timeScale影响的游戏时间。
如果游戏进入慢动作:
Time.timeScale=0.5f;那么等待2秒游戏时间,现实中可能要等4秒。
如果游戏暂停:
Time.timeScale=0f;这个协程将停在那里,不会继续执行。
如果你希望它不受暂停影响,应使用:
WaitForSecondsRealtime例如:
IEnumeratorRealTimeDelay(){yieldreturnnewWaitForSecondsRealtime(2f);Debug.Log("无论游戏是否暂停,现实两秒后执行");}这适合用于:
- 暂停界面动画;
- 网络超时;
- 登录等待;
- 广告倒计时;
- Loading提示;
- 现实时间统计;
- 不应随慢动作变慢的系统逻辑。
十一、常用时间变量速查
Unity中常见时间变量可以这样理解:
| 变量 | 含义 | 是否受timeScale影响 | 常见用途 |
|---|---|---|---|
Time.deltaTime | 当前帧的游戏时间间隔 | 是 | 普通移动、旋转、倒计时 |
Time.unscaledDeltaTime | 当前帧的真实时间间隔 | 否 | 暂停UI、真实倒计时 |
Time.time | 游戏启动后的累计游戏时间 | 是 | 周期运动、游戏计时 |
Time.unscaledTime | 游戏启动后的累计真实时间 | 否 | 不受暂停影响的计时 |
Time.realtimeSinceStartup | 从启动开始的真实时间 | 否 | 启动统计、超时检测 |
Time.fixedDeltaTime | 每次物理模拟的固定时间步长 | 间接受影响 | Rigidbody、物理逻辑 |
Time.fixedTime | 累计物理时间 | 是 | 物理事件计时 |
Time.timeScale | 游戏时间流速 | — | 暂停、慢动作、加速 |
Time.maximumDeltaTime | 最大允许帧间隔 | — | 防止卡顿造成逻辑跳跃 |
十二、一个完整的昼夜系统示例
下面用一个简单例子,把时间系统串起来。假设游戏中的一天等于现实中的120秒。
usingUnityEngine;publicclassDayNightCycle:MonoBehaviour{[Header("太阳光")]publicLightsunLight;[Header("游戏中一天对应的现实秒数")]publicfloatdayDuration=120f;[Range(0f,1f)]publicfloatcurrentTimeOfDay=0.25f;privatevoidUpdate(){// 每帧推进一天中的时间比例currentTimeOfDay+=Time.deltaTime/dayDuration;// 超过1说明一天结束,重新回到0if(currentTimeOfDay>=1f){currentTimeOfDay-=1f;}// 一天对应360度旋转floatsunAngle=currentTimeOfDay*360f-90f;// 旋转方向光,模拟太阳升落sunLight.transform.rotation=Quaternion.Euler(sunAngle,170f,0f);// 根据太阳高度调整光照强度floatintensity=Mathf.Clamp01(Mathf.Sin(currentTimeOfDay*Mathf.PI));sunLight.intensity=intensity;}}这个系统中:
Time.deltaTime负责保证昼夜变化不受帧率影响。
如果游戏开启慢动作:
Time.timeScale=0.5f;昼夜循环也会变慢,因为它使用的是游戏时间。
如果希望昼夜系统无论暂停还是慢动作都继续流动,则改成:
currentTimeOfDay+=Time.unscaledDeltaTime/dayDuration;这就是“选择哪一种时间”的实际意义。
结语:时间不是一个变量,而是一套游戏规则
Unity中的时间系统,表面上是一组Time.xxx变量,实际上它决定了整个游戏世界以什么速度运转。
Time.deltaTime让移动、旋转和倒计时摆脱帧率束缚;Time.fixedDeltaTime让物理模拟拥有稳定节拍;Time.timeScale让暂停、慢动作和加速成为可能;Time.unscaledDeltaTime则让UI、网络和现实倒计时不受游戏暂停影响。
可以把Unity时间系统理解成一座城市中的多套钟表:
- 游戏角色看的是“游戏时钟”;
- 物理世界听的是“固定节拍器”;
- 暂停菜单看的是“现实世界时钟”;
- 网络系统可能听的是“服务器时钟”;
- 动画、技能、粒子和特效,则根据需求选择各自的时间来源。
真正优秀的游戏程序,不是所有逻辑都统一使用Time.deltaTime,而是清楚地知道:
哪些事情该跟着游戏暂停,哪些事情不该暂停;
哪些事情该跟着帧率,哪些事情必须跟着物理节拍;
哪些时间代表视觉效果,哪些时间代表真实系统规则。
当开发者能够准确选择时间来源时,角色移动会更平滑,物理会更稳定,暂停会更自然,慢动作会更震撼,整个游戏世界也会拥有属于自己的节奏与呼吸。