前言

物理更新與畫面算繪

在 Godot 中需要理解的一個重要概念是物理更新 (有時也稱為迭代或物理幀) 與算繪幀之間的區別。物理是以固定的更新率進行的 (在 專案設定 > 物理 > 通用 > 每秒物理更新次數 中設定,預設為每秒 60 次更新)。

然而,遊戲引擎不一定會以相同的速率進行**算繪**。雖然許多螢幕的更新頻率是 60 Hz(每秒週期),但也有許多螢幕使用完全不同的頻率(例如 75 Hz、144 Hz、240 Hz 或更高)。即使螢幕可能能夠每秒顯示 60 個新影格,也不能保證 CPU 和 GPU 能夠以這個速率 提供 影格。舉例來說,當啟用垂直同步時,電腦可能速度不夠快而無法達到 60 FPS,只能趕上 30 FPS 的時限,在這種情況下,您看到的影格將以 30 FPS 的速率變化(導致畫面卡頓)。

但這裡有個問題。如果物理更新的頻率和畫面更新的頻率不同步會怎麼樣?如果物理更新的時序和畫面更新的時序不同步又會怎麼樣?更糟的是,如果物理更新的頻率 低於 算繪的畫面更新頻率會怎麼樣?

如果我們考慮一個極端情境,這個問題會更容易理解。假設您將物理更新頻率設定為每秒 10 次,在一個簡單的遊戲中,算繪幀率為每秒 60 幀。如果我們繪製一個物件位置相對於算繪幀的圖表,您會看到物件的位置每 1/10 秒會「跳動」一次,而不是呈現平滑的運動。當物理計算出一個新物件的新位置時,這個新位置不會只在一個影格中算繪,而是會持續 6 個影格。

../../../_images/fti_graph_fixed_ticks.webp

這種跳動在其他刻度/影格率的組合中可能會看見,表現為故障、抖動或不穩,這是因為物理刻度時間和算繪影格時間之間的差異所造成的階梯效應。

幀數和刻度不同步的問題,我們該怎麼處理?

鎖定幀率與物理更新率同步?

最直觀的解決方案是直接消除問題,方法是確保每個影格都伴隨著一次物理更新。這在舊式遊戲主機和固定規格的電腦上曾經是常見的做法。如果你確定所有玩家都會使用相同的硬體,你可以確保硬體效能足夠快速,能夠以例如 50 FPS 的速度計算物理更新和影格,這樣就能確保所有人的體驗都很順暢。

然而,現代遊戲通常不再是為固定硬體打造。您經常會計劃在桌上型電腦、行動裝置以及更多平台上發行。這些平台在效能上差異極大,螢幕更新率也各不相同。我們需要想出更好的方法來處理這個問題。

調整刻度率?

與其用固定的物理更新頻率來設計遊戲,我們可以允許更新頻率根據終端使用者的硬體進行調整。例如,我們可以針對該硬體使用一個可行的固定更新頻率,甚至可以調整每次物理更新的時間長度,使其符合特定的畫面更新時間。

這樣做可行,但有個問題。物理( 以及遊戲邏輯 ,通常也運行在 _physics_process 中)在以 固定 、預先決定的更新頻率運行時,表現最好且最穩定。如果您嘗試以例如 10 TPS(每秒更新次數)運行一個專為 60 TPS 設計的賽車遊戲物理,物理行為將會完全不同。控制可能較不靈敏,碰撞/軌跡可能會完全改變。您可能在 60 TPS 下徹底測試您的遊戲,然後發現在終端使用者的電腦上以不同的更新頻率運行時發生錯誤。

這可能會使品質保證變得困難,尤其是在難以重現的錯誤方面,在 AAA 級遊戲中,這類問題的代價可能非常高昂。對於多人遊戲的競技公平性而言,這也可能造成問題,因為以特定的更新頻率運行遊戲可能比其他頻率更有優勢。

鎖定更新頻率,但使用插值來平滑物理更新之間的畫面

這已成為處理此問題最受歡迎的方法之一,雖然它是可選的,並且預設為停用。

我們已經確立,為了保持一致性和可預測性,最理想的物理/遊戲邏輯安排是將物理更新頻率在設計時就固定下來。問題在於記錄的物理位置,以及為了在畫面呈現流暢的運動,我們「希望」物理物件顯示在哪裡之間存在差異。

答案其實很簡單,但一開始可能有點難以理解。

引擎中,我們不只追蹤物理物件的當前位置,還會追蹤 物件的當前位置,以及前一個物理週期的前一個位置

為什麼我們需要前一個位置 (實際上是整個變換,包含旋轉和縮放) ?透過一點數學技巧,我們可以使用 插值 來計算物體在這兩個點之間,在我們理想的平滑連續運動世界中,其變換會是什麼。

../../../_images/fti_graph_interpolated.webp

