时间和帧率控制

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

    1. var distancePerFrame: float;
    2. function Update() {
    3. transform.Translate(0, 0, distancePerFrame);
    4. }

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

    1. //JS script example
    2. var distancePerSecond: float;
    3. function Update() {
    4. transform.Translate(0, 0, distancePerSecond * Time.deltaTime);
    5. }

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

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

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

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

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

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

    1. function Pause() {
    2. Time.timeScale = 0;
    3. function Resume() {
    4. Time.timeScale = 1;
    5. }

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

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

    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. }

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