Unity.Time 小白科普

  Unity.Time 裏面的一系列和時間相關的數據,往往是我們做動畫處理的依據,比如 Time.deltaTime*每秒移動距離=本幀需移動的距離,等等,可以說Time類是非常重要的,他直接關係遊戲中的邏輯更新,同時Unity.Time 和fixedUpdate 也息息相關,直接決定了每幀中fixedUpdate的調用次數。現在因爲一個偶然的任務需要做客戶端和服務端的幀同步,所以我有回頭特地研究了下Unity.Time 發現很多東西並不是和我想象的一致,也許有的小白會和我一樣沒有注意到這些細節,所以將我的研究歷程寫下來供和我一樣的小白們參考。

        首先在講解Unity.Time之前,有一個概念需要明確,就是Unity引擎的所有代碼執行都是在一個主線程裏面執行的,比如你的腳本里面的update,awake,你開啓的攜程等等,他們都會按照Unity引擎的調度機制依次執行,注意沒有任何一個地方是並行執行的,是依次執行。.而這個執行的流程如下圖,這個是抄的別人的。

原文地址爲:https://blog.csdn.net/akof1314/article/details/39323081 不太明白這個執行流程的可以點進去看看,

可以看到Unity引擎就是這樣一遍,又一遍的從上到下的執行這些回調函數然後將渲染出的畫面發送到顯示器,使得我們的遊戲得以運行,像圖中所表示的這樣從頭到腳的執行一次爲一幀,執行這一幀所用的時間就是我們這一幀的時長。

在理想的情況下我們每幀的執行時長應該是0,也就是立即繪製出來,但是事實上不是這樣,我們的邏輯運算,物理碰撞,畫面顯示,幀的同步。都是需要時間的。這導致在上一幀開始的那個時間點到下一幀開始的時間點之間我們的遊戲世界的時間一直在流逝,而這一段時間內我們卻沒有做出任何反應(忙着在做上一幀的處理),所以當我們到達下一幀時,首先需要通過Time.deltaTime/Time.fixedDeltaTime 的數量來循環調用物理處理部分,依次推導物理運行直到現在的時間點。正如上面圖上所畫OnCliisionXXX之後又有一條執行路徑返回到FixedUpdate上方,繼續一次物理處理部分。然後接下來調用Update時,我們將可以通過Time.deltaTime 知道遊戲世界至上一幀後經過了多長時間,然後進行響應的處理。至此主要的兩個數據,Time.deltaTime 和 Time.fixedDeltaTime 的來源和用法以及意義我們都知道了。都搞定了,生活真美好不是嗎?hoho,別天真,圖樣圖森破 。。。。。

接下來所有的故事開始之前,我們要明確兩個概念,現實時間,遊戲世界時間他們是不同的,就像現實世界和遊戲世界是兩個不同的世界一樣,他們兩個的時間也不是一一對應的,否則玩一把文明6得要好幾代人了(順便推薦一下文明是個好遊戲)。所以Unity 分別設置了兩個沙漏Time.realtimeSinceStartup 和 Time.time 來分別保存遊戲開始到當前幀經過了多少現實時間和經過了多少遊戲世界時間。同樣Time.deltaTime也並不是你的電腦在上一幀開始的那個時間點到下一幀開始的時間點之間的現實時間,而是經過縮放機制處理後的遊戲世界時間。所以我們需要明白的是,Time.time ,Time.DeltaTime, fixedTime,fixedDeltaTime 這些常用的時間數據都是基於遊戲世界時間的,和現實世界時間是不同的,他們之間有一系列的機制進行處理和轉化,接下來我們將對這些會影響到我們Time 數據的機制進行分析。

1.V Sync Count 垂直同步

垂直同步的意思是,開啓這個功能,我們的引擎將只有在收到顯卡畫面更新完成的通知後,才能進入下一幀處理,假設我們的顯示器刷新率是60/秒,則每刷新一次的時間是1/60=0.016秒,如果我們當前幀的渲染時間是0.006秒,則引擎需要在渲染完成後等待0.01秒收到垂直同步信號後,才能更新圖像到顯示器,並開始調用下一幀的渲染。此功能可以在IDE(edit->projectSettings->Quality(inspecter面板)->V SyncCount) 修改設置。

測試代碼如下:

public class NewBehaviourScript1 : MonoBehaviour {

    private int m_frameCount;
    private System.DateTime m_lastTime;
    private double m_passTime;
    private double m_intervalTime;

    private void Awake()
    {
        m_lastTime = System.DateTime.Now;
        m_passTime = 0;
        m_intervalTime = 0;
        m_frameCount = 0;
    }