線性內插

達成此目的最簡單的方法是線性內插,或稱作 Lerp,您之前可能已經使用過。

我們只考慮位置,以及一種情況:我們知道前一個物理幀的 X 座標是 10 單位,而目前的物理幀的 X 座標是 30 單位。

備註

雖然這裡解釋了數學原理,但你不需要擔心細節,因為這個步驟會自動為你執行。在底層,Godot 可能會使用更複雜的差值運算,但線性差值就解釋而言是最容易理解的。

物理插值比例

如果我們的物理更新頻率是每秒 10 次(以這個例子來說),那麼當我們的算繪畫面發生在 0.12 秒時會發生什麼事?我們可以做一些數學運算來找出物件在兩個物理更新之間應該在哪個位置,以獲得平滑的運動。

首先,我們必須計算物件在物理更新的哪個時間點。如果上次物理更新發生在 0.1 秒,那麼我們目前處於一個已知會耗時 0.1 秒(每秒 10 次更新)的更新週期中的 0.02 秒 (0.12 - 0.1)。因此,這個更新週期所經過的比例是:

fraction = 0.02 / 0.10
fraction = 0.2

這稱為 物理內插比例 ,Godot 會方便地為您計算好。您可以在任何影格中透過呼叫 Engine.get_physics_interpolation_fraction 來取得這個數值。

計算插值後的位置

一旦我們取得插值比例,就可以將其代入標準的線性插值方程式。因此,X 座標會是:

x_interpolated = x_prev + ((x_curr - x_prev) * 0.2)

因此,將我們的 x_prev 代入 10,且 x_curr 代入 30:

x_interpolated = 10 + ((30 - 10) * 0.2)
x_interpolated = 10 + 4
x_interpolated = 14

讓我們逐步解析:

  • 我們知道 X 軸是從前一個刻度 (x_prev) 的座標開始,該座標是 10 個單位。

  • 我們知道在完整物理幀之後,目前物理幀和前一個物理幀之間的差值( x_curr - x_prev )將會被加上(也就是 20 個單位)。

  • 我們唯一需要調整的是加入這個差異的比例,取決於我們在物理更新週期中的進度。

備註

雖然這個範例是針對位置進行插值,但物體的旋轉和縮放也能以相同方式處理。不需要了解細節,Godot 會為您處理所有這些事情。

物理更新間的平滑變形?

總結以上所述,這表示應該可以對物件在目前和前一個物理週期之間的變換進行良好且平滑的估算。

等等,您可能已經注意到一些事情。如果我們是在目前和前一個影格之間進行插值,我們並非在估算物件「現在」的位置,而是在估算物件「過去」的位置。更精確地說,我們估算的是物件在「過去 1 到 2 個影格之間」的位置。

過去

這是什麼意思?這個方法確實可行,但這也表示我們實際上會在螢幕上看到的畫面,和物件 應該 在的位置之間,引入一個延遲。

實際上,大多數人不會注意到這個延遲,或者更確切地說,它通常 不會令人反感 。遊戲中本來就存在顯著的延遲,只是我們通常不會察覺。最主要的影響是輸入可能會有一點延遲,這在需要快速反應的遊戲中可能會是一個考量因素。在某些需要快速輸入的情況下,您可能會希望關閉物理插值並使用不同的方案,或者使用較高的更新頻率(tick rate),這樣可以減輕這些延遲。

為何回顧過去?為何不預測未來?

這個方案有個替代做法:不是在先前和目前的刻度之間進行內插,而是使用數學來 外插 到未來。我們嘗試預測物體 將會 在哪裡,而不是顯示它過去在哪裡。這可以做到,未來也可能作為一個選項提供,但存在一些顯著的缺點:

  • 預測可能不準確,尤其是在物理更新時物件與其他物件發生碰撞的情況下。

  • 當預測不正確時,物件可能會外插到「不可能」的位置,像是牆壁裡面。

  • 如果移動速度不快,這些不正確的預測可能不會是太大的問題。

  • 當預測不正確時,物件可能需要跳躍或突然彈回修正後的路徑。這在視覺上可能會很突兀。

固定時間步差值

在 Godot 中,這個完整的系統稱為物理內插,但您也可能聽過它被稱為 「固定時間步內插」 ,因為它是基於以固定時間步(每秒物理更新次數)移動的物件之間進行內插。在某些方面,第二個名稱更準確,因為它也可以用於內插非物理驅動的物件。

小訣竅

雖然物理插值通常是不錯的選擇,但有些例外情況您可能會選擇不使用 Godot 內建的物理插值(或僅限部分使用)。一個例子是網路多人遊戲。多人遊戲經常從其他玩家或伺服器接收基於時間或時序的資訊,這些資訊可能與本地物理更新不同步,因此自訂的插值技術通常會更適合。