設計我們的遊戲物理-簡易重力篇

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

開始前先來張圖解: 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 裡,讓整個遊戲共用一個重力。
  • 承上,再加上物件自己本身的空氣阻力,讓不同物件有不同的落下速度。
  • 承上,空氣阻力可以分為向上及向下二種,讓物件往上跳與落下時有不同的表示。
  • 角色的邊緣應也要打射線,才不會造成明明角色的邊緣有踩到物件,但還是會落下的現像。至於要打多少條?自己在效能與結果上衡量吧。

參考:

對「設計我們的遊戲物理-簡易重力篇」的一則回應

發表留言