    void Start () {
       
    }

    void Update () {

        Debug.Log("Update() Time.frameCount=" + Time.frameCount+", Time.realtimeSinceStartup="+ Time.realtimeSinceStartup + " | Time.unscaledTime=" + Time.unscaledTime.ToString() + " Time.unscaledDeltaTime=" + Time.unscaledDeltaTime.ToString() + ", | Time.time=" + Time.time.ToString()+" Time.deltaTime=" + Time.deltaTime);

        transform.Translate(Vector3.right * 0.1f);
    }

    private void OnRenderObject()
    {
        m_frameCount++;

        m_intervalTime = (System.DateTime.Now - m_lastTime).TotalSeconds;
        m_passTime += m_intervalTime;
        Debug.Log("OnRenderObject() m_frameCount=" + m_frameCount + ",m_passTime=" + m_passTime + " 間隔=" + m_intervalTime);
        m_lastTime = System.DateTime.Now;

    }
}

將上面的代碼綁定到任意一個Gameobject執行,會得到如下結果:

未打開了垂直同步:

可以看到Time.realTimeSinceStartup 之間的時間差,還是 Time.unscaleDeltaTime 和 我們自己用 DateTime.Now 計算出的時間差 都在小數點後第三位也就是毫秒級間隔而且誤差很小(這三個時間之間的細微誤差我各人猜測可能是存儲精度問題導致的)

打開垂直同步:

可以看到Time.realTimeSinceStartup 之間的時間差,還是 Time.unscaleDeltaTime 和 我們自己用 DateTime.Now 計算出的時間差 都在小數點後第二位都是0.015左右。

上面兩個測試結果可以發現,開通垂直同步後我們的單幀的時長明顯的增加了,因爲我的顯示器的刷新率是60所以1/60=0.016秒,大致和上面的0.015秒差不多,所以我們現在可以基本確定垂直同步對幀運行時長的影響。

2.Time.maximumDeltaTime 幀時長最大限制

這一項從字面就可以看出是控制DeltaTime的,上面我們已經講過,電腦實際運行的現實時間和遊戲世界時間是不同的,因此這一項的作用就是當你的現實時間中的一幀的處理時長超過了Time.maximumDeltaTime設置的值時,最多隻有Time.maximumDeltaTime的時長被增加到遊戲世界的時間上。

測試代碼如下:

public class NewBehaviourScript1 : MonoBehaviour {

    private int m_frameCount;
    private int m_count;
    private System.DateTime m_lastTime;
    private double m_passTime;
    private double m_intervalTime;
    // Use this for initialization

    private void Awake()
    {
        m_lastTime = System.DateTime.Now;
        m_passTime = 0;
        m_intervalTime = 0;
        m_frameCount = 0;

        m_count = 0;
    }
    void Start () {
       
    }

    private void FixedUpdate()
    {
        //Debug.Log("FixedUpdate() Time.frameCount=" + Time.frameCount + ", Time.realtimeSinceStartup=" + Time.realtimeSinceStartup + " | Time.fixedUnscaledTime=" + Time.fixedUnscaledTime.ToString() + " Time.fixedUnscaledDeltaTime=" + Time.fixedUnscaledDeltaTime.ToString() + ", | Time.fixedTime=" + Time.fixedTime.ToString() + " Time.fixedDeltaTime=" + Time.fixedDeltaTime);

    }

    // Update is called once per frame
    void Update () {

        

        Debug.Log("Update() Time.frameCount=" + Time.frameCount+", Time.realtimeSinceStartup="+ Time.realtimeSinceStartup + " | Time.unscaledTime=" + Time.unscaledTime.ToString() + " Time.unscaledDeltaTime=" + Time.unscaledDeltaTime.ToString() + ", | Time.time=" + Time.time.ToString()+" Time.deltaTime=" + Time.deltaTime);

        transform.Translate(Vector3.right * 0.1f);
    }

    private void OnRenderObject()
    {
        m_frameCount++;

        m_intervalTime = (System.DateTime.Now - m_lastTime).TotalSeconds;
        m_passTime += m_intervalTime;
        Debug.Log("OnRenderObject() m_frameCount=" + m_frameCount + ",m_passTime=" + m_passTime + " 間隔=" + m_intervalTime);
        m_lastTime = System.DateTime.Now;

        
        //這裏做延遲相當於渲染延遲
        m_count++;

        if (m_count > 5)
        {
            m_count = 0;

            for (int i = 0; i < 300000000; i++)
            {
                float a = 1f / 35.6f;
            }
        }

    }
}

