时间和帧率控制

脚本中的 Update 函数允许你定期地监听输入和其他事件,并执行适当的响应。例如,当按下『向前』键时,你可以会移动一个角色。处理类似这样的基于时间的行为时,一件重要的事情是,游戏的帧率不是恒定的,两次 Update 函数调用之间的时间长度也不是恒定的。

举个例子,假设有一个逐渐向前移动对象的任务,每桢移动一次。开始时,你可能只是使对象每桢移动一个固定距离:

  1. //C# script example
  2. using UnityEngine;
  3. using System.Collections;
  4. public class ExampleScript : MonoBehaviour {
  5. public float distancePerFrame;
  6. void Update() {
  7. transform.Translate(0, 0, distancePerFrame);
  8. }
  9. }
  1. //JS script example
  2. var distancePerFrame: float;
  3. function Update() {
  4. transform.Translate(0, 0, distancePerFrame);
  5. }

但是,因为桢时间不是恒定的,所以对象看起来将以不规则的速度移动。如果桢时间是 10 毫秒,那么该对象每秒向前移动 100 步 distancePerFrame。但是,如果桢时间增加到 25 毫秒(例如由于 CPU 负载),那么只会每秒向前移动 40 步,因此移动距离更短。解决方案是按照桢时间适配移动距离,可以从 Time.deltaTime 属性读取桢时间:

  1. //C# script example
  2. using UnityEngine;
  3. using System.Collections;
  4. public class ExampleScript : MonoBehaviour {
  5. public float distancePerSecond;
  6. void Update() {
  7. transform.Translate(0, 0, distancePerSecond * Time.deltaTime);
  8. }
  9. }
  1. //JS script example
  2. var distancePerSecond: float;
  3. function Update() {
  4. transform.Translate(0, 0, distancePerSecond * Time.deltaTime);
  5. }

注意,现在用 distancePerSecond 表示移动距离而不是 distancePerFrame。当帧率改变时,移动步长将相应地改变,对象的速度将是恒定的。

固定时间步长

与主桢更新不同,Unity 的物理系统以固定的时间步长运行,这对模拟的精确性和一致性非常重要。当物理更新开始时,Unity 在时间轴上设置一个固定时间步长(后将消失)的『警告器』,用来表示物理更新的结束时间。然后,物理更新开始执行计算,直到『警告器』消失。

通过时间管理器可以更改固定时间步长的大小,在脚本中,可以使用 Time.fixedDeltaTime 属性读取它的值。注意,较小的时间步长将导致更频繁的物理更新和更精确的模拟,代价是更大的 CPU 负载。除非对物理引擎有很高的要求,否则可能不需要更改默认的固定时间步长。

译注:时间管理器 Time Manager 的菜单位置 Edit > Project Settings > Time

最大时间步长

固定时间步长保证了物理模拟的实时精确,但是,当游戏使用了大量物理模拟时,可能会导致游戏帧率降低(由于有大量对象在运行)。频繁的物理更新会『挤压』主桢更新的时间,并且,如果有大量处理需要执行,那么一帧时间内可能发生多次物理更新。这时,当桢更新开始时,对象的位置和其他属性被冻结,图像可能与频繁的物理更新不同步。

尽管事实上只有有限的 CPU 功率可用,但是 Unity 提供了一个选项,可以让你有效地降低物理时间,从而让桢处理保持(恢复)同步。最大时间步长(在时间管理器中)限制了一帧中消耗在物理更新和 FixedUpdate 调用上的时间。如果桢更新消耗的时间超过了最大时间步长,物理引擎将暂停执行,从而让桢更新保持(赶上)同步。一旦桢更新完成,物理更新将恢复执行,就像自它停止后时间没有流逝一样。这样做的结果是,刚体将不会像通常那样完美地实时移动,而是稍微减慢。但是,物理时钟将会像正常移动一样跟踪它们。这种物理更新放慢通常不明显,是一种可接受的游戏性能平衡。

时间尺度

减慢游戏时间对于某些特效会很有用,例如『子弹时间』,可以使动画和脚本响应以降低后的速率运行。另外,有时可能想要完全冻结游戏时间,此时游戏被暂停。Unity 提供了一个时间尺寸 Time Scale 来控制游戏时间相对于真实时间的速度。如果时间尺寸设置为 1.0,那么游戏时间和真实时间一致。设置为 2.0,将使 Unity 中的游戏时间两倍于真实时间(例如,动作将加速);而设置为 0.5,将使游戏速度降低一半。设置为 0 将使事件完全『停止』。注意,时间尺寸不会真正减慢执行过程,而是简单地改变了 Time.deltaTime 和 Time.fixedDeltaTIme 返回给 Update 和 FixedUpdate 函数的时间步长。当游戏时间降低时,Update 函数的调用比正常情况更频繁,只是每桢返回的 deltaTime 被简单地降低了。其他脚本函数不会受到时间尺度的影响,例如,在游戏暂停时,显示可正常交互的 GUI。

