Unity引擎中的时间系统:让游戏世界“流动”起来的隐形时钟
如果说场景、模型、材质和光照构成了游戏世界的“身体”那么时间系统就是这个世界的心跳。没有时间角色不会奔跑子弹不会飞行昼夜不会交替技能冷却不会减少动画不会播放粒子不会飘散。一个游戏画面即使再精美也只是一张静止的图片。正是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.positionVector3.forward*speed;假设floatspeed5f;在60FPS下上面代码每秒执行60次物体每秒移动5 × 60 300单位而在30FPS下每秒只执行30次则移动5 × 30 150单位同一个角色在不同电脑上移动速度相差一倍这显然不可接受。正确写法应该是transform.positionVector3.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{publicfloatrotateSpeed90f;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{publicfloatamplitude0.5f;publicfloatfrequency2f;privateVector3startPosition;privatevoidStart(){startPositiontransform.position;}privatevoidUpdate(){floatoffsetYMathf.Sin(Time.time*frequency)*amplitude;transform.positionstartPositionVector3.up*offsetY;}}这里Mathf.Sin(Time.time*frequency)会在-1到1之间平滑变化形成自然的上下浮动。如果把Time.time看成一条持续流动的河流那么Sin函数就像在河面上不断起伏的波浪。时间往前流物体便不断上下摆动。不过需要注意Time.time会受到Time.timeScale影响。也就是说如果游戏暂停或慢动作Time.time会变慢甚至停止。五、Time.timeScale控制整个游戏世界的“时间流速”Time.timeScale是Unity时间系统中最有趣、也最强大的变量之一。默认情况下Time.timeScale1f;这表示游戏以正常速度运行。如果设置为Time.timeScale0.5f;游戏时间会变成半速。效果通常包括使用Time.deltaTime的角色移动变慢大多数动画变慢基于游戏时间的倒计时变慢物理模拟变慢粒子和部分特效可能变慢Time.time增长变慢。这就是慢动作效果的基础。例如子弹时间usingUnityEngine;publicclassSlowMotionController:MonoBehaviour{publicfloatslowMotionScale0.2f;privatevoidUpdate(){if(Input.GetKeyDown(KeyCode.Space)){Time.timeScaleslowMotionScale;}if(Input.GetKeyUp(KeyCode.Space)){Time.timeScale1f;}}}按住空格时世界进入慢动作松开后恢复正常。如果设置Time.timeScale0f;游戏时间将停止推进。这常用于暂停游戏publicvoidPauseGame(){Time.timeScale0f;}publicvoidResumeGame(){Time.timeScale1f;}但这里有一个非常重要的细节Time.timeScale 0并不意味着Unity完全停止运行。当时间缩放为0时Update()通常仍然会被调用UI仍然可以响应鼠标和键盘输入仍然可以读取脚本逻辑仍然可以执行只是Time.deltaTime会变成0Time.time不再增长FixedUpdate()通常不再正常推进。这意味着暂停菜单仍可以正常点击因为UI脚本的Update还在运行但角色移动代码由于乘了Time.deltaTime所以移动距离变成0。这正是Unity暂停机制灵活的地方。六、暂停时为什么UI动画和倒计时也停了假设你有一个暂停界面希望它弹出来时缓慢放大panel.localScaleVector3.Lerp(panel.localScale,Vector3.one,Time.deltaTime*5f);当你设置Time.timeScale0f;时Time.deltaTime也变成0于是Time.deltaTime*5f0UI完全不会继续缩放。这时应该使用Time.unscaledDeltaTime它表示不受Time.timeScale影响的真实帧间隔。修改后panel.localScaleVector3.Lerp(panel.localScale,Vector3.one,Time.unscaledDeltaTime*5f);即使游戏暂停UI仍可以播放动画。同理暂停菜单中的倒计时、网络连接超时、广告计时、Loading界面动画等很多都应该使用不受缩放影响的时间。Unity中常见的非缩放时间包括Time.unscaledDeltaTime Time.unscaledTime Time.realtimeSinceStartup它们可以理解为“现实世界的时间”。例如floatelapsedTime.realtimeSinceStartup;即使游戏处于暂停、慢动作状态这个时间仍然继续增长。七、FixedUpdate与fixedDeltaTime物理世界有自己的节拍器如果说Update是跟随画面帧率变化的节奏那么FixedUpdate就是一台固定节拍的节拍器。Unity物理系统例如Rigidbody移动重力碰撞摩擦弹跳力的施加Joint关节车辆轮子角色物理交互通常依赖FixedUpdate()。默认情况下Time.fixedDeltaTime0.02f;这意味着物理系统默认每隔0.02秒模拟一次即每秒50次。privatevoidFixedUpdate(){// 固定时间间隔执行}和Update不同FixedUpdate不一定每帧只执行一次。例如画面运行在120FPS时渲染帧很多但物理每秒仍然只模拟50次因此某些画面帧中可能没有FixedUpdate。反过来如果画面卡顿一帧用了很久Unity为了追赶物理时间可能在一帧内连续调用多次FixedUpdate。例如某一帧卡了0.1秒而物理间隔为0.02秒那么Unity可能需要补做约5次物理更新。这也是为什么在FixedUpdate中不能把它简单理解为“一帧一次”。八、为什么刚体要在FixedUpdate中移动假设有一个带Rigidbody的球体。不推荐这样写privatevoidUpdate(){transform.positionVector3.forward*speed*Time.deltaTime;}因为这样是直接修改Transform位置物理系统来不及正确参与碰撞计算。高速移动时物体可能直接穿过墙壁或者产生抖动、碰撞不稳定等问题。更推荐usingUnityEngine;publicclassRigidbodyMove:MonoBehaviour{publicfloatspeed5f;privateRigidbodyrb;privatevoidAwake(){rbGetComponentRigidbody();}privatevoidFixedUpdate(){Vector3moveDistanceVector3.forward*speed*Time.fixedDeltaTime;rb.MovePosition(rb.positionmoveDistance);}}或者通过施加力privatevoidFixedUpdate(){rb.AddForce(Vector3.forward*force);}这里需要注意在FixedUpdate中通常使用Time.fixedDeltaTime而不是Time.deltaTime因为物理逻辑对应的是固定步长。可以这样理解Update处理“玩家看到的每一帧”FixedUpdate处理“物理世界推进的一小步”。画面可能忽快忽慢但物理世界尽量保持稳定节奏。九、Time.maximumDeltaTime防止卡顿让游戏“一步跨太远”现实中游戏不可能永远稳定运行。当玩家切出窗口、电脑突然卡顿、加载资源、GC回收、场景中对象过多时一帧可能从正常的0.016秒突然变成0.5秒甚至更久。如果这时直接使用transform.positionvelocity*Time.deltaTime;那么物体会在一帧中移动非常远。例如速度是每秒10米10 × 0.5 5米角色可能瞬移5米子弹可能跨过墙壁摄像机可能猛地跳动。Unity通过Time.maximumDeltaTime限制单帧可用于模拟的最大时间跨度避免某一次卡顿造成过于严重的逻辑跳跃。当然这不是解决性能问题的根本办法但它像汽车的安全限速装置在异常情况下尽量避免游戏逻辑因为一个巨大的时间步长而失控。十、协程中的时间WaitForSeconds会受到暂停影响吗Unity协程经常用于延迟执行IEnumeratorAttackCooldown(){yieldreturnnewWaitForSeconds(2f);Debug.Log(两秒后执行);}这里的WaitForSeconds(2f)使用的是受Time.timeScale影响的游戏时间。如果游戏进入慢动作Time.timeScale0.5f;那么等待2秒游戏时间现实中可能要等4秒。如果游戏暂停Time.timeScale0f;这个协程将停在那里不会继续执行。如果你希望它不受暂停影响应使用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(游戏中一天对应的现实秒数)]publicfloatdayDuration120f;[Range(0f,1f)]publicfloatcurrentTimeOfDay0.25f;privatevoidUpdate(){// 每帧推进一天中的时间比例currentTimeOfDayTime.deltaTime/dayDuration;// 超过1说明一天结束重新回到0if(currentTimeOfDay1f){currentTimeOfDay-1f;}// 一天对应360度旋转floatsunAnglecurrentTimeOfDay*360f-90f;// 旋转方向光模拟太阳升落sunLight.transform.rotationQuaternion.Euler(sunAngle,170f,0f);// 根据太阳高度调整光照强度floatintensityMathf.Clamp01(Mathf.Sin(currentTimeOfDay*Mathf.PI));sunLight.intensityintensity;}}这个系统中Time.deltaTime负责保证昼夜变化不受帧率影响。如果游戏开启慢动作Time.timeScale0.5f;昼夜循环也会变慢因为它使用的是游戏时间。如果希望昼夜系统无论暂停还是慢动作都继续流动则改成currentTimeOfDayTime.unscaledDeltaTime/dayDuration;这就是“选择哪一种时间”的实际意义。结语时间不是一个变量而是一套游戏规则Unity中的时间系统表面上是一组Time.xxx变量实际上它决定了整个游戏世界以什么速度运转。Time.deltaTime让移动、旋转和倒计时摆脱帧率束缚Time.fixedDeltaTime让物理模拟拥有稳定节拍Time.timeScale让暂停、慢动作和加速成为可能Time.unscaledDeltaTime则让UI、网络和现实倒计时不受游戏暂停影响。可以把Unity时间系统理解成一座城市中的多套钟表游戏角色看的是“游戏时钟”物理世界听的是“固定节拍器”暂停菜单看的是“现实世界时钟”网络系统可能听的是“服务器时钟”动画、技能、粒子和特效则根据需求选择各自的时间来源。真正优秀的游戏程序不是所有逻辑都统一使用Time.deltaTime而是清楚地知道哪些事情该跟着游戏暂停哪些事情不该暂停哪些事情该跟着帧率哪些事情必须跟着物理节拍哪些时间代表视觉效果哪些时间代表真实系统规则。当开发者能够准确选择时间来源时角色移动会更平滑物理会更稳定暂停会更自然慢动作会更震撼整个游戏世界也会拥有属于自己的节奏与呼吸。