????????很多童鞋沒有系統(tǒng)的Unity3D游戲開發(fā)基礎(chǔ),也不知道從何開始學(xué)。為此我們精選了一套國外優(yōu)秀的Unity3D游戲開發(fā)教程,翻譯整理后放送給大家,教您從零開始一步一步掌握Unity3D游戲開發(fā)。?本文不是廣告,不是推廣,是免費(fèi)的純干貨!本文全名:喵的Unity游戲開發(fā)之路 - 移動(dòng)?-?自定義重力?- 在球體上行走
自定義重力
支持任意重力。
使用可變的上軸。
將所有內(nèi)容拉到一個(gè)點(diǎn)。
將自定義重力應(yīng)用于任意物體。
這是有關(guān)控制角色移動(dòng)的教程系列的第五部分。它涵蓋了使用自定義方法替換標(biāo)準(zhǔn)重力的方法,通過該方法,我們支持在球體上行走。
本教程使用Unity 2019.2.21f1制作。它還使用ProBuilder軟件包。
效果之一
探索一個(gè)小小的星球。
可變重力
到目前為止,我們一直使用固定的重力矢量:垂直往下9.81。這對(duì)于大多數(shù)游戲而言已足夠,但并非全部。例如,目前無法在代表行星的球體表面上行走。因此,我們將添加對(duì)自定義重力的支持,而不必統(tǒng)一。
在變得復(fù)雜之前,讓我們開始簡(jiǎn)單地翻轉(zhuǎn)重力,并通過項(xiàng)目設(shè)置使重力矢量的Y分量為正,看看會(huì)發(fā)生什么。這有效地將其變成反重力,這應(yīng)該使我們的球體向上掉落。
事實(shí)證明,我們的球體確實(shí)向上飛行,但最初緊貼地面。那是因?yàn)槲覀冋趯⑵湮降降孛妫⑶椅覀兊拇a假定法向重力。我們必須對(duì)其進(jìn)行更改,以便它可以與任何重力矢量一起使用。
向上軸(Up Axis)
我們依靠向上軸始終等于Y軸。為了放開這個(gè)假設(shè),我們必須向MovingSphere添加一個(gè)上軸字段,并使用該字段。為了支持隨時(shí)變化的重力,我們必須在FixedUpdate的起點(diǎn)設(shè)置上軸。它指向與重力相反的方向,因此它等于取反后的歸一化重力矢量。
Vector3 upAxis;
…
void FixedUpdate () { upAxis = -Physics.gravity.normalized; … }
現(xiàn)在我們必須用新的上軸替換所有的Vector3.up用法。首先,在UpdateState中,當(dāng)球體處于空中時(shí),我們將其用作接觸法線。
void UpdateState () { … else { contactNormal =upAxis; } }
其次,在Jump中偏向跳躍方向時(shí)。
void Jump () { … jumpDirection = (jumpDirection +upAxis).normalized; … }
而且,我們還必須調(diào)整如何確定跳躍速度。這個(gè)想法是我們抵消重力。我們使用的是重力Y分量的-2倍,但這不再起作用。相反,我們必須使用重力矢量的大小,而不管其方向如何。這意味著我們也必須刪除減號(hào)。
float jumpSpeed = Mathf.Sqrt(2f* Physics.gravity.magnitude*jumpHeight);
最后,在SnapToGround中探查地面時(shí),我們必須用負(fù)的上軸代替Vector3.down。
bool SnapToGround () { … if (!Physics.Raycast( body.position,-upAxis, out RaycastHit hit, probeDistance, probeMask )) { return false; } … }
點(diǎn)積
當(dāng)我們需要點(diǎn)積時(shí),我們也不能再直接使用法向向量的Y分量。我們必須使用上軸和法線向量作為參數(shù)來調(diào)用Vector3.Dot。首先在SnapToGround中,檢查我們是否發(fā)現(xiàn)地面。
float upDot = Vector3.Dot(upAxis, hit.normal); if (upDot< GetMinDot(hit.collider.gameObject.layer)) { return false; }
然后在CheckSteepContacts中看看我們是否陷入了縫隙中。
bool CheckSteepContacts () { if (steepContactCount > 1) { steepNormal.Normalize(); float upDot = Vector3.Dot(upAxis, steepNormal); if (upDot>= minGroundDotProduct) { … } } return false; }
并在EvaluateCollision中檢查我們有什么樣的連接方式。
void EvaluateCollision (Collision collision) { float minDot = GetMinDot(collision.gameObject.layer); for (int i = 0; i < collision.contactCount; i++) { Vector3 normal = collision.GetContact(i).normal; float upDot = Vector3.Dot(upAxis, normal); if (upDot>= minDot) { groundContactCount += 1; contactNormal += normal; } else if (upDot> -0.01f) { steepContactCount += 1; steepNormal += normal; } } }
現(xiàn)在,無論朝哪個(gè)方向,我們的球體都可以移動(dòng)。在播放模式下也可以更改重力方向,它將立即適應(yīng)新情況。
相對(duì)控制
但是,盡管將重力倒置完全沒有問題,但任何其他方向都會(huì)使球體的控制更加困難。例如,當(dāng)重力與X軸對(duì)齊時(shí),我們只能控制沿Z軸的移動(dòng)。沿Y軸的運(yùn)動(dòng)是我們無法控制的,只有重力和碰撞會(huì)影響它。由于我們?nèi)匀辉谑澜缈臻gXZ平面中定義控件,因此消除了輸入的X軸。我們必須在重力對(duì)齊的平面中定義所需的速度。
重力可以改變,我們也必須為右軸和前軸添加字段讓它們變?yōu)橄鄬?duì)。
Vector3 upAxis, rightAxis, forwardAxis;
我們需要項(xiàng)目方向在平面上做這項(xiàng)工作,所以讓我們把ProjectOnContactPlane換成一個(gè)更一般的方法ProjectDirectionOnPlane,適用于任意正常和正?;€執(zhí)行。
//Vector3 ProjectOnContactPlane (Vector3 vector) { // return vector - contactNormal * Vector3.Dot(vector, contactNormal); //}
Vector3 ProjectDirectionOnPlane (Vector3 direction, Vector3 normal) { return (direction - normal * Vector3.Dot(direction, normal)).normalized; }
用這種新方法在AdjustVelocity中確定X和Z控制軸,給它提供軸和法線變量。
void AdjustVelocity () { Vector3 xAxis =ProjectDirectionOnPlane(rightAxis, contactNormal); Vector3 zAxis =ProjectDirectionOnPlane(forwardAxis, contactNormal);
… }
重力相對(duì)軸在Update中派生。如果一個(gè)玩家輸入空間存在,我們?cè)谥亓ζ矫嫔显O(shè)置它的右軸和前軸以找到重力對(duì)齊的X和Z軸。否則我們賦值為世界坐標(biāo)軸?,F(xiàn)在所需的速度是相對(duì)于定義這些軸,所以不需要將輸入向量轉(zhuǎn)換為一個(gè)不同的空間。
void Update () { … if (playerInputSpace) { rightAxis = ProjectDirectionOnPlane(playerInputSpace.right, upAxis); forwardAxis = ProjectDirectionOnPlane(playerInputSpace.forward, upAxis); } else { rightAxis = ProjectDirectionOnPlane(Vector3.right, upAxis); forwardAxis = ProjectDirectionOnPlane(Vector3.forward, upAxis); } desiredVelocity = new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed; //} desiredJump |= Input.GetButtonDown("Jump"); }
這仍然不能解決控制軸與重力對(duì)齊時(shí)被消除的問題,但是當(dāng)使用軌道攝像機(jī)時(shí),我們可以對(duì)其進(jìn)行定向,以便重新獲得完全控制權(quán)。
對(duì)準(zhǔn)軌道攝像機(jī)
軌道攝像頭仍然很笨拙,因?yàn)樗冀K將世界Y軸用作其向上方向。因此,當(dāng)向上或向下看時(shí),我們?nèi)匀豢梢韵刂戚S。理想情況下,軌道攝像機(jī)將自身與重力對(duì)準(zhǔn),這既直觀又確保相對(duì)運(yùn)動(dòng)始終如預(yù)期那樣起作用。
我們使用軌道角度來控制相機(jī)的軌道并對(duì)其進(jìn)行約束,以使其不會(huì)太高或太低。無論采用哪種方式,我們都希望保留此功能。這可以通過應(yīng)用第二次旋轉(zhuǎn)來完成,該旋轉(zhuǎn)使軌道旋轉(zhuǎn)與重力對(duì)齊。為此給 OrbitCamera?添加一個(gè)Quaternion gravityAlignment字段,并使用身份輪換進(jìn)行初始化。
Quaternion gravityAlignment = Quaternion.identity;
LateUpdate調(diào)整開始時(shí),它與當(dāng)前的向上方向保持同步。為了使軌道在需要調(diào)整時(shí)不會(huì)發(fā)生不規(guī)則的變化,我們必須使用從當(dāng)前路線到新路線的最小旋轉(zhuǎn)。可以通過Quaternion.FromRotation找到最小旋轉(zhuǎn),這會(huì)產(chǎn)生從一個(gè)方向到另一個(gè)方向的旋轉(zhuǎn)。我們的原因是從最后對(duì)齊的向上方向到當(dāng)前的向上方向。然后,將其與當(dāng)前對(duì)齊方式相乘,最后得到新的對(duì)齊方式。
void LateUpdate () { gravityAlignment = Quaternion.FromToRotation( gravityAlignment * Vector3.up, -Physics.gravity.normalized ) * gravityAlignment; … }
軌道旋轉(zhuǎn)邏輯必須保持不知道重力對(duì)準(zhǔn)。為此,請(qǐng)?zhí)砑右粋€(gè)字段以單獨(dú)跟蹤軌道旋轉(zhuǎn)。該四元數(shù)包含軌道角度旋轉(zhuǎn),應(yīng)在Awake中初始化,并將其設(shè)置為與初始攝像機(jī)旋轉(zhuǎn)相同的值。我們可以為此使用鏈接分配。
Quaternion orbitRotation; … void Awake () { … transform.localRotation =orbitRotation =Quaternion.Euler(orbitAngles); }
僅在手動(dòng)或自動(dòng)旋轉(zhuǎn)時(shí)才需要在LateUpdate中更改。外觀旋轉(zhuǎn)然后變?yōu)橹亓β肪€乘以軌道旋轉(zhuǎn)。
void LateUpdate () { … //Quaternion lookRotation; if (ManualRotation() || AutomaticRotation()) { ConstrainAngles(); orbitRotation= Quaternion.Euler(orbitAngles); } //else { // lookRotation = transform.localRotation; //} Quaternion lookRotation = gravityAlignment * orbitRotation;
… }
這在手動(dòng)調(diào)整軌道時(shí)有效,但是AutomaticRotation失敗了,因?yàn)樗鼉H在重力指向下方時(shí)才有效。我們可以通過在確定正確的角度之前取消重力對(duì)齊來解決此問題。這是通過將反重力比對(duì)應(yīng)用于運(yùn)動(dòng)增量來完成的,我們可以通過該方法Quaternion.Inverse獲得。
Vector3 alignedDelta =Quaternion.Inverse(gravityAlignment) *(focusPoint - previousFocusPoint);Vector2 movement = new Vector2(alignedDelta.x,alignedDelta.z);
球形重力
我們支持任意重力,但仍然限于統(tǒng)一矢量Physics.gravity。如果我們想支撐球形重力并在行星上行走,那么我們必須提出一個(gè)定制的重力解決方案。
自定義重力
在本教程中,我們將使用非常簡(jiǎn)單的方法。給定在世界空間中的位置,并使用可返回重力矢量CustomGravity的公共方法GetGravity創(chuàng)建靜態(tài)類。最初,我們將返回未修改的內(nèi)容Physics.gravity。
using UnityEngine;
public static class CustomGravity {
public static Vector3 GetGravity (Vector3 position) { return Physics.gravity; }}
當(dāng)我們使用重力來確定球面和軌道攝像機(jī)的上軸時(shí),我們還要添加一個(gè)方便的GetUpAxis方法,再次使用位置參數(shù)。
public static Vector3 GetUpAxis (Vector3 position) { return -Physics.gravity.normalized; }
我們可以走得更遠(yuǎn),并包括一種可以一舉兩得的變型方法GetGravity。讓我們通過添加向上軸的輸出參數(shù)來實(shí)現(xiàn)。我們通過out在參數(shù)定義的前面編寫來標(biāo)記它。
public static Vector3 GetGravity (Vector3 position, out Vector3 upAxis) { upAxis = -Physics.gravity.normalized; return Physics.gravity; }
輸出參數(shù)如何工作?
它的工作方式類似于Physics.Raycast,它返回是否有人命中并將相關(guān)數(shù)據(jù)放入RaycastHit作為輸出參數(shù)提供的結(jié)構(gòu)中。
該out關(guān)鍵字告訴我們,方法負(fù)責(zé)正確設(shè)置參數(shù),取代先前的值。不為其分配值將產(chǎn)生編譯器錯(cuò)誤。
在這種情況下,其基本原理是GetGravity的主要目的是返回重力矢量,但您也可以通過輸出參數(shù)同時(shí)獲得關(guān)聯(lián)的上軸。
應(yīng)用自定義重力
從現(xiàn)在開始,我們可以依靠CustomGravity.GetUpAxis在OrbitCamera.LateUpdate執(zhí)行重力對(duì)準(zhǔn)。我們將基于當(dāng)前焦點(diǎn)進(jìn)行此操作。
gravityAlignment = Quaternion.FromToRotation( gravityAlignment * Vector3.up, CustomGravity.GetUpAxis(focusPoint) ) * gravityAlignment;
并且在MovingSphere.FixedUpdate中我們可以使用CustomGravity.GetGravity基于body的位置來獲取重力和上軸。我們必須自己施加引力,只需將其添加到最終速度作為加速度即可。另外,讓我們將重力向量傳遞給Jump。
void FixedUpdate () { //upAxis = -Physics.gravity.normalized; Vector3 gravity = CustomGravity.GetGravity(body.position, out upAxis); UpdateState(); AdjustVelocity();
if (desiredJump) { desiredJump = false; Jump(gravity); }
velocity += gravity * Time.deltaTime;
body.velocity = velocity; ClearState(); }
這樣,我們可以在需要時(shí)計(jì)算重力的大小,而不必再次為我們的位置確定重力。
void Jump (Vector3 gravity) { … float jumpSpeed = Mathf.Sqrt(2f *gravity.magnitude * jumpHeight); … }
而且由于我們使用的是自定義重力,因此必須確保標(biāo)準(zhǔn)重力不會(huì)應(yīng)用到球體上。我們可以通過將body的isGravity屬性設(shè)置為false?來強(qiáng)制執(zhí)行此操作Awake。
void Awake () {body = GetComponent<Rigidbody>();body.useGravity = false;OnValidate();}
走向原點(diǎn)
盡管我們已切換到自定義重力方法,但所有操作仍應(yīng)相同。更改Unity的引力矢量會(huì)像以前一樣影響所有事物。為了使重力變?yōu)榍蛐?,我們必須進(jìn)行一些更改。我們將使其保持簡(jiǎn)單,并使用世界原點(diǎn)作為重力源的中心。因此,上軸只是指向位置的方向。相應(yīng)地調(diào)整CustomGravity.GetUpAxis。
public static Vector3 GetUpAxis (Vector3 position) { returnposition.normalized; }
真實(shí)重力隨距離而變化。您越遠(yuǎn),受到的影響就越小。但是,我們將使用Unity重力矢量的已配置Y分量保持其強(qiáng)度不變。因此,我們可以按比例放大向上軸。
public static Vector3 GetGravity (Vector3 position) { returnposition.normalized * Physics.gravity.y; } public static Vector3 GetGravity (Vector3 position, out Vector3 upAxis) { upAxis =position.normalized; returnupAxis * Physics.gravity.y; }
這就是使簡(jiǎn)單的球形重力工作所需要的全部。
請(qǐng)注意,在小行星上行走和跳躍時(shí),有可能最終陷于圍繞它的軌道中。您正在跌倒,但是向前的動(dòng)量使您像衛(wèi)星一樣掉落在表面上,而不是朝表面傾斜。
可以通過增加重力或行星半徑,允許空氣加速或通過引入使您減速的阻力來緩解這種情況。
推開
我們不必局限于現(xiàn)實(shí)情況。通過使重力為正,我們最終將球體推離原點(diǎn),從而可以沿球體內(nèi)部移動(dòng)。但是,在這種情況下,我們必須翻轉(zhuǎn)上軸。
public static Vector3 GetGravity (Vector3 position, out Vector3 upAxis) { Vector3 up = position.normalized; upAxis =Physics.gravity.y < 0f ? up : -up; returnup* Physics.gravity.y; } public static Vector3 GetUpAxis (Vector3 position) { Vector3 up = position.normalized; returnPhysics.gravity.y < 0f ? up : -up; }
其他機(jī)構(gòu)
我們的球面和軌道攝像頭可以使用自定義重力,但是其他一切仍然依賴于默認(rèn)重力才能下降。為了使具有對(duì)象的任意對(duì)象Rigidbody落入原點(diǎn),我們還必須對(duì)它們應(yīng)用自定義重力。
專門的剛體組件
我們可以擴(kuò)展現(xiàn)有Rigidbody組件以添加自定義重力,但是這將使得難以隱藏已經(jīng)配置了Rigidbody的對(duì)象。因此,我們將創(chuàng)建一個(gè)新的CustomGravityRigidbody組件類型,它需要一個(gè)主體,并在其喚醒時(shí)檢索對(duì)它的引用。它還會(huì)禁用常規(guī)重力。
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]public class CustomGravityRigidbody : MonoBehaviour {
Rigidbody body;
void Awake () { body = GetComponent<Rigidbody>(); body.useGravity = false; }}
要使物體落入原點(diǎn),我們要做的就是在FixedUpdate其上調(diào)用AddForce,并根據(jù)其位置將其自定義重力傳遞給它。
void FixedUpdate () { body.AddForce(CustomGravity.GetGravity(body.position)); }
但是重力是一種加速度,因此添加ForceMode.Acceleration第二個(gè)參數(shù)。
body.AddForce( CustomGravity.GetGravity(body.position), ForceMode.Acceleration );
為什么飛行方塊會(huì)抖動(dòng)?
發(fā)生這種情況的原因與我們的球體抖動(dòng)一樣。當(dāng)相機(jī)也在移動(dòng)時(shí),對(duì)于快速移動(dòng)的物體尤其明顯。如果太明顯,則可以使多維數(shù)據(jù)集插值其位置。也可以添加邏輯以僅在需要時(shí)打開插值。
睡眠
每次固定更新時(shí)Rigidbody都要自己施加引力的缺點(diǎn)是不再沉睡。PhysX盡可能使body進(jìn)入睡眠狀態(tài),有效地使body處于停滯狀態(tài),從而減少了要做的工作量。因此,最好限制我們的自定義重力影響多少個(gè)body。
我們可以做的一件事是FixedUpdate,通過調(diào)用人體的IsSleeping方法來檢查人體在開始時(shí)是否處于睡眠狀態(tài)。如果是這樣,它就處于平衡狀態(tài),我們不應(yīng)該打擾它,所以請(qǐng)立即返回。
void FixedUpdate () { if (body.IsSleeping()) { return; }
body.AddForce( CustomGravity.GetGravity(body.position), ForceMode.Acceleration ); }
但是它永遠(yuǎn)不會(huì)入睡,因?yàn)槲覀儗?duì)其施加了加速。因此,我們必須首先停止這樣做。讓我們假設(shè),如果人體的速度很低,它就靜止了。我們將使用0.0001閾值作為其速度的平方大小。每秒0.01個(gè)單位。它比不施加重力要慢。
void FixedUpdate () { if (body.IsSleeping()) { return; }
if (body.velocity.sqrMagnitude < 0.0001f) { return; }
body.AddForce( CustomGravity.GetGravity(body.position), ForceMode.Acceleration ); }
那是行不通的,因?yàn)槭w開始靜止不動(dòng),也可能由于種種原因而仍然停留在空中而暫時(shí)懸停在適當(dāng)?shù)奈恢?。因此,讓我們添加一個(gè)浮動(dòng)延遲,在此期間我們假定主體處于浮動(dòng)狀態(tài),但可能仍會(huì)掉落。除非速度低于閾值,否則它將始終重置為零。在這種情況下,我們要等一秒鐘再停止施加重力。如果那還沒有足夠的時(shí)間讓body運(yùn)動(dòng),那它應(yīng)該休息了。
float floatDelay;
…
void FixedUpdate () { if (body.IsSleeping()) { floatDelay = 0f; return; }
if (body.velocity.sqrMagnitude < 0.0001f) { floatDelay += Time.deltaTime; if (floatDelay >= 1f) { return; } } else { floatDelay = 0f; } body.AddForce( CustomGravity.GetGravity(body.position), ForceMode.Acceleration ); }
請(qǐng)注意,我們不強(qiáng)迫body自己入睡。我們將其留給PhysX。這不是支持睡眠的唯一方法,但是對(duì)于大多數(shù)簡(jiǎn)單情況而言,這是簡(jiǎn)單而足夠的。
為什么body有時(shí)拒絕睡覺?
發(fā)生這種情況是因?yàn)镻hysX不斷做出微小的調(diào)整,要么變化非常緩慢,要么在兩種狀態(tài)之間振蕩。當(dāng)存在幾乎穩(wěn)定的碰撞狀態(tài)時(shí),可能會(huì)發(fā)生這種情況。
保持清醒
我們的方法相當(dāng)強(qiáng)大,但并不完美。我們做出的一個(gè)假設(shè)是,重力對(duì)于給定位置保持恒定。一旦我們停止施加重力,即使重力突然翻轉(zhuǎn),物體也會(huì)保持原樣。在其他情況下,我們的假設(shè)也可能失敗,例如,當(dāng)我們漂浮但尚未入睡時(shí),body可能會(huì)非常緩慢地移動(dòng),或者地板可能會(huì)消失。另外,如果body短暫存活,例如暫時(shí)的碎屑,我們也不必?fù)?dān)心睡覺。因此,讓我們可以配置是否允許body漂浮以使其進(jìn)入睡眠狀態(tài)。
[SerializeField] bool floatToSleep = false;
…
void FixedUpdate () { if (floatToSleep) { … } body.AddForce( CustomGravity.GetGravity(body.position), ForceMode.Acceleration ); }
下一個(gè)教程是“ 復(fù)雜重力”。
資源庫(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-05-custom-gravity/
往期精選
Unity3D游戲開發(fā)中100+效果的實(shí)現(xiàn)和源碼大全 - 收藏起來肯定用得著
Shader學(xué)習(xí)應(yīng)該如何切入?
聲明:發(fā)布此文是出于傳遞更多知識(shí)以供交流學(xué)習(xí)之目的。若有來源標(biāo)注錯(cuò)誤或侵犯了您的合法權(quán)益,請(qǐng)作者持權(quán)屬證明與我們聯(lián)系,我們將及時(shí)更正、刪除,謝謝。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/custom-gravity/
翻譯、編輯、整理:MarsZhou
More:【微信公眾號(hào)】?u3dnotes