[Unity] 從零開始的學習心得 #3 – 物理系統的陷阱

unity-learning-totally-review-3

此篇文章是《從零開始的 Unity 學習心得》系列的第三篇文章,這個系列是記錄我對 Unity 的開發及學習心得,包含我用何種方式去理解 Unity 的運作,並且在我所知的範圍內盡可能說明相關原理與開發要點。完整系列的目錄可以參考:

上篇文章說明了幾個 Unity 的基本組成,並且開始接觸 MonoBehaviour。這篇文章將延續話題,聊聊我對於 Unity 物理系統 – 一個最常用在與 MonoBehaviour 共築遊戲邏輯的系統 – 的理解。

常見的物件互動手段 - 物理系統

Unity 內建了一套物理模擬系統給開發者利用,包括物理慣性運動、重力場、摩擦阻力、碰撞與觸發等功能。雖然作為物理模擬,Unity 的內建系統稱不上精確與穩定,偶爾會有令人噴飯的意外狀況,但是如果理解它的原理與適用範圍,仍舊是在遊戲開發上相當方便的內建系統。

Unity 物理系統的使用網路上有相當資訊可以查詢,要一一說明也要不少的篇幅,這邊我想提的是關於 FixedUpdate 這個 Event 的三兩事。

物理系統的模擬跟現實有個最大的差距:時間的連續性是無法在軟體中完美重現的,不論何種物理模擬,都只能盡量縮短模擬的時間間隔,來提高近似度與精準度,不可能真正的重現物理。而在遊戲中我們不可能無限制的提升模擬結果的精準度,因為與之相對的軟硬體效能是相當有限的。

Unity 的模擬間隔可以在 Project Setting 去設定,預設值是 0.02 秒;而 FixedUpdate 辨識與之對應,每次模擬時會呼叫到的 Unity Event Function,預設每秒呼叫 50 次。

光從呼叫的時間點就可以看出 FixedUpdateUpdate 是處在兩個截然不同的時間軸上,一個是物理的循環;另一個是畫面更新的循環 (同時也是 Game Loop)。要建立橫跨兩個時間軸的遊戲邏輯,便很容易遇到奇奇怪怪的狀況,輕則畫面閃動,重則腳色直接噴飛。

這些狀況可能都跟物理系統有關

畫面閃動意外

如果在 FixedUpdate 之中對物體進行強制定位 (設定Postiion),即使是配合了 Time.fixedDeltaTime 想製作平滑的移動,也無法避免畫面閃動的問題。因為 FixedUpdateUpdate 的更新頻率不相同,所以 Update 每隔一段時間,就會連續執行兩次,然後才發生 FixedUpdate,本來應該平滑移動的物件,便在這瞬間發生了"突然停止移動",也就使畫面產生卡頓或閃動。

除了在物體本身發生閃動,也有可能是在兩個物體之間產生不同步,比如跑步的腳色跟追隨的 Camera 之間。解決方法很簡單,就是只要畫面中有牽涉到物理系統的任何組件,便要避免用 Time.fixedDeltaTimeTime.deltaTime 來設定位置,應該採用 AddForce 或者取用 rigidbody 的參數來設定物理運動。

在牆邊顫抖的物件

一般來說腳色會有被障礙擋住的情況,此時玩家即使不斷按住前進鍵,腳色也應該停在原地做太空漫步,而不是瘋狂顫抖。這發生的原因是因為玩家的 輸入(Input) 在 Update 進行接收與使用,但物理的碰撞卻是在 FixedUpdate 更新,所以腳色便重複進行著 “走進牆內、被擠出牆外” 的循環,畫面上看起來就像在顫抖一般。

解決方式可以視情況去做設計,常見的做法之一是在接收到玩家輸入後,還要利用 Raycast 檢查是否撞到障礙,然後才決定是否前進,而不是直接依賴碰撞框來阻止腳色的移動範圍。

腳色或物件噴飛

這也是跟 FixedUpdateUpdate 的更新頻率不相同有關,Unity 在實現物理模擬時,所有的運算都在 FixedUpdate 的迴圈中運作,如果你在 Update 裡面對物理狀態進行了更動,那要注意 Update 可能會跑得比 FixedUpdate 快的問題,一不小心累積了兩倍分量的物理操作,在下一個 FixedUpdate 之後,你的腳色可能就不知道飛到哪裡去了。

另外有個可能不是因為你 Update 的操作有誤,而是物體的移動速度過快,所以數個物件"過度深入"對方,產生的碰撞力道超乎預期,那物件可能也會噴飛;或者因為速度過快,根本來不及檢查出物件碰撞,物件穿透了應當產生阻擋效果的物件,這又是另一種噴飛了。

噴飛的原因百百種,所以解決方式也各不一定。物理系統本身不是完美的 (上面有提到原因,這不全是 Unity 的錯),所以理解它的原理跟極限,不要抱有太過度的期待,你才能順利的控制你的遊戲作品。