從代碼中我們可以看到,我們在OnRenderObject中增加了一個300000000次的循環已模擬渲染延遲的情況,同樣的我們關閉了垂直同步,避免受到干擾。結果如下:

可以看到藍色高亮部分,就是我們發生延遲的地方,我們可以發現延遲導致幀時長爲2.3秒,Time.realtimeSinceStartup 的時間差一樣也是2.3秒左右,但是我們記錄的Time.deltaTime 只有0.33333333,Time.time 也只增加了0.3333333,這事因爲默認情況下,Time.maximumDeltaTime 的值爲1/3秒,所以可以確定,當我們的幀時長超過了Time.maximumDeltaTime 設置的只,將會只有Time.maximumDeltaTime 的時長增加到遊戲世界時間中。

3.Time.captureFramerate 拍攝幀率

名字上很容易讓人誤解,因爲除了垂直同步,沒有其他地方控制幀時長的,跟不談幀率了,這裏的captureFramerate的意思是遊戲世界的幀率,即每一幀對應到多長的遊戲世界的時長,寫成公式就是:

1/Time.captureFramerate = 每幀增加多長遊戲世界時間

設置這個就是我們每幀,給遊戲時間的時間增加一個固定時長,這個與默認的遊戲世界時間的增加的機制是完全不同的,默認的機制是我們每幀的執行時長在小於Time.maximumDeltaTime的情況下根據實際幀執行時長增加遊戲世界的時間,超過了就增加Time.maximumDeltaTime,而設置了Time.captureFramerate 後的機制是每一幀不論執行多長時間,都固定的爲遊戲世界時間增加一個固定的時長。

測試代碼如下(依然關閉垂直同步避免受到干擾):

public class NewBehaviourScript1 : MonoBehaviour {

    private int m_frameCount;
    private int m_count;
    private System.DateTime m_lastTime;
    private double m_passTime;
    private double m_intervalTime;
    // Use this for initialization

    private void Awake()
    {
        Time.captureFramerate = 2;

        m_lastTime = System.DateTime.Now;
        m_passTime = 0;
        m_intervalTime = 0;
        m_frameCount = 0;

        m_count = 0;
    }
    void Start () {
       
    }

    private void FixedUpdate()
    {
        //Debug.Log("FixedUpdate() Time.frameCount=" + Time.frameCount + ", Time.realtimeSinceStartup=" + Time.realtimeSinceStartup + " | Time.fixedUnscaledTime=" + Time.fixedUnscaledTime.ToString() + " Time.fixedUnscaledDeltaTime=" + Time.fixedUnscaledDeltaTime.ToString() + ", | Time.fixedTime=" + Time.fixedTime.ToString() + " Time.fixedDeltaTime=" + Time.fixedDeltaTime);

    }

    // Update is called once per frame
    void Update () {

        

        Debug.Log("Update() Time.frameCount=" + Time.frameCount+", Time.realtimeSinceStartup="+ Time.realtimeSinceStartup + " | Time.unscaledTime=" + Time.unscaledTime.ToString() + " Time.unscaledDeltaTime=" + Time.unscaledDeltaTime.ToString() + ", | Time.time=" + Time.time.ToString()+" Time.deltaTime=" + Time.deltaTime);

        transform.Translate(Vector3.right * 0.1f);
    }

    private void OnRenderObject()
    {
        m_frameCount++;

        m_intervalTime = (System.DateTime.Now - m_lastTime).TotalSeconds;
        m_passTime += m_intervalTime;
        Debug.Log("OnRenderObject() m_frameCount=" + m_frameCount + ",m_passTime=" + m_passTime + " 間隔=" + m_intervalTime);
        m_lastTime = System.DateTime.Now;

        
        //這裏做延遲相當於渲染延遲
        m_count++;

        if (m_count > 5)
        {
            m_count = 0;

            for (int i = 0; i < 300000000; i++)
            {
                float a = 1f / 35.6f;
            }
        }

    }

可以看到,我們就是多加了一行,設置Time.captureFramerate =2 也就是每幀增加0.5秒的遊戲世界時間

結果如下:

可以看到,無論我們的幀執行時長是低於0.5,還是高於0.5,Time.time 都是按照0.5秒增加,Time.deltaTime 也總是0.5。所以可以確定Time.captureFramerate 是表示每幀固定增加多少遊戲世界時長。

4.Time.timeScale 時間縮放

這一項從字面意思已經很好理解了,就是對每幀增加的遊戲世界時間進行縮放,需要注意的是這個縮放是 Time.captureFramerate 和 Time.maximumDeltaTime 機制處理過後的需要增加的遊戲世界時間,而不是對幀執行時間的直接縮放。

測試代碼這裏就不寫了,和上面的Time.captureFramerate 的測試代碼一樣,吧下面這一段改一改成這樣就可以了

