Kinematic
遊戲裡角色的操作大多是 Kinematic 的,若不用 Kinematic 的設定,會發現角色其實動起來太真實了,會走不動,撞到東要就翻人倒了或轉來轉去,這樣太真實的物理用在角色上其實一點也不好玩,怎麼辨?自己用簡單的方式計算吧!
重力
重力,以前大家都學過吧?沒學過(沒學好?)也該聽過吧? 在這裡我們不需要精準的物理運算,因為太精準反而會讓遊戲不好玩。 這裡列一下計算物體落下需要那些東西:
- 重力: gravity
- 速率: velocity
- frame 與 frame 之間的時間差: deltaTIme
- 重力加速度: gravity m/s2
有了上面這些就成功一半了。每個 frame 可以由 gravity 計算出 velocity,時間越長 velocity 越大。
//Vector3 velocity. //float gravity. //float deltaTime. velocity.y += gravity * deltaTime;
再來我們就可以用 velocity 算出位移量了
//Vector3 deltaMove. deltaMove = velocity * deltaTime
如果我們直接把 deltaMove 拿給 GameObjet 用,我們就能得到一個會一直往下掉的物件了!
//Vector3 deltaMove. gameobject.Translate(deltaMove, Space.World);
這樣就完成重力的設計了!再來重力要設多大才好呢?這取決於你的場景物件有多大。比如我們的人物很小重力設很大,它會看起像是快速掉落;若我們的人物很大重力設很小,它就會看起來像是慢慢的掉落。這都是因為視覺上相對關係所造成的,所以請調出合適自己的數值吧!
物體是不會因為質量、重量大而掉落得比較快的!
與地面碰撞
只會往下掉的東西跟本不能玩阿!要怎麼停下來? 這裡需要:
- BoxCollider
- Physics.Raycast
開始前先來張圖解: 一個方塊由 “開始的位置" 落至 “掉落的位置",最後再修正為 “最終的位置",圖中的定義如下:
- halfBoundY: 方塊的碰撞高度的一半。
- deltaMove:這個 frame 移動長度。
- rayLength:由方塊中心打出去的 Ray 長度。
- RaycastHit:由方塊中心打出去的 Ray 擊中的位置長度資訊。
首先建立一條 Ray ,因為 Collider 可以微調 center,所以 Ray 的起始位置要加上 center。
Vector3 raystart = transform.position + myCollder.center * transform.localScale.y;
Ray 的射向由 velocity.y 來決定,負的就是往下射,正的則往上射。
float updown = 1; if (velocity.y < 0) { updown = -1; } Vector3 raydir = Vector3.up * updown;
Ray 的長度由移動量與本身的碰撞高度決定,這樣子的算法可以有效防止體物穿插。
BoxCollider myCollder = GetComponent<BoxCollider>(); float halfBoundY = (myCollder.size.y * 0.5f) * transform.localScale.y; float rayLength = Mathf.Abs(deltaMove.y) + halfBoundY;
接下來就可以做 Raycast 了,在我們行進的過程中若有打中東西,就修改移動量停在東西前,不會穿插過去;若沒有打中則保持原移動量。接著移動過去。因為 Ray 是從自己中心打出去,所以會打到自己的 collider,在這裡使用 tag 來做區分防止打到自己的 collider。
RaycastHit rhf = new RaycastHit(); if (Physics.Raycast(raystart, raydir, out rhf, rayLength)) { if (rhf.collider != null && rhf.collider.gameObject.tag == "ground") { deltaMove.y = rhf.point.y - raystart.y + halfBoundY * -updown; } }
移動可以用:
- transform.Translate()
- Rigidbody.MovePosition()
在這裡用 transform.Translate() 意思一下就好。
gameObject.transform.Translate(deltaMove, Space.World);
當我們都移動完成了,最後再做一次速率的校正。這個方法是由 3DBuzz 所提出的,我覺得這方法很不錯,大家參考看看。就這樣完成了簡易的重力及碰撞了。
if (deltatime > 0) { velocity = deltaMove / deltatime; }
以上要在 LateUpdate() 裡計算,這樣子物件的 transform 才會是這個 frame 的值。
跳躍
東西只會落下至地面,好像無聊了點,我們再加個跳躍好了。
public void AddForce(Vector3 force) { velocity += force; } void HandleKeyInput() { if (Input.GetKey(KeyCode.W)) { AddForce(new Vector3(0, 1, 0)); } }
呼~我們終於親手完成了重力,給自己一個掌聲!趕快按下 Play 感受一下自己寫的重力吧!
下一篇應是水平移動了吧?
完整程式碼:
using UnityEngine; using System.Collections; public class MyPlayerScript : MonoBehaviour { public float gravity = 9.8f; public Vector3 velocity = Vector3.zero; public Vector3 deltaMove = Vector3.zero; public float halfBoundY; public float disthit; public BoxCollider myCollder; public Vector3 addconstforce = Vector3.zero; // Use this for initialization void Start () { myCollder = GetComponent(); halfBoundY = (myCollder.size.y * 0.5f) * transform.localScale.y;; } // Update is called once per frame void Update () { HandleKeyInput(); } void LateUpdate () { float deltatime = Time.deltaTime; UpdateGravity(deltatime); UpdateMovement(deltatime); } void UpdateGravity(float deltatime) { velocity.y += gravity * deltatime;; } void UpdateMovement(float deltatime) { deltaMove = velocity * deltatime; RaycastHit rhf = new RaycastHit(); Vector3 raystart = transform.position + myCollder.center * transform.localScale.y;; float updown = 1; if (velocity.y < 0) { updown = -1; } Vector3 raydir = Vector3.up * updown; float rayLength = Mathf.Abs(deltaMove.y) + halfBoundY; Vector3 debugRayEnd = new Vector3(0, rayLength * updown, 0); Debug.DrawLine(raystart, raydir + transform.position + debugRayEnd, Color.yellow); if (Physics.Raycast(raystart, raydir, out rhf, rayLength)) { if (rhf.collider != null && rhf.collider.gameObject.tag == "ground") { deltaMove.y = rhf.point.y - raystart.y + halfBoundY * -updown; } } gameObject.transform.Translate(deltaMove, Space.World); //recalute velocity by final deltaMove. if (deltatime > 0) { velocity = deltaMove / deltatime; } } public void AddForce(Vector3 force) { velocity += force; } void HandleKeyInput() { if (Input.GetKey(KeyCode.W)) { AddForce(addconstforce); } } }
最後加分:
- 可以把重力的值移出去至全域的 class 裡,讓整個遊戲共用一個重力。
- 承上,再加上物件自己本身的空氣阻力,讓不同物件有不同的落下速度。
- 承上,空氣阻力可以分為向上及向下二種,讓物件往上跳與落下時有不同的表示。
- 角色的邊緣應也要打射線,才不會造成明明角色的邊緣有踩到物件,但還是會落下的現像。至於要打多少條?自己在效能與結果上衡量吧。
參考:
對「設計我們的遊戲物理-簡易重力篇」的一則回應