在 Unity 用途廣泛的數值彈簧 - Numeric Springing
今天來聊聊一個很數學、很物理的東西:彈簧。
做遊戲難免會遇到的優化問題,就是玩家操控腳色的流暢性,一個腳色在玩家的操控下走走停停、跑跑跳跳,光是如何讓物件的 position 可以流暢且自然,就是個大課題。或者在外部操控器的訊號跟物件的移動之間的協調,如何讓物件的移動有著 “起承轉合” 般的真實動感,在 VR 遊戲上會大幅影響玩家的舒適度,以及對遊戲的接受度。
有個方式可以處理上述的議題,就是套用 “數值彈簧” (numeric springing) 在物體的移動上,產生起步的加速、停留的減速,減少物體在移動時讓然覺得卡頓或者突兀的感覺。“數值彈簧” 是個我一直想要理解與實作的東西,如今終於動工了,這篇文章分享的是我的作法,以及 數值彈簧 的廣泛用途。
數值彈簧簡介
數值彈簧 (numeric springing) 本身的概念並不複雜,就只是個在數學上進行模擬的彈簧系統,不過利用彈簧系統的特性,可以使得物件的移動非常接近實際物理(啟動時加速、停止時減速),而且不需要使用複雜的物理引擎,沒有力道、質量、加速度等概念需要去調整,便可以達到相當好的效果。有許多數據及研究指出,使用等速的移動方式來進行移動動畫,會讓觀看者產生輕微的不適感(如 3D 暈),有著真實物理的加減速,能延長觀看者沉浸在數位內容中的時間,這方面 數值彈簧 可以在很高程度上產生需要的效果。
數值彈簧的使用範圍很廣泛,這邊我實作的是 阻尼系統 (damping),用 “拖拉” 的方式來移動物件,讓物件的動畫可以更加自然,同時可以排除掉玩家操作上或操控器訊號中的 “震盪”。用個實際例子來比喻,就是類似用彈簧秤拖著一個砝碼的效果,只是這邊的彈簧秤是數據模擬而不可視的,而且彈簧秤的長度大小為 0,物件 (砝碼) 會確實地被拖動到指定位置。
除了用於腳色或物件的移動動畫上,這個阻尼式的彈簧系統還能用於很多地方。例如:
- 游標的移動:使用搖桿進行遊戲,如果有使用游標的需要時
- 旋轉動畫的優化:物件旋轉角度的加減速,也可以讓動畫看起來更自然
- 放大與縮小:透過彈簧的參數設定,也能產生放大過頭再回縮的效果,讓放大縮小的動畫顯得更 “軟Q”
- Tweening 的優化:google 的 design guideline 中強調,有著加減速的 UI 特效會優於等速移動的特效
阻尼系統的數學原理
主要的實作部分與參考連結中的內容大同小異,不過我還是用自己的方式研究了一下相關的物理及數學。首先,實作數值彈簧主要依賴的便是彈簧的運動方程式,其中 xt
是彈簧系統的原點:
然後為了在 Unity 中使用,將方程式改寫成這樣,其中 x
相當於物件的目前位置,v
是暫存的移動變化量(速度),xt
是希望移動到的位置,Δt
則是 Update 的間隔時間:
數學部分就點到為止,利用上面兩條式子,先解出 Δv
後便可以得到 Δx
,以及下一禎畫面該將物件移動到何處了 (x + Δx)
。
除了物件的當前狀況(位置及速度),求出來的下一禎位置還跟兩個參數有關,分別是彈簧頻率 (ω)以及阻尼係數 (ζ),兩者的數值通常會在這個阻尼系統宣告時便設定好,後續會影響物件是用怎樣的移動曲線來到達指定的目標位置。彈簧頻率與彈簧強度正相關,數值越高則代表物件移動到指定位置的時間越短,但是對於震盪的敏感度也會提高;阻尼係數是一個避免彈簧來回震動的阻力,通常設為 1 是最通用的,依照需要也可改成其他數值,影響如下列及附圖:
- 阻尼係數 = 1:最通用的設定,又稱為臨界阻尼,物件可以平穩的移動到指定位置。
- 阻尼係數 > 1:阻力偏大,物件會花上較多時間移動。
- 阻尼係數 < 1:阻力偏小,不只會快速移動到指定位置,還會移動過頭而來回震盪,再漸漸停止。
- 阻尼係數 = 0:無阻力,會變成簡諧運動,不會停止。
由I, Sandycx,創用CC 姓名標示-相同方式分享 3.0,https://commons.wikimedia.org/w/index.php?curid=2325619
Numeric Spring 程式碼
1 | using UnityEngine; |
- Line 13:宣告 NumericSpring3D 這個類別時需要給予 物件的初始位置,並設定 彈簧頻率、**阻尼係數 **及 靈敏度。
- 彈簧頻率 (m_fFrequency) 的值越大,物件移動到目標位置的速度就越快,但也越容易受到震動影響。
- 阻尼係數 (m_fDumpingRatio) 的值預設為 1,通常不用改變,調整的效果於前段描述過了。
- 靈敏度 (m_fSensitivity) 用於避免無謂的細微運算,當物件距離目標位置小於靈敏度的時候,便結束阻尼運作。
- Line 24:設定要移動的目標位置,可以隨時隨地設定目標讓阻尼系統跟上動作,不必等到阻尼系統結束動作。
- Line 28:通常在 Update() 裡呼叫,取得物件應該顯示的位置,達到平滑移動的效果。
Numeric Spring 使用範例
1 | using UnityEngine; |
- Line 9~10:宣告類別,同時設定初始位置以及彈簧頻率,如果發現移動好像慢一拍便可以提高頻率試試。
- Line 17:設定最新的移動目標,不論彈簧運作到何處,是否已經停止都可以設定新的目標。
- Line 18:取得計算後的顯示位置,並將物件移至該處,得到彈簧系統的移動效果。
後話
- 在我的專案上,套入數值彈簧後,腳色物件的移動轉瞬間平滑了起來,讓我相當開心沒有看走眼,果然數值彈簧是相當重要的一個部分。不過要在移動速度與震盪之間取得一個良好的平衡,需要反覆測試調整彈簧頻率,甚至要加上其他措施。
- 未來我應該還會繼續研究數值彈簧在各處上的應用與優化,也會有不同的彈簧系統被實做出來,到時候會有更進一步的分享。
- Unity 的物理引擎中有個 Spring Joint 的功能,在某些部分與本文範例的目的是相似的,有興趣的人可以自行研究看看。我之所以另外實作一個類別,是因為我想將這個類別運用在各處,不只是移動物件,旋轉或縮放物件都可以使用它。
- 另外還有 Vector3.SmoothDamp() 這個函式也有類似的功能。
參考
- Game Math: Precise Control over Numeric Springing
- Game Math: Numeric Springing Examples
- Damping - Wikipedia
附錄:float 版本的 Numeric Spring
上面的 NumericSpring3D 是使用 Vector3 作為核心,同時也可用於 Vector2 也沒問題,不過要用於單一數值就有點過頭了,所以有了下方的一維版本,以 float 作為核心。
1 | using UnityEngine; |