神秘的 Time

顾名思义,Time 就是用来管理 Unity 引擎时间的一个管理类。Time 是一个从代码定义上来看非常简单的类,只有100行左右,而且全都是一些属性的定义。但是,这个 Time 类真的非常神秘,需要认真理解才能真正搞懂它内部的机制。
由于 Time 用来管理 Unity 引擎时间,而时间又和 Unity 的事件循环机制密切相关,所以,我们通过观察 Time 是如何影响 Unity 事件循环的,来进一步理解这个神秘的类。
用 DLL 反编译工具,我们可以很轻松的得到 Time 类的实现代码,如下:

  /// <summary>
  ///   <para>The interface to get time information from Unity.</para>
  /// </summary>
  public class Time
  {
    /// <summary>
    ///   <para>The time at the beginning of this frame (Read Only). This is the time in seconds since the start of the game.</para>
    /// </summary>
    public static extern float time { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The time this frame has started (Read Only). This is the time in seconds since the last level has been loaded.</para>
    /// </summary>
    public static extern float timeSinceLevelLoad { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The time in seconds it took to complete the last frame (Read Only).</para>
    /// </summary>
    public static extern float deltaTime { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The time the latest MonoBehaviour.FixedUpdate has started (Read Only). This is the time in seconds since the start of the game.</para>
    /// </summary>
    public static extern float fixedTime { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The timeScale-independant time for this frame (Read Only). This is the time in seconds since the start of the game.</para>
    /// </summary>
    public static extern float unscaledTime { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The TimeScale-independant time the latest MonoBehaviour.FixedUpdate has started (Read Only). This is the time in seconds since the start of the game.</para>
    /// </summary>
    public static extern float fixedUnscaledTime { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The timeScale-independent interval in seconds from the last frame to the current one (Read Only).</para>
    /// </summary>
    public static extern float unscaledDeltaTime { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The timeScale-independent interval in seconds from the last fixed frame to the current one (Read Only).</para>
    /// </summary>
    public static extern float fixedUnscaledDeltaTime { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The interval in seconds at which physics and other fixed frame rate updates (like MonoBehaviour's MonoBehaviour.FixedUpdate) are performed.</para>
    /// </summary>
    public static extern float fixedDeltaTime { [MethodImpl(MethodImplOptions.InternalCall)] get; [MethodImpl(MethodImplOptions.InternalCall)] set; }

    /// <summary>
    ///   <para>The maximum time a frame can take. Physics and other fixed frame rate updates (like MonoBehaviour's MonoBehaviour.FixedUpdate).</para>
    /// </summary>
    public static extern float maximumDeltaTime { [MethodImpl(MethodImplOptions.InternalCall)] get; [MethodImpl(MethodImplOptions.InternalCall)] set; }

    /// <summary>
    ///   <para>A smoothed out Time.deltaTime (Read Only).</para>
    /// </summary>
    public static extern float smoothDeltaTime { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The maximum time a frame can spend on particle updates. If the frame takes longer than this, then updates are split into multiple smaller updates.</para>
    /// </summary>
    public static extern float maximumParticleDeltaTime { [MethodImpl(MethodImplOptions.InternalCall)] get; [MethodImpl(MethodImplOptions.InternalCall)] set; }

    /// <summary>
    ///   <para>The scale at which the time is passing. This can be used for slow motion effects.</para>
    /// </summary>
    public static extern float timeScale { [MethodImpl(MethodImplOptions.InternalCall)] get; [MethodImpl(MethodImplOptions.InternalCall)] set; }

    /// <summary>
    ///   <para>The total number of frames that have passed (Read Only).</para>
    /// </summary>
    public static extern int frameCount { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    public static extern int renderedFrameCount { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>The real time in seconds since the game started (Read Only).</para>
    /// </summary>
    public static extern float realtimeSinceStartup { [MethodImpl(MethodImplOptions.InternalCall)] get; }

    /// <summary>
    ///   <para>Slows game playback time to allow screenshots to be saved between frames.</para>
    /// </summary>
    public static extern int captureFramerate { [MethodImpl(MethodImplOptions.InternalCall)] get; [MethodImpl(MethodImplOptions.InternalCall)] set; }

    /// <summary>
    ///   <para>Returns true if called inside a fixed time step callback (like MonoBehaviour's MonoBehaviour.FixedUpdate), otherwise returns false.</para>
    /// </summary>
    public static extern bool inFixedTimeStep { [MethodImpl(MethodImplOptions.InternalCall)] get; }
  }

首先从代码中我们可以知道,Time.time 等这些属性都是在 C++ 层面实现的,C# Time 类只是一个 Wrapper。
+ Time.frameCount, 游戏运行到现在,一共过了多少帧。注意:frameCount 是在所有的需要运行的 Awake 运行完毕后开始计算的,在这之前 frameCount 的值是未定义的(测试表明这个值是0)。

  • Time.time,从游戏开始运行到现在所累积的时间,以秒为单位(只读)。 注意:Time.time 只在每一帧的开始时进行更新,也就是说在同一帧内,Update 和 LateUpdate 里面,Time.time 是相同的,测试如下:

  • Time.timeSinceLevelLoad,从场景加载到现在所累积的时间,以秒为单位(只读)。Time.timeSinceLevelLoad 只在每一帧的开始时进行更新,当一个新的场景使用 SceneManager.LoadScene("level_2", LoadSceneMode.Single); 进行加载时,会卸载旧场景,并加载新场景,这个时间会被清空,重新计时。但是,SceneManager.LoadScene("level_2", LoadSceneMode.Additive); 并不会卸载旧场景,timeSinceLevelLoad 也不会被重新计时。

  • Time.unscaledTime,这个数值是不受 Time.timeScale 影响。注意,从测试结果来猜测,unscaledTime 并不是在每一帧的开始进行更新,应该在 Start 开始后才开始计时,然后每一帧进行更新。当Time.timeScale = 1 时,Time.unscaledTime 和 Time.time 的数值也不是相等的,与 Time.realtimeSinceStartup 也不相同。这个官方文档没有清晰的描述,工作原理不清楚。

  • Time.deltaTime,这个数值表示完成上一帧所花费的时间(秒),是当前这一帧的 Time.time 减去上一帧的 Time.time 所得到的结果。注意:在 Awake 和 Start 里面这个数值是预先定义的,因为这个时候只运行了一帧,并没有上一帧,所以不能用两帧相减。测试表明:在 Awake 和 Start 里,Time.deltaTime = 0.02, 并不会随着 Time.timeScale 变化。在 Update 、LateUpdate 等事件内,由于 Time.time 受 timeScale 影响,自然 Time.deltaTime 也会受到影响。

  • Time.unscaledDeltaTime,类似于 Time.deltaTime,但是不受 timeScale 影响。
  • Time.smoothDeltaTime,对 Time.deltaTime 进行平滑处理后的数值,平滑算法未知。
  • Time.timeScale 可以用来控制 Unity 游戏中时间的运行速度,举个例子来说,比如你想实现一个慢动作,可以通过 Time.timeScale= 0.5f; 来把速度降低一倍。需要注意的是,这种时间的改变是全局的,使用的时候需要谨慎。 Time.timeScale 并不会影响 Update 的调用频率,即使 Time.timeScale= 0f; Update 也会和正常调用,只会影响 Time.deltaTime,而由于 Coroutine 中的 WaitForSeconds 函数是基于deltaTime进行时间累积的,所以,自然也就把 Coroutine 的运行变慢甚至停止。很多动画的实现也是基于 Time.deltaTime 的,所以动画自然也会受到影响。