FixedUpdate真的是固定的時間間隔執行嗎?聊聊遊戲定時器

0x00 前言

有時候即便是官方的文檔手冊也會讓人產生誤解,比如本文將要討論的Unity引擎中的FixedUpdate方法。

This function is called every fixed framerate frame, if the MonoBehaviour is enabled.

而大家似乎也都認爲FixedUpdate是在固定的時間間隔執行,不受遊戲幀率的影響。其實這麼說並不準確,甚至有些誤導。因此,我就寫這篇文章來聊聊Unity的FixedUpdate方法,或者說來聊聊遊戲引擎的定時器實現吧。

0x01 直觀印象

我相信各位對FixedUpdate的直觀印象首先來自於它的名字。並且常常會和一般Update方法進行對比,以突顯它的,呃,特殊性。

其次,有一些實驗精神的小夥伴也許會親自來測試一下這個方法的時間間隔。而Console輸出的內容也十分符合他的預期,簡直毫釐不差。
在這裏插入圖片描述
(圖片來自網絡)

不過,我相信很多篤信FixedUpdate的朋友一定有過類似這樣的疑慮:默認情況下FixedUpdate的更新頻率是50FPS(0.02s),如果當遊戲的更新頻率較大時——例如60FPS——那麼FixedUpdate有固定的更新頻率還稍微可以理解,但是如果遊戲本身的更新頻率就很低——例如30FPS——那麼是怎麼保證FixUpdate的更新頻率呢?

而Unity的主要邏輯是單線程的(參見Aras的回答),所以也不存在有不同的邏輯循環可能,換句話說Update和FixedUpdate是在同一個線程上調用的。

問題似乎開始變得有趣了。

如果我們來重複一下各位測試FixedUpdate時經常採用的方式:打印兩次調用之間的時間間隔,或者是計算每次調用的累積時間,但區別在於我直接使用了真實的時間:

void FixedUpdate()
{
    Debug.Log("FixedUpdate realTime: " + Time.realtimeSinceStartup);
}
 
void Update()
{
    Debug.LogError("Update realTime: " + Time.realtimeSinceStartup);
}

爲了以示區別,正常的Log是FixedUpdate,LogError是Update,並且設定整個遊戲的更新頻率爲30FPS。

在這裏插入圖片描述

通過上圖可以發現,FixedUpdate並不是間隔0.02s才調用一次,相反,它有可能在Update之前調用多次。

例如剛啓動時,在某次Update之前連續調用了11次FixedUpdate,而之後每次Update調用之前都會調用1~2次FixedUpdate方法,這也很好理解,因爲FixedUpdate的頻率是50FPS,而我們設定的更新頻率爲30FPS,FixedUpdate的調用次數勢必要多於Update。

所以,FixedUpdate除了用來處理物理邏輯之外並不適合處理其他模塊的邏輯,尤其是當大家的潛意識裏都篤定它的更新頻率是固定的時候。因爲這很危險,比如一個需要狀態同步的遊戲要求按照固定的頻率向目標同步狀態,如果貿然採用FixedUpdate方法,就會出現上圖那樣可能在短時間內多次調用的問題。所以最好只在物理邏輯的處理中使用FixedUpdate,而不要濫用。而由於FixedUpdate主要是用於物理邏輯的,因此下文的討論也主要圍繞物理邏輯。

0x02 可變增量時間

ok,那我們來看看遊戲引擎的定時器是如何來實現的吧。假設我們手頭沒有一個現成的遊戲引擎,一切都需要自己來實現,那麼一個最簡單的遊戲循環大概就是下面這個樣子的。

double lastTime = Timer.GetTime();
...
 
while (!quit()){
 
  double currentTime = Timer.GetTime();
  double frameTime = currentTime - lastTime ;
 
  UpdateWorld(frameTime);
  RenderWorld();
 
  lastTime = currentTime;
}

這種實現方式使得增量時間和具體的機器設備相關,並且它每一次的時間增量都不一定相同。

當機器十分快時,引擎可能通過線程休眠來保證固定的FPS。

while(timeout > 0.001 && deltaTime<timeout)
{
    //...休眠後計算新的增量時間
}

所以看上去整個遊戲保證一個大致的更新頻率似乎不難。但是現在問題的關鍵在於每次更新時的時間增量無法保證相同。而在物理模擬中,保證一個固定的增量時間是十分重要的。這是因爲在遊戲引擎進行物理模擬時要使用數值積分,而作爲最簡單的數值積分方法——歐拉法在遊戲引擎中大量使用。

在這裏插入圖片描述

上面就是一個歐拉法的簡單例子。可以看到增量時間是很重要的。而在遊戲引擎的物理模擬中,一個不穩定的增量時間可能導致很多和預期相悖的結果。

由於此時計算的是真實的時間,而真實的增量時間無法保證固定,那換一個思路,我們把參與物理模擬的增量時間當做一個常量可以嗎?換句話說,不論遊戲的更新頻率如何,參與物理模擬的增量時間是一個常數。

0x03 固定增量時間

一個最簡單的固定增量時間的實現,顯然就是將固定的增量時間作爲一個常量參數傳遞給物理模擬模塊,這樣我們就能夠保證物理模擬的增量時間固定,同時還能將物理模擬的更新頻率和遊戲引擎的更新頻率進行解耦——物理的模擬不受引擎的更新頻率影響,無論遊戲的更新頻率是多少,傳遞給物理模擬的增量時間都是一個常量。

這裏還拿Unity引擎來舉例子,默認情況下項目的Fixed Timestep的值爲0.02s。也就是說物理模擬的頻率是50FPS,假設我們的遊戲的更新頻率是25FPS,那麼會發生什麼呢?沒錯,遊戲每1次Update時,物理模擬都要推進2次,也就是之前我們看到的在Update之前多次調用了FixedUpdate。那麼如果我們的遊戲更新頻率是100FPS呢?這次就變成了每2次Update調用1次FixedUpdate。

這也就解釋了爲何有的朋友在做相關的小測試的時候,在每次FixedUpdate內打印Time.deltaTime時輸出的都是0.02了,因爲Time.deltaTime並非兩次調用FixedUpdate之間真實的時間間隔,而是來自我們在項目的Time設置內設置的值——它是一個與真實時間無關的常量。

When called from inside MonoBehaviour’s FixedUpdate, returns the fixed framerate delta time.

那麼這種固定增量時間的邏輯如何用代碼表示呢?很簡單,只需要在主循環的內部,再來一個二級循環。

double  simulationTime = 0;
double  fixedTime = 20;
 
while (!quit()){
 
  double  realTime = Timer.GetTime();
 
  while (simulationTime < realTime){
         simulationTime += fixedTime; 
         UpdateWorld(fixedTime);
  }
 
  RenderWorld();
}

而Unity的實現顯然也是類似的,這個在Unity手冊中關於執行順序的相關章節內可以看到。

在這裏插入圖片描述

上圖標明瞭FixedUpdate是屬於物理模擬模塊的,同時在主循環的內部,物理模擬的部分還有一個二級循環。

0x04 總結及建議

好了,到了需要總結一下的時候了。

經過上文的分析,我想各位應該都能瞭解一個遊戲引擎是如何實現定時器的了吧,而且在物理模擬時使用一個固定的增量時間也並不神祕,只需要讓物理模擬和真實時間解耦,使用一個常量作爲其增量時間即可。

同時還要提醒各位,不要濫用Unity的FixedUpdate方法,因爲它是用來處理物理模擬的,更重要的是它並非根據真實時間的間隔執行。

歡迎關注作者的公衆號:慕容的遊戲編程(chenjd01)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章