译注:子弹时间(Bullet time)是一种使用在电影、电视广告或电脑游戏中,用计算机辅助的摄影技术模拟变速特效,例如强化的慢镜头、时间静止等效果。“子弹时间”效果因在好莱坞华纳兄弟电影公司出品的电影《骇客帝国》中大量使用名声大噪。其中男主角 Neo 仰身躲子弹的慢动作镜头堪称经典,“子弹时间”也因此得名。—— 百度百科

时间管理器提供了一个可全局设置时间尺度的属性,不过,通常是在脚本中使用 Time.timeScale 属性设置时间尺度:

  1. //C# script example
  2. using UnityEngine;
  3. using System.Collections;
  4. public class ExampleScript : MonoBehaviour {
  5. void Pause() {
  6. Time.timeScale = 0;
  7. }
  8. void Resume() {
  9. Time.timeScale = 1;
  10. }
  11. }
  1. //JS script example
  2. function Pause() {
  3. Time.timeScale = 0;
  4. }
  5. function Resume() {
  6. Time.timeScale = 1;
  7. }

捕获帧率

时间管理的一个特例是把游戏记录为视频。如果你尝试在正常的游戏过程中录制视,因为保存屏幕图像需要相当长的时间,游戏帧率通常会大大降低。这将导致视频不能真实反应游戏的性能。

幸运的是,Unity 提供了一个捕获帧率 Capture Framerate 属性来解决这个问题。当该属性的值被设置为非 0 时,游戏速度将降低,桢更新将以精确的时间间隔定期执行。两桢之间的间隔等于 1 / Time.captureFramerate,因此,如果该值被设置为 5.0,那么桢更新每 1/5 秒发生一次。随着帧率的有效降低,Update 函数有充足的时间来保存屏幕截图或执行其他行为。

  1. //C# script example
  2. using UnityEngine;
  3. using System.Collections;
  4. public class ExampleScript : MonoBehaviour {
  5. // Capture frames as a screenshot sequence. Images are
  6. // stored as PNG files in a folder - these can be combined into
  7. // a movie using image utility software (eg, QuickTime Pro).
  8. // The folder to contain our screenshots.
  9. // If the folder exists we will append numbers to create an empty folder.
  10. string folder = "ScreenshotFolder";
  11. int frameRate = 25;
  12. void Start () {
  13. // Set the playback framerate (real time will not relate to game time after this).
  14. Time.captureFramerate = frameRate;
  15. // Create the folder
  16. System.IO.Directory.CreateDirectory(folder);
  17. }
  18. void Update () {
  19. // Append filename to folder name (format is '0005 shot.png"')
  20. string name = string.Format("{0}/{1:D04} shot.png", folder, Time.frameCount );
  21. // Capture the screenshot to the specified file.
  22. Application.CaptureScreenshot(name);
  23. }
  24. }
  1. //JS script example
  2. // Capture frames as a screenshot sequence. Images are
  3. // stored as PNG files in a folder - these can be combined into
  4. // a movie using image utility software (eg, QuickTime Pro).
  5. // The folder to contain our screenshots.
  6. // If the folder exists we will append numbers to create an empty folder.
  7. var folder = "ScreenshotFolder";
  8. var frameRate = 25;
  9. function Start () {
  10. // Set the playback framerate (real time will not relate to game time after this).
  11. Time.captureFramerate = frameRate;
  12. // Create the folder
  13. System.IO.Directory.CreateDirectory(folder);
  14. }
  15. function Update () {
  16. // Append filename to folder name (format is '0005 shot.png"')
  17. var name = String.Format("{0}/{1:D04} shot.png", folder, Time.frameCount );
  18. // Capture the screenshot to the specified file.
  19. Application.CaptureScreenshot(name);
  20. }

使用这种技术录制的视频虽然通常看起来很不错,但是当游戏速度大幅降低时,游戏可能没法完了。为了保证充足的记录时间,并避免使播放器任务过度复杂化,你可能需要反复实验 Time.captureFramerate 的值。