大家好,這里是七七,今天開始更新物理引擎相關的優化部分了,本文介紹的是物理引擎內部工作情況。
Unity技術有兩種不同的物理引擎:用于3D物理的Nvidia的PhysX和用于2D物理的開源項目Box2D。然而,Unity對它們的實現是高度抽象的,從通過主Unity引擎配置的更高級別Unity API的角度來看,兩個物理引擎解決方案以功能相同的方式運行。
無論是哪種情況,對Unity的物理引擎了解的越多,就越能理解可能的性能增強。本文將介紹一些關于Unity如何實現這些系統的理論。
一、物理和時間
物理引擎通常是在時間按固定值前進的假設下運行的,Unity的兩個物理引擎也都以這種方式運行。每個迭代稱為時間步長。物理引擎將只使用特定時間值來處理每個時間步長,這與渲染上一幀所花費的時間無關。該時間步長在Unity中稱為固定更新的時間步長,默認值為20ms。
注意:由于體系結構(浮點值的表示方式)的不同以及客戶端之間的延遲,如果物理引擎使用可變的時間步長,就很難在兩臺不同的計算機之間產生一致的碰撞和力的結果。這樣物理引擎往往會在多人的客戶端之間或錄制的重播期間生成不一致的結果。
固定的更新在物理引擎執行自己的更新之前處理,而這兩者之間的聯系是不可分割的。這個過程開始于確定是否已經過了足夠的時間來開始下一個固定的更新。一旦確定了這一點,則結果將有所不同,這取決于自上次固定更新以來經過的時間。
如果經過了足夠的時間,則固定更新的處理將調用在場景中所有激活的MonoBehaviour中定義的FixedUpdate()回調,接著處理與固定更新相關的任何協程(特別是那些生成WaitForFixedUpdate的協程)。注意,對于在這兩個過程中調用的方法,沒有執行順序的保證,所以不應該在這個假設下編寫代碼。一旦這些任務完成,物理引擎就可以開始處理當前的時間步長,并調回任何必要的觸發器和碰撞器。
相反,如果上次固定更新以來經過的時間太短(小于20ms),則跳過當前的固定更新,并且之前列出的所有任務不會再當前迭代期間處理。此時,輸入、游戲邏輯和渲染將正常進行。完成此活動后,Unity將檢查是否需要處理下一個固定更新。
在高幀率下,渲染更新可能會在物理引擎獲得自身更新機會之前完成多次更新。這個過程在運行時不斷重復,使固定的更新和物理引擎比渲染具有更高的優先級,同時也強制物理模擬具有固定的幀率。
提示:為了確保對象在固定更新之間平穩移動,物理引擎根據下一次固定更新之前的剩余時間,在處理當前狀態之后,在上一個狀態和應處于的狀態之間對每個對象的可見位置進行插值。這種插值可以確保對象的移動非常平穩,盡管它們的物理位置、速度等更新的頻率低于渲染幀率。
FixedUpdate()回調適用于任何期望獨立于幀率的游戲行為。AI通常在固定的更新中計算,因為如果假設一個固定更新的頻率,會更容易開發。
1.1 最大允許的時間步長
需要注意的是,如果自上次固定更新(例如游戲暫時卡頓)以來已經過了很長時間,那么固定更新將繼續在相同的固定更新循環中計算,直到物理引擎趕上當前時間。如果上一幀畫了100ms用于渲染(例如,一個突然的CPU峰值導致主線程阻塞了很長時間),那么物理引擎將需要更新5次。由于默認固定更新的時間步長為20ms,在再次調用Update()之前還需要調用5次FixedUpdate()。當然,如果在這5次固定更新時有很多物理活動需要處理,例如總共花費了超過20ms處理它們,那么物理引擎將繼續調用第6次更新。
因此,在物理活動較多時,物理引擎處理固定更新的時間可能比模擬的時間要長。例如,如果用30ms來處理一個固定的更新,模擬20ms的游戲,它就已經落后了,需要它處理更多的時間步長來嘗試和跟上,但這可能會導致它落后得更遠,需要它處理更多的時間步長,等等。在這些情況下,物理引擎永遠無法擺脫固定的更新循環,并允許另一幀進行渲染,這個問題通常稱為死亡螺旋。但是,為了防止物理引擎在這些時刻鎖定游戲,存在允許物理引擎處理每個固定更新循環的最長時間,則它將停止并放棄進一步的處理,直到下一次渲染更新完成。這種設計允許渲染管線至少將當前狀態進行渲染,并允許用戶輸入以及游戲邏輯在物理引擎出現異常的罕見時刻做出一些決策。
該設置可以通過Edit|Project Settings|Time|Maximum Alowed Timestep來訪問。
1.2 物理更新和運行時的變化
當物理引擎以給定的時間步長處理時,它必須移動激活的剛體對象,檢測新的碰撞,并調用相應對象的碰撞回調。Unity文檔明確指出,應該在FixedUpdate()和其他物理回調中處理對剛體對象的更改,原因正是如此。這些方法與物理引擎的更新頻率緊密耦合,而不是游戲循環的其他部分,如Update()。
這意味著,諸如FixedUpdate()和OnTriggerEnter()的回調函數能夠安全更改Rigidbody的位置,而諸如Update()和對WaitForSeconds或WaitForEndOfFrame的協程卻不能。忽略這一建議可能會導致意想不到的物理行為,因為在物理引擎有機會捕獲和處理所有這些對象之前,可能會對同一個對象進行多次更改。
對Update()回調中的對象應用力或脈沖而不考慮這些調用的頻率是非常危險的。例如,在玩家按住一個鍵時,給Update功能應用10牛頓的力,會導致兩個不同設備之間的合成速度完全不同于在固定更新中執行相同的操作。事實上,不能依賴Update()調用的次數是一致的。但是,在FixedUpdat()回調中這樣做會更加一致。因此,必須確保在適當的回調中處理所有與物理引擎相關的行為,否則就可能引入一些令人困惑,很難重現的游戲漏洞。
從邏輯上講,在任何給定的固定更新迭代中花費的時間越多,在下一次游戲邏輯和渲染過程中花費的時間就越少。由于物理引擎幾乎沒有任何工作要做,而且FixedUpdate()回調有很多時間來完成它們的工作,因此大多數情況下這會導致一些小的、不明顯的后臺處理任務。然而,在某些游戲中,物理引擎可能在每次固定更新期間執行大量計算。這種物理處理時間上的瓶頸會影響幀率,導致它在當物理引擎負擔越來越大的工作負載時,幀率急劇下降。基本上,渲染管線將嘗試正常進行,但每當需要進行固定更新時(物理引擎處理時間很長),渲染管線在幀結束之前幾乎沒有時間生成畫面,會導致突然停頓。物理引擎達到允許的最大時間步長,會導致過早停止的視覺效果。所有這些加在一起會產生非常糟糕的用戶體驗。
因此,為了保持平滑、一致的幀率,需要通過最小化物理引擎處理任何給定時間步長所需的時間,來為渲染釋放盡可能多的時間,這適用于最佳情況(沒有移動)和最壞情況(所有對象同時與其它對象發生碰撞)。可以在物理引擎中調整一些與事件相關的特征和值,以避免這些性能缺陷。
二、靜態碰撞器和動態碰撞器
在Unity中,術語"靜態"和"動態"又一個相當極端的命名空間沖突。靜態通常意味著所討論的對象或處理不移動、保持不變或只存在于一個位置,而動態則意味著相反,對象或處理傾向于改變或移動。然而要記住,術語"靜態"和"動態"的用法在每種情況下都不同。
動態碰撞器只意味著GameObject包含Collider組件和Rigidbody組件。通過將Rigidbody添加到Collider所附加的相同對象上,物理引擎會將該碰撞器視為帶有包圍物理對象的立體對象,它會對外部的力和與其他Rigidbody的碰撞體作出反應。如果一個動態碰撞器與另一個動態碰撞器發生碰撞,它們都會基于牛頓運動定律做出反應。
也可以使用沒有附加Rigidbody組件的碰撞器,這種稱為靜態碰撞器。這種碰撞器有效地起到了無形屏障的作用,動態碰撞器可以撞到這些屏障,但是靜態碰撞器不會做出響應。從另一個角度來看,就是把沒有Rigidbody組件的物體想象成具有無窮大的質量。因此,靜態碰撞器非常適合用作全局屏障和其他不能移動的障礙物。
物理引擎自動將動態碰撞器和靜態碰撞器分為兩種不同的數據結構,每種結構都經過優化以處理現有碰撞器的類型。這有助于簡化未來的任務,例如,解析兩個靜態碰撞器之間的碰撞和脈沖。
三、碰撞檢測
Unity中的碰撞檢測有3種設置,可以在Rigidbody組件的Collision Detection屬性中設置Discrete(離散)、Continuous(連續)和ContinuousDynamic(連續動態)。
Discrete設置可以實現離散碰撞檢測,有效地根據物體的速度和經過的時間,在某個時間步長將對象傳送一小段距離。一旦所有對象都被移動了,物理引擎就會對所有重疊執行便捷進行立體檢查,將它們視為碰撞,并根據它們的物理屬性和重疊方式來處理它們。如果小對象移動得太快,此方法可能會有丟失碰撞的風險。
其余的兩個設置都將啟用連續碰撞檢測,其工作方式是從當前時間步長的起始和結束位置,并見哈這個時間段中是否有任何碰撞。這降低了錯過碰撞的風險,生成了更景區的模擬,但代價是CPU的開銷顯著高于離散碰撞檢測。
Continuous設置盡在給定碰撞器和靜態碰撞器之間用連續碰撞檢測。同一碰撞器與動態碰撞器之間的碰撞仍將使用離散碰撞檢測。
同時,ContinuousDynamic設置使碰撞器能夠與所有靜態和動態碰撞器進行連續碰撞檢測,其在資源消耗方面最大。
四、碰撞器類型
Unity中有4種不同類型的3D碰撞器,其性能成本從最小到最大依次為球體(Sphere)、膠囊體(Capsule)、立方體(Box)、網格(Mesh)。
前三個碰撞器類型通常稱為基礎類型。包含非常特殊的形狀,盡管它們通常可以在不同方向縮放以滿足某些要求。網格碰撞器可以根據指定的網格自定義為特定形狀。還有3種類型的二維碰撞器:圓(Circle)、方框(Box)和多邊形(Polygon),在功能上分別與球形、立方體和網格碰撞器相似。以下所有信息基本上都可以轉換為等效的二維形狀。
提示:也可以在Unity中生成3D圓柱體,但這只是它的圖形表現。自動生成的圓柱體使用膠囊體碰撞器表示其物理保衛面積,這可能不會產生預期的物理行為。
另外,有兩種不同的網格碰撞器:Convex(凸的)和Concave(凹的)。兩者的不同之處在于,凹形形狀至少具有一個大于180度的內角,如圖所示:
?提示:區分凹形和凸形的一個簡單方法是凹形至少有一個凹陷。
兩種網格碰撞器類型都使用相同的組件(MeshCollider組件),這種網格碰撞器類型是通過切換Convex復選框選項生成的。啟用此選項將允許對象與所有基本形狀(球形、長方體等)以及其他啟用Convex的網格碰撞器碰撞
此外,如果為凹形的網格碰撞器啟用了Convex復選框,則物理引擎將自動簡化該網格碰撞器,生成的碰撞器具有能將其包圍的最接近的凸形。
在上圖中,如果導入右側的凹形網格并啟用Convex復選框,它將生成一個更接近左側凸形形狀的碰撞器。在這兩種情況下,物理引擎都將嘗試生成一個碰撞器,該碰撞器與附加的網格的形狀匹配,上限為255個頂點。如果目標網格的頂點數超過此值,則在網格生成過程中會引發錯誤。
碰撞器組件還包含IsTrigger屬性,允許將他們視為非物理對象,但當其他碰撞器進入或離開它們時仍調用物理時間。這些稱為觸發體積。通常,當一個碰撞器接觸、保持接觸、或停止接觸時,分別會調用OnCollosionEnter()等三個回調,但當碰撞器用作出發體積時,將調用OnTriggerEnter()等三個回調。
注意:由于處理物體間碰撞的復雜性,凹形網格碰撞器不能是動態碰撞器,只能用作靜態碰撞器或觸發體積。如果試圖將Rigidbody組件添加到凹形網格碰撞器中,Unity將完全忽略它。
提示:如果真的需要將凹形網格碰撞器作為Rigidbody組件,則解決方案是將對象分割成獨立的凸形網格碰撞器的組合。例如,想利用兩個凸形來組合一個L形剛體。不幸的是,因為這是一個微妙的決定,所以沒有自動的方法來實現,需要手動執行這樣的分解。
?五、碰撞矩陣
物理引擎具有一個碰撞矩陣,該矩陣定義允許哪些對象與哪些對象發生碰撞。當處理邊界體積重疊和碰撞時,物理引擎家那個自動忽略不適合此矩陣的對象。這節省了碰撞檢測階段的物理處理,還允許對象彼此移動而不發生任何碰撞。
碰撞矩陣可以通過Edit | Project Settings | (Physics/Physice2D) | Layers Collision Matrix訪問。
碰撞矩陣系統通過Unity的層(Layer)系統工作。矩陣表示層與層之間的組合,啟用復選框意味著在碰撞檢測階段將檢查這兩個層中的碰撞器。
要注意的是,對于整個項目,總共只能有32個層。
六、Rigidbody激活和休眠狀態
每一個現代物理引擎都有一個共同的優化技術,即靜止物體的內部狀態從活動變為休眠。當Rigidbody處于休眠狀態時,在固定的更新過程中,處理器幾乎沒有時間來更新對象,直到它被外力或碰撞事件喚醒。
用于確定靜止狀態的測量值,在不同的物理引擎中往往會有所不同,可以使用Rigidbody的線速度和角速度、動能、棟梁或其他一些物理屬性來計算。Unity的兩個物理引擎都是通過評估物體的質量歸一化動能來工作的,這基本上可以取決于物體速度平方的大小。
如果物體的速度在短時間內沒有超過某個閾值,那么物理引擎將假設物體在經歷新的碰撞或施加新的力之前不再需要再次移動。在此之前,休眠對象將保持其當前位置。可以在Edit | Project Settings | Physice | Sleep Threshold下修改閾值,還可以從Profiler窗口的Physics Area中獲取活動Rigidbody對象的總數。
七、射線和對象投射
物理引擎的另一個常見特征是能夠將射線從一個點投射到另一個點,并用路徑中的一個或多個對象生成碰撞信息,這就是所謂的射線投射。通過射線投射來實現一些游戲機制是很常見的,比如射擊,其實現方式通常是執行從玩家到目標位置的射線投射,并在其路徑中找到任何符合要求的目標。
還可以通過Physics.OverlapSphere()檢查在空間中固定點的有限距離內獲得的目標列表,這通常用于實現效果區域的游戲功能,如手榴彈爆炸,甚至可以用Physics.SphereCast()和Physics.CapsuleCast()在空間中向前投射整個對象。這些方法通常用來模擬寬激光束,或者只是確定什么東西在移動角色的路徑中。
八、調試物理
物理錯誤通常分為兩類:本來不應該碰撞的一對對象碰撞了;本來應該碰撞的沒碰撞,在碰撞發生之后,發生了意想不到的事情。前一種情況往往更容易調試,通常是由于碰撞矩陣中的錯誤,射線投射中使用的層不正確,或者對象碰撞器的大小或形狀錯誤。后一種情況往往更難解決,因為要獲得以下3條消息:
- 確定哪個碰撞對象導致了問題
- 在解決之前確定碰撞條件
- 重現碰撞
獲得了這3條信息中的任何一條都會使解決方案更容易,但這些信息在某些情況下都很難獲得。
Profiler在Physics和Physics(2D)區域提供了一些測量信息,這是相當有用的,可以得到CPU活動在不通過類型隔離的所有剛體和剛體組上花費的量,這些類型包括動態碰撞器、靜態碰撞器、運動對象、觸發體積、約束和觸點。
Physics 2D區域包含了更多的信息,比如睡眠和活動剛體的數量,以及處理時間步長的時間。在這兩種情況下,詳細的細分試圖提供了更多的信息。這些信息有助于關注物理性能,但它并不能指出在物理行為中出現錯誤時發生了什么。
一個更適合幫助調試物理問題的工具是Physics Debugger,它可以通過Window | Physics Debugger打開。這個工具有助于從Scene窗口中過濾出不同類型的碰撞器,從而更好地了解哪些對象相互碰撞。當然,這對確定問題的條件和復現問題沒有太大幫助。