天天看點

Unity下,移動撞牆後抖動的解決方案

Unity下的移動方案:

1.Rigidbody.MovePosition

2.Rigidbody.AddForce

3.Transform.Translate;

Transform.position = vector3;

目前主要分這三大類的移動方式。

1和2是實體移動方式

3是實體對象坐标的移動方式

然後說說題目,為啥會抖動呐:

public class Test : MonoBehaviour
{
    public float m_nSpeed = 0;
    void Update()
	{
        transform.Translate(Vector3.forward * m_nSpeed * Time.deltaTime);
	}
}
           

通常,你會把移動寫成這樣。

當然沒有錯,因為我們相信API。官方API上就是這麼寫的API Demo:https://docs.unity.cn/cn/current/ScriptReference/Transform.Translate.html

但是實際上移動時候,我們為了做碰撞,會在場景牆壁上挂上Collider。 主角身上也會挂上Collider和Rigidbody。

主角和場景之間通過碰撞,可以有阻擋的表現。

然後就發現了!瘋狂抽搐!

來分析一下原因

先引用一下大佬的連結,Unity的生命周期:

Unity 腳本生命周期流程圖

FixedUpdate會先走,之後才會走到Update。

那麼想一想,上面的代碼 Translate把物體的坐标改變了。

那麼碰撞體跟着物體進行了移動。但是在目前這一次的生命周期循環裡,Update之後沒有實體判斷了。

那就是說,在這一幀的畫面,最終渲染出來的時候。主角的碰撞體是嵌入在牆壁裡面的。

那麼下一幀,走了FixedUpdate進行了實體判斷,發現碰撞體是嵌入的。那麼按照實體規則,實體引擎把人物給彈出來保證實體正确。

于是乎,反複的移動操作,實際上就是 碰撞體嵌入,擠出,嵌入,擠出。 我們就看到了人物撞牆一直在抖動。

那麼來看看解決方案:

我們要解決的問題是

在人物移動後,可以讓實體系統可以在渲染前,得出正确的結果。

目前我們有2個點 Update和FixedUpdate需要處理。

先得了解FixedUpdate是實體幀,走定時器,并不是一次FixedUpdate和一次Update對應。相關的資料:

https://www.cnblogs.com/murongxiaopifu/p/7683140.html

其實就很容易了解了。我們盡量不要讓移動的操作同時經過Update和FixedUpdate處理,混合處理容易出現時序問題,很難查問題,因為Update的時間間隔是不穩定的。就挑一個位置來處理移動。

[SerializeField] float m_nSpeed = 0;
	void Update()
    {
        float nDis = m_nSpeed * Time.deltaTime;
        Vector3 v3Dis = transform.forward * nDis;
        //這邊是重點
        Vector3 from = transform.position;
        from.y += 0.5f;
        Vector3 to = from + v3Dis;
        to.z += 0.5f;

        RaycastHit rh;
        Debug.DrawLine(from, to, Color.black);
        if (Physics.Linecast(from, to, out rh, LayerMask.GetMask("wall")))
        {
            v3Dis = rh.point - from;
            v3Dis.z -= 0.5f;
        }
        //重點結束

        transform.Translate(v3Dis, Space.Self);
	}
           

解釋一下這段代碼:

移動速度 和 Translate 沒啥可以多說的了。

主要是重點這一段,計算出目前移動距離後,并不着急去Translate移動,先按照移動距離向前發射一根射線,如果射線碰撞到了牆

那麼我就可以知道,這次移動如果移動滿距離必定會嵌入牆體,那麼我隻需要移動可以移動的最大距離,就可以貼着牆,并且不嵌入牆體。

再來想一想下一幀,射線必然是碰撞的,算出的結果,必然是0.那麼下一幀,就會移動0距離。

不嵌入牆體,實體系統不會把碰撞體給擠出來。于是就不抖動了。

PS:這段代碼裡面的 y軸 0.5f 其實可以忽略,看自己情況而定。因為我測試時候的模型中心點在腳底。

z軸 0.5f,是因為我的模型上挂的膠囊碰撞體,半徑是0.5f。

上面是一個方案,然後基于在人物移動後,可以讓實體系統可以在渲染前,得出正确的結果我們可以想到,上面的方案的主要邏輯是在Update裡面處理的。那我忽略Update,全部在FixedUpdate裡面處理,是否可行。

開頭的兩個函數:

1.Rigidbody.MovePosition

2.Rigidbody.AddForce

可以在FixedUpdate裡面解決這個問題。

首先Rigidbody.MovePosition官方文檔裡面有說道:

如果在剛體上啟用了剛體插值,則調用 Rigidbody.MovePosition 會導緻在渲染的任意中間幀中的兩個位置之間平滑過渡。若要在每個 FixedUpdate 中連續移動剛體,則應使用該方法。

官方文檔連結:https://docs.unity.cn/cn/current/ScriptReference/Rigidbody.MovePosition.html

None 不使用插值。

Interpolate 插值将始終滞後一點,但比外推更流暢。

Extrapolate 外推将根據目前速度預測剛體的位置。

官方文檔連結:

https://docs.unity.cn/cn/current/ScriptReference/RigidbodyInterpolation.html

官方還寫了Demo,我就偷懶不寫了。

上面這個方法,我自己是覺得心裡空唠唠的,不是很靠譜。主要是因為我們的遊戲邏輯很多是在Update裡面運作,很多人在做移動的時候,可以算出最終的移動距離和坐标,需要經過複雜的計算。FixedUpdate下,性能 以及 Update的資料互動,可能都會很難以去處理。

如果隻是單純的移動,也許可以嘗試,但是FixedUpdate的方案下,我更喜歡用Rigidbody.AddForce的方案去替換上面的MovePosition的方案,因為AddForce不需要使用Rigidbody的插值功能。

[SerializeField] float m_nSpeed = 0;

	private void FixedUpdate()
	{
        transform.GetComponent<Rigidbody>().AddForce(Vector3.forward * m_nSpeed * Time.fixedDeltaTime);
	}
           

很簡單的代碼,FixedUpdate裡用力去推剛體讓他前進。

效果反而更好,用力會有加速度的效果。

看上去反而更加自然了。

并且在FixedUpdate裡操作後,實體引擎會在C#代碼結束後處理碰撞,在渲染前,實際位置已經是被擠出牆壁的位置。是以渲染時候完全不會抖動。

但是用力的方案,出現的加速度問題,可能是需要關注的一個點。

因為停下來的時候,可能會有慣性,和動畫不比對了,看上去就是人滑了一段距離。

那麼可以用

Rigidbody.velocity 速度來處理。

AddForce後,會直接表現在velocity上面

我們F12檢視這個API,可以發現

// 摘要:

// The velocity vector of the rigidbody. It represents the rate of change of Rigidbody

// position.

public Vector3 velocity { get; set; }

Rigidbody.velocity這個東西包含了Set

那麼:

Rigidbody.velocity = Vector3.zero;
           

停止AddForce後,所有方向的速度全部歸零,剛體就會立即停止.

最後, 如果你發現螢幕還在抖動,記得檢查一下你的錄影機腳本。

大多第一人稱,第三人稱,都會把錄影機綁在人物節點下。

如果有錄影機相關的邏輯,記得把操作放到LateUpdate下。

原因其實和 FixedUpdate一樣,是時序問題。

特别是LookAt。本篇就不多贅述了。

程式學無止盡。

歡迎大家溝通,有啥不明确的,或者不對的,也可以和我私聊

我的QQ 334524067 神一般的狄狄