1. 项目概述:为什么Unity开发者必须吃透Time类?
如果你刚开始接触Unity,或者已经写过一些简单的移动、旋转脚本,那你肯定用过Time.deltaTime。但你可能也遇到过一些“灵异事件”:为什么我的物体移动时快时慢?为什么物理模拟在某些设备上会抽搐?为什么暂停游戏后,UI动画还在动?这些问题的根源,十有八九都出在对Unity的Time类理解不透彻上。
Time类远不止一个简单的计时器。它是Unity游戏世界的心脏,负责协调游戏逻辑、物理模拟、动画更新与真实世界时间流逝之间的关系。理解它,是写出稳定、流畅、跨平台表现一致的游戏代码的基石。今天,我们就抛开官方文档那冰冷的列表,从一个一线开发者的视角,彻底拆解Time类的每一个核心属性,并结合那些热搜词里提到的“坑”(比如性能优化、面试题、打包问题),告诉你它们在实际项目中到底怎么用,以及怎么避坑。
2. Time类核心属性深度解析与实战场景
Unity的Time类提供了近20个静态属性,乍一看令人眼花缭乱。我们可以把它们分为几个核心家族来理解:增量时间族、累计时间族、时间缩放族和特殊用途族。记住这个分类,学习起来会清晰很多。
2.1 增量时间族:驱动每帧运动的核心
增量时间,顾名思义,就是上一帧到这一帧所经过的时间。这是实现与帧率无关的平滑运动的关键。
1. Time.deltaTime: 最常用,也最容易用错这是你实现物体移动、旋转、插值等操作时最应该使用的属性。它的值是上一帧完成到当前帧开始所经过的秒数。
// 标准用法:每秒移动10个单位 transform.Translate(Vector3.forward * 10f * Time.deltaTime);为什么必须乘以Time.deltaTime?假设你的游戏在60FPS的机器上运行,deltaTime大约是0.0167秒。在30FPS的机器上,deltaTime大约是0.0333秒。通过乘以deltaTime,你实际上是在说“每秒钟移动10个单位”。那么:
- 在60FPS下,每帧移动
10 * 0.0167 ≈ 0.167个单位。 - 在30FPS下,每帧移动
10 * 0.0333 ≈ 0.333个单位。 虽然单帧移动距离不同,但经过一秒(60帧或30帧)后,总移动距离都是10个单位。这就实现了帧率无关的运动。如果不乘,物体在高速设备上会移动得飞快,在低速设备上则慢如蜗牛。
实操心得:在
Update()方法中进行任何与位置、旋转、进度(Lerp的t值)相关的计算,99%的情况都应该使用Time.deltaTime。这是新手和老手最显著的区别之一。
2. Time.unscaledDeltaTime: 不受时间缩放影响的“真实”增量这个属性是deltaTime的“老实人”兄弟。它不受Time.timeScale的影响,总是返回基于真实时间的帧间隔。
// 即使游戏暂停(timeScale = 0),这个计时器也会正常累加 float pauseMenuTimer += Time.unscaledDeltaTime;典型应用场景:
- 游戏暂停菜单的动画:即使游戏世界暂停了,菜单的弹出、按钮的高亮动画仍需流畅播放。
- UI特效:比如一个不受游戏内“子弹时间”影响的UI粒子效果。
- 网络心跳检测:需要基于真实时间判断超时,而不是游戏内变慢的时间。
3. Time.smoothDeltaTime: 平滑但滞后的增量Unity会计算一个过去几帧deltaTime的平均值,使其更加平滑。这能减少因单帧卡顿导致的运动抖动。但请注意,平滑意味着延迟。它反映的不是最新鲜的时间间隔,而是一个历史平均值。
// 适用于对瞬时波动不敏感,但要求极度平滑的运动,如摄像机跟随 cameraFollowPosition = Vector3.Lerp(cameraFollowPosition, targetPosition, smoothSpeed * Time.smoothDeltaTime);使用建议:在摄像机跟随、某些平滑的UI移动中可以考虑使用。但对于需要快速响应的玩家控制角色移动,使用标准的deltaTime更合适。
4. Time.fixedDeltaTime: 物理世界的固定心跳这是物理更新(FixedUpdate)的固定时间步长,默认是0.02秒(即50Hz)。FixedUpdate的调用频率与帧率无关,Unity会根据需要在一个渲染帧内调用多次FixedUpdate来“追赶”真实时间。
// 在FixedUpdate中,使用fixedDeltaTime(虽然在此方法内deltaTime的值也等于fixedDeltaTime) void FixedUpdate() { rigidbody.AddForce(Vector3.up * 10f * Time.fixedDeltaTime); }核心要理解:FixedUpdate是物理模拟的圣地。所有对Rigidbody的操作、物理力的施加,都应放在这里,并使用fixedDeltaTime或Time.deltaTime(在FixedUpdate内两者等价)。如果你在Update里修改Rigidbody的velocity或AddForce,可能会引发不可预知的物理抖动,这也是很多“物理抽搐”问题的根源。
5. Time.maximumDeltaTime: 应对卡顿的“安全阀”这是一个非常重要的保护性参数。当某一帧因为复杂计算、加载等原因耗时极长(比如deltaTime达到了0.5秒),如果把这个巨大的值直接用于物理和逻辑计算,可能会导致物体“穿越”墙壁、动画瞬间跳变等灾难性后果。maximumDeltaTime(默认0.333秒)就是deltaTime的上限。当一帧耗时超过此值,Unity会将其钳制在这个最大值内。超出的时间会被“丢弃”吗?不,物理引擎会通过增加单帧内FixedUpdate的调用次数来尝试弥补,但这可能导致CPU峰值。这也是为什么性能优化(热搜词:unity性能优化)如此重要——避免触发这个安全阀,保持游戏体验的连贯性。
2.2 累计时间族:游戏世界的时钟
这类属性告诉你游戏已经运行了多久。
1. Time.time: 游戏世界的标准时间从游戏开始到现在所经过的秒数,受Time.timeScale影响。当timeScale为0(暂停)时,Time.time停止增长;当timeScale为2时,它流逝的速度是真实时间的两倍。
// 计时3秒后触发事件 if (Time.time - startTime > 3f) { DoSomething(); }2. Time.unscaledTime: 墙上的挂钟从游戏开始到现在所经过的真实秒数,完全不受Time.timeScale影响。即使游戏暂停,它也在默默计时。
// 记录玩家在游戏中实际度过了多少时间(包括暂停时间) totalRealPlayTime = Time.unscaledTime;3. Time.realtimeSinceStartup: 最“真实”的时钟这个属性与Time.unscaledTime类似,但有一个关键区别:它计算的是从Unity播放器启动(或应用启动)到现在所经过的真实秒数,即使在编辑器暂停时也会继续增加。而Time.time和Time.unscaledTime在编辑器暂停时会停止。
// 测量一段代码执行的精确真实耗时 float start = Time.realtimeSinceStartup; PerformComplexCalculation(); float duration = Time.realtimeSinceStartup - start; Debug.Log($"计算耗时:{duration} 秒");注意事项:
realtimeSinceStartup在游戏刚开始的几帧可能不稳定,且其值在应用长时间运行后可能会非常大。通常用于短时间测量,而非长时间计时。
4. Time.fixedTime 与 Time.timeSinceLevelLoad
Time.fixedTime: 最后一次FixedUpdate开始时的Time.time值。在FixedUpdate中使用它,可以获取与物理步调一致的时间戳。Time.timeSinceLevelLoad: 当前关卡加载完成后所经过的时间(受timeScale影响)。用于关卡内的计时非常方便。
2.3 时间缩放族:掌控游戏节奏的魔法棒
Time.timeScale: 游戏世界的变速齿轮这是Time类中最强大、也最危险的属性之一。它缩放的是Time.time和Time.deltaTime的流逝速度。
timeScale = 1.0f: 正常速度。timeScale = 0.0f: 完全暂停。Time.time停止,Time.deltaTime为0。所有基于deltaTime的运动和动画都会停止。但Update和FixedUpdate仍然会每帧调用!只是deltaTime为0。timeScale = 0.5f: 慢动作,速度减半。timeScale = 2.0f: 快进,速度加倍。
实现“子弹时间”效果:
public void EnterBulletTime() { Time.timeScale = 0.3f; // 世界变慢 // 同时,可能需要提高FixedUpdate的频率,让物理模拟在慢速下依然平滑 Time.fixedDeltaTime = 0.02f * Time.timeScale; // 保持物理迭代次数 }重大陷阱与解决方案:
- UI动画暂停:当你设置
Time.timeScale = 0暂停游戏时,所有使用Time.deltaTime的UI动画也会停止。解决方案是UI动画使用Time.unscaledDeltaTime。 - 协程(Coroutine)中的 WaitForSeconds:
WaitForSeconds受timeScale影响。如果你在暂停时启动一个WaitForSeconds(2)的协程,它会一直等待。需要使用WaitForSecondsRealtime。 - 物理模拟异常:大幅改变
timeScale可能导致物理不稳定。一个最佳实践是同时按比例调整Time.fixedDeltaTime,以保持物理更新的实际频率相对稳定。
2.4 特殊用途与高级属性
1. Time.frameCount: 帧号从游戏开始到现在渲染的总帧数。常用于需要每N帧执行一次的操作,性能开销远小于基于时间的判断。
// 每10帧执行一次昂贵的操作,而不是每0.1秒 if (Time.frameCount % 10 == 0) { UpdateExpensiveDebugInfo(); }2. Time.captureDeltaTime / captureFramerate: 制作平滑慢镜头或定格动画这两个属性用于手动控制时间,通常用于录制固定帧率的视频或制作“矩阵”式慢镜头。
// 设置以每秒60帧的速度“模拟”时间,用于录制 Time.captureFramerate = 60; // 此时,Time.deltaTime 会被锁定为 1/60 ≈ 0.0167 // Update和FixedUpdate仍被调用,但游戏时间被精确控制。注意:设置此属性后,
Time.deltaTime将变得固定,Time.timeScale失效。完成录制后务必将其设回0。
3. Time.inFixedTimeStep: 判断当前是否在物理更新中这是一个只读属性,在FixedUpdate及其调用的函数中返回true,在Update中返回false。这在一些共享工具函数中非常有用,可以判断当前上下文并选择合适的时间增量。
public float GetAppropriateDeltaTime() { return Time.inFixedTimeStep ? Time.fixedDeltaTime : Time.deltaTime; }3. 核心应用场景与实战代码剖析
理解了属性,我们来看看如何把它们组合起来,解决实际开发中的经典问题。
3.1 场景一:实现一个稳健的游戏计时器
游戏内计时器(如技能CD、倒计时)需要能在游戏暂停时停止,在慢动作时变慢。
public class GameTimer : MonoBehaviour { private float _duration; // 总时长 private float _remainingTime; // 剩余时间 private bool _isRunning; public void StartTimer(float duration) { _duration = duration; _remainingTime = duration; _isRunning = true; } void Update() { if (!_isRunning) return; // 使用受timeScale影响的deltaTime,这样计时器会随游戏世界一起暂停或变慢 _remainingTime -= Time.deltaTime; if (_remainingTime <= 0f) { _remainingTime = 0f; _isRunning = false; OnTimerComplete?.Invoke(); } } // 获取剩余时间的百分比(0到1),用于UI进度条 public float GetNormalizedTime() { return Mathf.Clamp01(_remainingTime / _duration); } }如果要一个在游戏暂停时也能走的“真实世界”计时器(比如网络连接超时),只需把Time.deltaTime换成Time.unscaledDeltaTime。
3.2 场景二:制作一个暂停菜单,并让菜单动画不受暂停影响
这是面试常考题,也是实战高频需求。
public class PauseMenuManager : MonoBehaviour { public GameObject pauseMenuPanel; public Animator menuAnimator; // 菜单自身的Animator void Update() { if (Input.GetKeyDown(KeyCode.Escape)) { TogglePause(); } } public void TogglePause() { bool isPausing = Time.timeScale > 0f; if (isPausing) { // 暂停游戏世界 Time.timeScale = 0f; // 显示菜单 pauseMenuPanel.SetActive(true); // 播放菜单入场动画。Animator默认使用Unscaled Time,所以动画不受影响。 // 如果Animator被误设置为Scaled Time,需在Animator组件或代码中设置: // menuAnimator.updateMode = AnimatorUpdateMode.UnscaledTime; menuAnimator.Play("MenuFadeIn"); } else { // 恢复游戏世界 Time.timeScale = 1f; // 播放菜单退场动画 menuAnimator.Play("MenuFadeOut"); // 动画播放完毕后,在动画事件中禁用面板,或使用协程等待 StartCoroutine(DisableMenuAfterAnimation(0.5f)); } } IEnumerator DisableMenuAfterAnimation(float delay) { // 使用WaitForSecondsRealtime,因为timeScale=0时WaitForSeconds会永远等待 yield return new WaitForSecondsRealtime(delay); pauseMenuPanel.SetActive(false); } }3.3 场景三:平滑摄像机跟随与帧率无关的动画
结合deltaTime和smoothDeltaTime,实现高级效果。
public class SmoothCameraFollow : MonoBehaviour { public Transform target; public float followSpeed = 5f; public float rotationSpeed = 3f; public bool useSmoothDelta = false; private Vector3 _velocity = Vector3.zero; void LateUpdate() { if (target == null) return; // 位置跟随:使用SmoothDamp,它内部已经处理了时间。我们只需传入正确的deltaTime。 float delta = useSmoothDelta ? Time.smoothDeltaTime : Time.deltaTime; transform.position = Vector3.SmoothDamp(transform.position, target.position, ref _velocity, 0.3f, Mathf.Infinity, delta); // 旋转跟随:使用Lerp,需要手动乘以deltaTime Quaternion targetRotation = Quaternion.LookRotation(target.position - transform.position); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * delta); } }何时用smoothDeltaTime?对于摄像机这种对瞬时卡顿敏感,追求丝滑而非即时响应的对象,使用smoothDeltaTime可以有效消除因单帧耗时波动带来的轻微抖动。但对于玩家角色控制,必须用deltaTime以保证操作的跟手性。
4. 性能优化、疑难杂症与面试题精讲
很多热搜词反映的问题,都与Time类的误用或深层机制有关。
4.1 性能优化:Time.maximumDeltaTime 与 Time.maximumParticleDeltaTime
Time.maximumDeltaTime: 如前所述,这是最后防线。但优化目标是不让它生效。你需要通过性能剖析器(Profiler)找到卡顿元凶:可能是复杂的AI计算、过多的DrawCall、同步加载大型资源、复杂的物理模拟(热搜词:simulation is not running in real time due to excessive cpu)等。针对性地优化,让每一帧的deltaTime都远低于这个阈值(比如保持在0.0167ms以下以实现60FPS)。Time.maximumParticleDeltaTime: 这是粒子系统的专用“安全阀”。当一帧时间过长时,Unity会将粒子更新拆分成多个子步骤,避免因单次更新量过大导致粒子“爆炸”或位置异常。通常不需要手动修改,除非你遇到非常特殊的粒子表现问题。
4.2 疑难杂症排查清单
问题1:物体移动一卡一卡的,尤其在低帧率设备上。
- 可能原因1:在
Update中直接修改Rigidbody.position或Transform.position,与物理引擎的FixedUpdate更新冲突。解决方案:对物理对象,所有位置、力的改变都应在FixedUpdate中进行,并使用Rigidbody.MovePosition或AddForce。 - 可能原因2:运动计算没有乘以
Time.deltaTime,导致帧率直接影响速度。 - 可能原因3:使用了
Time.deltaTime,但运动计算本身放在了一个帧率不稳定的地方(例如,在协程中每帧yield return null但进行了复杂计算,导致等待时间不稳定)。确保运动逻辑在稳定的Update循环中。
问题2:游戏暂停后,某些特效或UI元素也停了。
- 排查:检查这些动画或脚本是否使用了
Time.deltaTime或受timeScale影响的Time.time。将其替换为Time.unscaledDeltaTime和Time.unscaledTime。 - 对于Animator:检查组件上的
Update Mode,设置为Unscaled Time。
问题3:网络同步或日志记录的时间戳对不上。
- 排查:你是否错误地使用了
Time.time?Time.time会受timeScale和游戏启动时机影响。对于需要和服务器或真实世界对齐的时间戳,应使用Time.realtimeSinceStartup或System.DateTime.UtcNow。
问题4:从AssetStore下载的插件在Time.timeScale = 0时行为异常。
- 原因:插件内部可能硬编码了
Time.deltaTime。解决方案:查看插件源码(如果有),或联系作者。有时可以通过在暂停时禁用该插件组件来规避。
4.3 高频面试题深度剖析
面试题1:Time.deltaTime和Time.fixedDeltaTime有什么区别?分别在什么情况下使用?
- 区别:
Time.deltaTime:上一帧到当前帧的真实时间间隔,在Update中使用,值每帧变化。Time.fixedDeltaTime:固定的物理更新时间间隔,在FixedUpdate中使用,值恒定(默认0.02s)。
- 使用场景:
Time.deltaTime:所有与渲染帧同步的逻辑,如玩家输入处理、非物理动画、游戏状态更新、UI动画(若需受timeScale影响)。Time.fixedDeltaTime:所有与物理模拟相关的逻辑,如力的施加、速度的改变、物理关节的控制。在FixedUpdate方法内,Time.deltaTime的值就等于Time.fixedDeltaTime。
面试题2:如何实现游戏的暂停功能?需要注意哪些坑?
- 核心实现:设置
Time.timeScale = 0。 - 需要注意的坑:
- UI与菜单:暂停菜单的动画需使用
Unscaled Time模式或Time.unscaledDeltaTime。 - 协程:暂停期间启动的、基于
WaitForSeconds的协程会卡住,需改用WaitForSecondsRealtime。 - 音效:背景音乐可能也需要暂停。
AudioSource有Pause()方法,但注意有些UI音效可能不需要暂停。 - 粒子系统:检查粒子系统的
Simulation Speed是否受timeScale影响,可能需要单独处理。 - 输入:暂停后,通常需要屏蔽或重定向玩家对游戏世界的输入,但保留对菜单的输入。
- UI与菜单:暂停菜单的动画需使用
面试题3:Time.realtimeSinceStartup和Time.unscaledTime有什么不同?
Time.unscaledTime:游戏启动后,不受timeScale影响的游戏时间。在编辑器暂停时会停止。Time.realtimeSinceStartup:应用启动后,绝对的、真实的挂钟时间。在编辑器暂停时也不会停止。它基于系统时间,更适用于性能测量和需要与外部真实时间对齐的场景。
面试题4:解释一下Time.captureFramerate的作用。
- 作用:用于离线渲染或录制固定帧率视频。设置后,Unity会尝试以固定帧率运行游戏,每帧的
Time.deltaTime被锁定为1/captureFramerate,Time.timeScale失效。 - 原理:Unity通过在两帧之间插入精确的等待来实现固定帧间隔。
- 注意:这会导致游戏运行速度变慢(因为要等待),仅用于录制目的,正常游戏运行时必须将其设为0。
5. 进阶话题:Time与Unity其他系统的协作
Time类不是孤立的,它与Unity的多个核心系统紧密耦合。
5.1 Time与物理系统(Physics)
这是最深度的耦合。FixedUpdate的调用频率由Time.fixedDeltaTime决定。Unity内部有一个“时间累积器”,记录自上一帧以来过去的时间。每一帧,累积器增加Time.deltaTime。只要累积器大于等于fixedDeltaTime,Unity就调用一次FixedUpdate,并从累积器中减去一个fixedDeltaTime。这个过程会重复,直到累积器小于fixedDeltaTime。这确保了物理模拟的稳定性,不受渲染帧率波动的影响。
这就是为什么在FixedUpdate中执行物理操作是至关重要的。如果你在Update中频繁修改刚体的速度或位置,你是在以不可预测的帧率干扰一个以固定频率运行的系统,必然导致抖动和不可靠的物理行为。
5.2 Time与动画系统(Animation/Animator)
- Legacy Animation:动画播放速度默认受
Time.timeScale影响。 - Animator:通过
Animator.updateMode控制:Normal:受Time.timeScale影响。AnimatePhysics:与物理更新同步(在FixedUpdate后更新),受Time.timeScale影响。UnscaledTime:不受Time.timeScale影响,使用Time.unscaledDeltaTime。 制作暂停菜单时,将菜单Animator设为UnscaledTime是标准做法。
5.3 Time与粒子系统(Particle System)
粒子系统的Simulation Speed属性缩放的是粒子自身的模拟时间。它默认会受到Time.timeScale的乘数影响。这意味着当timeScale = 0时,粒子也会暂停。如果你希望游戏暂停时某些背景粒子(如星空、氛围尘埃)继续运动,需要单独设置这些粒子系统的Simulation Speed,或使用脚本控制。
5.4 Time与协程(Coroutines)
这是最容易踩坑的地方。
yield return new WaitForSeconds(2);: 等待2秒的游戏时间,受Time.timeScale影响。yield return new WaitForSecondsRealtime(2);: 等待2秒的真实时间,不受Time.timeScale影响。yield return null;/yield return 0;: 等待下一帧。恢复执行的时间点取决于timeScale。即使游戏暂停(timeScale=0),协程也会在下一帧被唤醒,只是Time.deltaTime为0。yield return new WaitForFixedUpdate();: 等待下一次FixedUpdate调用。
关键点:在实现与时间相关的延迟、计时功能时,务必根据需求选择正确的等待指令。游戏逻辑计时用WaitForSeconds,真实世界计时(如广告倒计时、网络超时)用WaitForSecondsRealtime。
6. 实战框架:构建一个高鲁棒性的时间管理模块
对于中型以上项目,我强烈建议抽象一个时间管理模块(如TimeManager),而不是在代码中到处直接调用Time.timeScale。这提供了更好的控制力和可维护性。
using System; using UnityEngine; public class TimeManager : MonoBehaviour { public static TimeManager Instance { get; private set; } // 全局时间缩放 public float GlobalTimeScale { get => Time.timeScale; set { Time.timeScale = value; OnGlobalTimeScaleChanged?.Invoke(value); } } // 局部时间缩放(例如,仅为某个角色或特效设置慢动作) private Dictionary<object, float> _localTimeScales = new Dictionary<object, float>(); private float _effectiveTimeScale = 1f; public event Action<float> OnGlobalTimeScaleChanged; void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } // 设置局部时间缩放 public void SetLocalTimeScale(object key, float scale) { _localTimeScales[key] = scale; UpdateEffectiveTimeScale(); } public void RemoveLocalTimeScale(object key) { if (_localTimeScales.Remove(key)) { UpdateEffectiveTimeScale(); } } private void UpdateEffectiveTimeScale() { float product = 1f; foreach (var scale in _localTimeScales.Values) { product *= scale; } _effectiveTimeScale = product; // 这里可以驱动一个自定义的更新循环,用于受局部时间缩放影响的逻辑 // 例如:CustomUpdate(_effectiveTimeScale * Time.unscaledDeltaTime); } // 获取适用于特定上下文的时间增量(考虑了局部缩放) public float GetDeltaTime(object context = null) { float baseDelta = Time.inFixedTimeStep ? Time.fixedDeltaTime : Time.deltaTime; if (context != null && _localTimeScales.TryGetValue(context, out float localScale)) { return baseDelta * localScale; } return baseDelta; } // 暂停游戏(保留当前timeScale以便恢复) private float _pausedTimeScale; public void Pause() { if (GlobalTimeScale > 0) { _pausedTimeScale = GlobalTimeScale; GlobalTimeScale = 0f; } } public void Resume() { GlobalTimeScale = _pausedTimeScale; } }这个模块提供了:
- 单例访问:全局可访问。
- 封装的时间缩放控制:通过属性安全地修改
Time.timeScale,并触发事件。 - 局部时间缩放:实现“子弹时间”仅影响特定对象,而不暂停整个游戏世界。
- 安全的暂停/恢复:记住暂停前的速度。
- 智能的DeltaTime获取:根据上下文返回可能经过局部缩放的时间增量。
在你的游戏对象脚本中,可以这样使用:
void Update() { // 使用TimeManager获取可能被局部缩放的时间增量 float delta = TimeManager.Instance.GetDeltaTime(this); // this 作为上下文key transform.Translate(Vector3.forward * speed * delta); }7. 总结与最终建议
Unity的Time类是你控制游戏节奏和确保跨平台一致性的瑞士军刀。记住以下核心要点,能帮你避开绝大多数陷阱:
- 运动与
deltaTime是铁律:在Update中做任何与“每秒”相关的操作,都必须乘以Time.deltaTime。 - 物理属于
FixedUpdate:对刚体的操作务必放在FixedUpdate里,并使用Time.fixedDeltaTime(或Time.deltaTime)。 - 暂停时想想
unscaled:当Time.timeScale = 0时,所有需要继续运行的东西(UI、菜单动画、真实计时器)都应切换到Time.unscaledDeltaTime和Time.unscaledTime。 - 协程等待要选对:游戏内延迟用
WaitForSeconds,真实世界延迟用WaitForSecondsRealtime。 - 理解时间缩放的影响范围:
Time.timeScale影响Time.time/deltaTime、受缩放影响的动画、粒子系统速度、以及基于WaitForSeconds的协程。它不影响Update/FixedUpdate的调用频率,不影响使用unscaled时间属性的逻辑,也不影响WaitForSecondsRealtime。 - 性能是底线:时刻关注
Time.deltaTime的波动,利用性能剖析工具,避免其触及Time.maximumDeltaTime的红线,这是保证游戏流畅体验的根本。
最后,最好的学习方式是在项目中实践并遇到问题。当你下次遇到物体移动飘忽不定、暂停功能失灵或者慢动作效果怪异时,第一个就应该回来检查你对Time类的使用是否正确。把这套机制吃透,你的Unity代码质量会立刻上一个台阶。