遊戲開發日誌 #05
這個世界充滿了物理法則,遊戲世界亦一樣。
我們習慣了現實世界的物理環境和慣性,也會把同樣的感知力放進遊戲世界。
若果遊戲世界的物理環境和慣性跟現實世界有出入,就會產生違和感。
不過現實卻是。。。
「即使遊戲引擎使用現實世界的物理法則去運算和表現物理效果,人還是會產生違和感。」
到底是為什麼呢?
物理和感知力
對非進修物理學的人,要說物理現象的話,第一件會想到的相信是重力 Gravity。
我們活在地球的重力中,但地球的重力有多大?
不論重量如何,物件都會以 9.81 m/s2 的加速度向地球的中心移動。
物件沒有真的到達地球中心,全因有承受物件質量的物質存在,例如我們踏著的大地。
但生物的感知力並沒有被物理所規範,生物所感受到的現象有很多特點,而最日常和對遊戲世界最大的一個,就是:
其實在蚊子或小貓小狗的眼中,我們人類的動作有如慢動作一樣。
我們會覺得細小的東西從桌子上掉到地面的速度很快很難捕捉,但其實跟我們從桌子上掉到地面的速度是一樣的。
這個感知力對遊戲世界有很大的影響。
當我們在現實世界看著遊戲世界時,即使遊戲世界做得跟現實世界一樣,我們控制的角色也是跟現實世界一樣的人類也好,我們也會有進入小人國的感知,也就是加速力的不同。
在遊戲的世界,角色的行走速度只要能配合場景和動作就沒有違和感,但如果是牽涉到重力的移動,例如跳動、下墜等動作,就會有角色飄浮而非下墜的感覺。
這也是很多遊戲開發者都說內建的物理引擎不行,效果不好,自己寫的最好,甚至像宗教一樣,硬要去推行自建物理,一有機會就說內建的物理引擎不行。
其實只要使用得宜,盡用內建物理引擎的功能,再加上自行擴建的物理修正,就可以把違和感消除。
這一篇,主要分享針對「重力」方面如何作出修正,也就是所謂的半物理系統。
不論重量如何,物件都會以 9.81 m/s2 的加速度向地球的中心移動。
物件沒有真的到達地球中心,全因有承受物件質量的物質存在,例如我們踏著的大地。
但生物的感知力並沒有被物理所規範,生物所感受到的現象有很多特點,而最日常和對遊戲世界最大的一個,就是:
- 大的生物會覺得小的生物移動速度快,小的生物會覺得大的生物移動速度慢。
相對大小越大,速度感差會越大。
其實在蚊子或小貓小狗的眼中,我們人類的動作有如慢動作一樣。
我們會覺得細小的東西從桌子上掉到地面的速度很快很難捕捉,但其實跟我們從桌子上掉到地面的速度是一樣的。
這個感知力對遊戲世界有很大的影響。
當我們在現實世界看著遊戲世界時,即使遊戲世界做得跟現實世界一樣,我們控制的角色也是跟現實世界一樣的人類也好,我們也會有進入小人國的感知,也就是加速力的不同。
在遊戲的世界,角色的行走速度只要能配合場景和動作就沒有違和感,但如果是牽涉到重力的移動,例如跳動、下墜等動作,就會有角色飄浮而非下墜的感覺。
這也是很多遊戲開發者都說內建的物理引擎不行,效果不好,自己寫的最好,甚至像宗教一樣,硬要去推行自建物理,一有機會就說內建的物理引擎不行。
其實只要使用得宜,盡用內建物理引擎的功能,再加上自行擴建的物理修正,就可以把違和感消除。
這一篇,主要分享針對「重力」方面如何作出修正,也就是所謂的半物理系統。
Physics Engine 物理引擎
在 Unity 的物理管理員中,可以看到一些參數。
先把沒用的東西關掉,以降底物理運算。
影片中的場景中有三個上坡,角度從左到右是 60, 45 25。
在影片的前半,使用內建物理引擎的重力設定,會看到角色上坡時速度減慢,從高處墜下時也有飄浮感;當轉到自訂的重力修正就沒問題了。
在影片的後半,沒有重力修正時,角色是以物理的加速力來計算移動,所以中間的 45 度上坡也可以爬上去,但左邊的 60 度就不行;當使用自訂的重力修正後,由於有設置上坡限制,所以中間的上坡也爬不上去。
先把沒用的東西關掉,以降底物理運算。
- World Bounds:在確保世界的中心是 0,0,0 後,Extent 就是世界的半徑了,把這個設置成遊戲世界的大小,再加大多 10 就可以。
- Layer Collision Matrix:是剛體 Rigidbody 的碰撞偵測,把不須要碰撞的 Layer 關掉。
在角色的中,把 Rigidbody 的 Use Gravity 關掉,我們不使用引擎的重力。
加入自己的 CustomPhysics.cs 來作重力修正。
把角色的 Collider 拉到 CustomPhysics 中的 Col。
其他的東西保留就可以。
加入自己的 CustomPhysics.cs 來作重力修正。
把角色的 Collider 拉到 CustomPhysics 中的 Col。
其他的東西保留就可以。
Custom Physics 自定義物理
這次進行的物理修正,只針對以下幾項:
- 重力:不需要重力時就沒有重力
- 上坡移動:上坡角度限制、上坡重力修正
- 意外離地重力修正
- 下墜重力修正
Sample Video 示範影片
影片中的場景中有三個上坡,角度從左到右是 60, 45 25。
在影片的前半,使用內建物理引擎的重力設定,會看到角色上坡時速度減慢,從高處墜下時也有飄浮感;當轉到自訂的重力修正就沒問題了。
在影片的後半,沒有重力修正時,角色是以物理的加速力來計算移動,所以中間的 45 度上坡也可以爬上去,但左邊的 60 度就不行;當使用自訂的重力修正後,由於有設置上坡限制,所以中間的上坡也爬不上去。
Coding 程序碼
*有一些小修正
<必要元件 Components>
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]
首先,必要的 Components 是 Rigidbody 和 Capsule Collider 碰撞器。
因為只對重力作修正,保留了內建的碰撞偵測,所以剛體是必要的,在半物理系統下使用 Rigidbody 移動也是最理想的。
需要 Collider 的原因,為了自動取得物件的半徑,也可以在今後做自訂碰撞偵測作準備,需然沒有這個打算。
<參數 Parameters>
[Header("Settings")]
public LayerMask groundMask;
public float radius = 0f;
public float colliderSkin = 0.05f;
public float gravity = 50f;
public float maxSlope = 45f;
[Header("Components")]
public CapsuleCollider col;
private Rigidbody rb;
[Header("Public Parameters")]
public bool isGrounded;
public float inAirHeight;
// Others
// You can public following variables for your use.
private float currentGravity;
private bool isFalling = false;
private bool isClimbing = false;
private bool IsMoving
{
get { return rb.position - oldPosition != Vector3.zero; }
}
private bool isTouchingGround;
private float radius;
private float slopeAngle;
private float platformAngle;
private Vector3 oldPosition;
private List colGround;
private RaycastHit hit;
private Vector3 RayPos
{
get { return rb.position.y(col.bounds.min.y); }
}
設定參數:
- groundMask:Ground Layer
- colliderSkin:物件碰撞器的皮膚厚度
- gravity:自訂重力
- maxSlope:上坡上限角度
- col:碰撞器,由於用了萬用碰撞器,所以必須自己在 Inspector 中設置。
分享參數:
- isGrounded:是否著地
- inAirHeight:離地距離
<初始化 Initialization>
private void Awake()
{
if (!rb)
rb = GetComponent();
if (!col)
col = GetComponent();
if (radius == 0)
radius = col.bounds.extents.x;
}
生成時,設好參數 rb、col 和 radius。
radius 是物件碰撞器的半徑。
<每物理循環 Every physics cycle>
private void FixedUpdate()
{
// Detection
CheckGround();
CheckPlatform();
CheckSlope();
// Gravity Adjustment
Climbing();
Falling();
// Record
currentGravity =
Mathf.Clamp(-rb.velocity.y, 0, float.MaxValue).ToZero(0.0001f, false);
// For next cycle
oldPosition = rb.position;
}
由於對 Rigidbody 進行修正,所以在 FixedUpdate 中執行。
這裡先做好偵測後,再進行重力的調整。
要執行的五件事情:
。CheckGround()
private void CheckGround()
{
// Check grounded and height
Vector3 pos = RayPos.y(RayPos.y + colliderSkin);
Ray rayDown = new Ray(pos, Vector3.down);
Physics.Raycast(rayDown, out hit, 10f, groundMask);
inAirHeight = hit.transform ?
hit.distance.ToZero(0.0001f, false) : 10f;
// Check touching ground
colGround = new List();
pos = RayPos.y(RayPos.y + radius);
foreach (Collider col in Physics.OverlapSphere(pos, radius + colliderSkin, groundMask))
{
if (!colGround.Contains(col.transform))
colGround.Add(col.transform);
}
isTouchingGround = colGround.Count > 0;
isGrounded = inAirHeight < 0.15f || isTouchingGround;
}
檢查是否著地。(有修訂)- 在物件底部高一點(皮膚厚度)向下射出一條 10 長度 Ray,只偵察地面 layer,計算物件和打中地面的距離,套用在參數 (float)inAirHeight。
- 用一個 OverlapSphere,只偵測地面 layer,位置在設在角色底部拉高角色碰撞器的半徑,而半徑則是角色碰䃥器的半徑加 colliderSkin,這樣角色的 Sphere Collider 的半圓底部就有皮膚厚度去偵測全角度的地面,無論是上坡下坡也可以,把結果套用在 (bool)isTouchingGround。
- 如果 inAirHeight < 0.15f 或 isTouchingGround 的話,就設定 isGrounded 為 true 著地。
。CheckPlatform()
private void CheckPlatform()
{
Vector3 pos = RayPos.y(RayPos.y + colliderSkin);
Ray rayDown = new Ray(pos, Vector3.down);
Physics.Raycast(rayDown, out hit, 0.20f, groundMask);
platformAngle = hit.transform ?
Vector3.Angle(hit.normal, Vector3.up) : 0;
}
檢查腳下地面的角度。- 在物件底部高一點(皮膚厚度)向下射出一條 0.2f 長度 Ray,只偵察地面 layer,計算著地地面的角度,套用在參數 (float)platformAngle。
。CheckSlope()
private void CheckSlope()
{
Ray rayFront = new Ray(RayPos, rb.velocity.y(0));
Physics.Raycast(rayFront, out hit, radius, groundMask);
slopeAngle = hit.transform ?
Vector3.Angle(hit.normal, Vector3.up) : 0;
}
檢查前方地面的角度。- 在物件底部高一點(皮膚厚度)向前射出一條比半徑長的 Ray,只偵察地面 layer,計算前方地面的角度,套用在參數 (float)slopeAngle。
。Climbing()
private void Climbing()
{
float moveDistance = Vector3.Distance(rb.velocity.y(0), Vector3.zero);
if (slopeAngle > 0)
{
if (slopeAngle < maxSlope)
{
isClimbing = isTouchingGround;
if (isTouchingGround)
rb.velocity = rb.velocity.y(
Mathf.Sin(slopeAngle * Mathf.Deg2Rad) * moveDistance);
}
else
{
isClimbing = false;
rb.position = oldPosition;
rb.velocity -= -transform.forward * moveDistance;
}
}
else
{
isClimbing = IsMoving ? false : platformAngle > 0;
}
}
上坡重力修正。(有修訂)角色的移動是水平的,因為碰撞到上坡的地面,才被擠壓到更高的位置,這樣會產生很大的阻力,讓角色的加速力大減,角色的移動速度就會大減。
- 當 slopeAngle 大於 0 時,以上坡的角度和前進的距離來計算向上的逆重力,讓角色可以在上坡時的速度變得正常。
角色並不是什麼上坡都可以爬上去,總是有一個限度,所以上坡大過 maxSlope 的話,就不會進行動力修正,會把 rigidbody 回到上一偵的位置。 - 以 (bool)isClimbing 記錄是否上坡中。由於 CheckSlope() 在角色靜止時沒法偵測前方,slopeAngle 就會等於 0,所以當 slopeAngle 等於 0 時,就要用 (bool)IsMoving 檢查角色是否移動中,false 的話 isClimbing 就要看 platformAngle 是否太於 0。
- 由於 CheckSlope() 只會檢查前方地面,所以下坡時 slopeAngle 就會等於 0,isClimbing 就會記錄正確狀態。
- 由於往前偵測上坡時,角色和上坡有一點距離,所以角色會提早被升高。解決辦法是利用 isTouchGround 來決定是否提升和設定 isClimbing,當人物被提升但還沒有到達上坡時,下墜修正就會正常運作,人物就自然會貼地。
。Falling()
private void Falling()
{
if (!isTouchingGround)
{
isFalling = !isGrounded;
rb.velocity += Vector3.up * -gravity * Time.fixedDeltaTime;
}
else
{
isFalling = false;
}
}
下墜重力修正。(有修訂)- 只要不是 isTouchingGround 的話,就有必要提供重力讓角色下墜,否則角色就會懸浮在半空中。
- 使用 (bool)isFalling 記錄是否墜下中,但不能把角色的意外懸浮當成是墜下,所以在需要重力修正時,由 isGrounded 來決定是墜下中還是意外懸浮的修正,因為 isGrounded 在 inAirHeight 少於 0.15f 都會是 true。
<重力的數值計算>
在這個重力修正中,有一個參數是 Gravity,我們必須自己設定,那要如何計算應該的重力呢?有個比較簡單的方法去計算,就是利用跳躍,這裡需要兩個數值:
- Jump Height (m) 跳躍高度 (米)
- Time to Apex (s) 到頂點所需時間 (秒)
假設跳躍高度是 1 米,所需時間是 0.2 秒的話。。。
- Gravity = (2 * Jump Height) / Time to Apex2
- G = (2 * 1) / 0.22
- G = 2 / 0.04
- G = 50
<System Class>
在程序中需要用到本人的 system class 內的 method,請在下面下載並放到閣下的 project 中。
Conclusion 總結
使用重力修正,有助遊戲中物件的物理運動效果變得更自然,也有助降低物理引擎的運算量。
這次的分享,也可以再補正到其他遊戲中,但先好好了解內建物理引擎的運作,否則只會增加物理運算量,對遊戲的整體性能有很大的影響。
這次的分享,也可以再補正到其他遊戲中,但先好好了解內建物理引擎的運作,否則只會增加物理運算量,對遊戲的整體性能有很大的影響。
留言