  private void Awake()
    {
        //Time.captureFramerate = 2;
        Time.timeScale = 2;

        m_lastTime = System.DateTime.Now;
        m_passTime = 0;
        m_intervalTime = 0;
        m_frameCount = 0;

        m_count = 0;
    }

可以看到,我們設置Time.timeScale=2 也就是放大兩倍,這樣導致遊戲世界時間快兩倍。

結果如下:

可以看到,紅色標記的兩條記錄,第二條記錄顯示,實際的幀執行時間 Time.unscaleDeltaTime= 0.017 被翻倍成了  Time.deltaTime=0.034 (我們計算的間隔和 Time.unscaleDeltaTime 有0.01內的誤差,這個可能是存儲誤差導致的,但是可以看到相同的增長變化),第一條記錄顯示,實際的幀執行時間 Time.unscaleDeltaTime= 2.37 經過Time.maximumDeltaTime  的機制處理後,增加的遊戲世界時間爲 Time.deltaTime= Time.maximumDeltaTime *2 所以是0.666667 ,所以可以確定這個縮放是在Time.maximumDeltaTime 和Time.captureFramerate  這些過濾機制的結果只上進行的。

4.Time.fixedDeltaTime 固定更新時差

要想講清楚 fixedDeltaTime 就會涉及到fixedUpdate 這裏面關聯到很多其他數據,基本上所有fixed相關的時間數據,事實上都是通過 time 推導出來的,由於太複雜,我這裏直接用一張圖來表示他們的關係。

如圖所示,fixedDeltaTime 的確是決定了 fixUpdate 的調用頻率,Unity引擎將根據當前幀的time 和最後一次調用fixedDeltaTime 的時間差,決定調用幾次 fixUpdate,每次調用都會更新 fixedTime,並且根據當前最後一次調用fixupdate時的fixedTime 與time 的時間差,倒推出每次調用fixupdate時的 fixedUnscaleDeltaTime 和 fixedUnscaleTime數據。

大家可以通過如下代碼自行測試:

public class NewBehaviourScript1 : MonoBehaviour {

    private int m_frameCount;
    private int m_count;
    private System.DateTime m_lastTime;
    private double m_passTime;
    private double m_intervalTime;
    // Use this for initialization

    private void Awake()
    {
        //Time.captureFramerate = 2;
        //Time.timeScale = 2;

        m_lastTime = System.DateTime.Now;
        m_passTime = 0;
        m_intervalTime = 0;
        m_frameCount = 0;

        m_count = 0;
    }
    void Start () {
       
    }

    private void FixedUpdate()
    {
        Debug.Log("FixedUpdate() Time.frameCount=" + Time.frameCount + ", Time.realtimeSinceStartup=" + Time.realtimeSinceStartup + " | Time.fixedUnscaledTime=" + Time.fixedUnscaledTime.ToString() + " Time.fixedUnscaledDeltaTime=" + Time.fixedUnscaledDeltaTime.ToString() + ", | Time.fixedTime=" + Time.fixedTime.ToString() + " Time.fixedDeltaTime=" + Time.fixedDeltaTime);

    }

    // Update is called once per frame
    void Update () {

        

        Debug.Log("Update() Time.frameCount=" + Time.frameCount+", Time.realtimeSinceStartup="+ Time.realtimeSinceStartup + " | Time.unscaledTime=" + Time.unscaledTime.ToString() + " Time.unscaledDeltaTime=" + Time.unscaledDeltaTime.ToString() + ", | Time.time=" + Time.time.ToString()+" Time.deltaTime=" + Time.deltaTime);

        transform.Translate(Vector3.right * 0.1f);
    }

    private void OnRenderObject()
    {
        m_frameCount++;

        m_intervalTime = (System.DateTime.Now - m_lastTime).TotalSeconds;
        m_passTime += m_intervalTime;
        Debug.Log("OnRenderObject() m_frameCount=" + m_frameCount + ",m_passTime=" + m_passTime + " 間隔=" + m_intervalTime);
        m_lastTime = System.DateTime.Now;

        
        //這裏做延遲相當於渲染延遲
        m_count++;

        if (m_count > 5)
        {
            m_count = 0;

            for (int i = 0; i < 300000000; i++)
            {
                float a = 1f / 35.6f;
            }
        }

    }

到這裏常用的,Time數據基本上都講完了,歡迎大家留言交流。轉載請註明出處,謝謝。

发表评论