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 神一般的狄狄