????????很多童鞋沒有系統(tǒng)的Unity3D游戲開發(fā)基礎(chǔ)涩咖,也不知道從何開始學(xué)似忧。為此我們精選了一套國外優(yōu)秀的Unity3D游戲開發(fā)教程璃氢,翻譯整理后放送給大家,教您從零開始一步一步掌握Unity3D游戲開發(fā)蕊蝗。?本文不是廣告仅乓,不是推廣,是免費的純干貨匿又!本文全名:喵的Unity游戲開發(fā)之路 - 移動?-?自定義重力?- 在球體上行走
自定義重力
支持任意重力。
使用可變的上軸建蹄。
將所有內(nèi)容拉到一個點碌更。
將自定義重力應(yīng)用于任意物體。
這是有關(guān)控制角色移動的教程系列的第五部分洞慎。它涵蓋了使用自定義方法替換標準重力的方法痛单,通過該方法,我們支持在球體上行走劲腿。
本教程使用Unity 2019.2.21f1制作旭绒。它還使用ProBuilder軟件包。
效果之一
探索一個小小的星球焦人。
可變重力
到目前為止挥吵,我們一直使用固定的重力矢量:垂直往下9.81。這對于大多數(shù)游戲而言已足夠花椭,但并非全部忽匈。例如,目前無法在代表行星的球體表面上行走矿辽。因此丹允,我們將添加對自定義重力的支持,而不必統(tǒng)一袋倔。
在變得復(fù)雜之前雕蔽,讓我們開始簡單地翻轉(zhuǎn)重力,并通過項目設(shè)置使重力矢量的Y分量為正宾娜,看看會發(fā)生什么批狐。這有效地將其變成反重力,這應(yīng)該使我們的球體向上掉落前塔。
事實證明贾陷,我們的球體確實向上飛行,但最初緊貼地面嘱根。那是因為我們正在將其吸附到地面髓废,并且我們的代碼假定法向重力。我們必須對其進行更改该抒,以便它可以與任何重力矢量一起使用慌洪。
向上軸(Up Axis)
我們依靠向上軸始終等于Y軸顶燕。為了放開這個假設(shè),我們必須向MovingSphere添加一個上軸字段冈爹,并使用該字段涌攻。為了支持隨時變化的重力,我們必須在FixedUpdate的起點設(shè)置上軸频伤。它指向與重力相反的方向恳谎,因此它等于取反后的歸一化重力矢量。
Vector3 upAxis;
…
void FixedUpdate () {
upAxis = -Physics.gravity.normalized;
…
}
現(xiàn)在我們必須用新的上軸替換所有的Vector3.up用法憋肖。首先因痛,在UpdateState中,
當(dāng)球體處于空中時岸更,我們將其用作接觸法線鸵膏。
void UpdateState () {
…
else {
contactNormal =upAxis;
}
}
其次,在Jump中
偏向跳躍方向時怎炊。
void Jump () {
…
jumpDirection = (jumpDirection +upAxis).normalized;
…
}
而且谭企,我們還必須調(diào)整如何確定跳躍速度。這個想法是我們抵消重力评肆。我們使用的是重力Y分量的-2倍债查,但這不再起作用。相反瓜挽,我們必須使用重力矢量的大小攀操,而不管其方向如何。這意味著我們也必須刪除減號秸抚。
float jumpSpeed = Mathf.Sqrt(2f* Physics.gravity.magnitude*jumpHeight);
最后速和,在SnapToGround中探查地面時,我們必須用負的上軸代替Vector3.down剥汤。
bool SnapToGround () {
…
if (!Physics.Raycast(
body.position,-upAxis, out RaycastHit hit,
probeDistance, probeMask
)) {
return false;
}
…
}
點積
當(dāng)我們需要點積時颠放,我們也不能再直接使用法向向量的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)在,無論朝哪個方向畜晰,我們的球體都可以移動砾莱。在播放模式下也可以更改重力方向,它將立即適應(yīng)新情況凄鼻。
相對控制
但是腊瑟,盡管將重力倒置完全沒有問題聚假,但任何其他方向都會使球體的控制更加困難。例如闰非,當(dāng)重力與X軸對齊時膘格,我們只能控制沿Z軸的移動。沿Y軸的運動是我們無法控制的财松,只有重力和碰撞會影響它瘪贱。由于我們?nèi)匀辉谑澜缈臻gXZ平面中定義控件,因此消除了輸入的X軸辆毡。我們必須在重力對齊的平面中定義所需的速度菜秦。
重力可以改變,我們也必須為右軸和前軸添加字段讓它們變?yōu)橄鄬Α?/p>
Vector3 upAxis, rightAxis, forwardAxis;
我們需要項目方向在平面上做這項工作,所以讓我們把ProjectOnContactPlane換成一個更一般的方法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);
…
}
重力相對軸在Update中派生访锻。如果一個玩家輸入空間存在,我們在重力平面上設(shè)置它的右軸和前軸以找到重力對齊的X和Z軸闹获。否則我們賦值為世界坐標軸∑谌現(xiàn)在所需的速度是相對于定義這些軸,所以不需要將輸入向量轉(zhuǎn)換為一個不同的空間。
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");
}
這仍然不能解決控制軸與重力對齊時被消除的問題避诽,但是當(dāng)使用軌道攝像機時龟虎,我們可以對其進行定向,以便重新獲得完全控制權(quán)沙庐。
對準軌道攝像機
軌道攝像頭仍然很笨拙鲤妥,因為它始終將世界Y軸用作其向上方向。因此拱雏,當(dāng)向上或向下看時棉安,我們?nèi)匀豢梢韵刂戚S。理想情況下铸抑,軌道攝像機將自身與重力對準贡耽,這既直觀又確保相對運動始終如預(yù)期那樣起作用。
我們使用軌道角度來控制相機的軌道并對其進行約束鹊汛,以使其不會太高或太低蒲赂。無論采用哪種方式,我們都希望保留此功能刁憋。這可以通過應(yīng)用第二次旋轉(zhuǎn)來完成滥嘴,該旋轉(zhuǎn)使軌道旋轉(zhuǎn)與重力對齊。為此給 OrbitCamera?添加一個Quaternion gravityAlignment
字段至耻,并使用身份輪換進行初始化氏涩。
Quaternion gravityAlignment = Quaternion.identity;
LateUpdate
調(diào)整開始時届囚,它與當(dāng)前的向上方向保持同步。為了使軌道在需要調(diào)整時不會發(fā)生不規(guī)則的變化是尖,我們必須使用從當(dāng)前路線到新路線的最小旋轉(zhuǎn)意系。可以通過Quaternion.FromRotation找到最小旋轉(zhuǎn)饺汹,這會產(chǎn)生從一個方向到另一個方向的旋轉(zhuǎn)蛔添。我們的原因是從最后對齊的向上方向到當(dāng)前的向上方向。然后兜辞,將其與當(dāng)前對齊方式相乘迎瞧,最后得到新的對齊方式。
void LateUpdate () {
gravityAlignment =
Quaternion.FromToRotation(
gravityAlignment * Vector3.up, -Physics.gravity.normalized
) * gravityAlignment;
…
}
軌道旋轉(zhuǎn)邏輯必須保持不知道重力對準逸吵。為此凶硅,請?zhí)砑右粋€字段以單獨跟蹤軌道旋轉(zhuǎn)。該四元數(shù)包含軌道角度旋轉(zhuǎn)扫皱,應(yīng)在Awake中初始化足绅,并將其設(shè)置為與初始攝像機旋轉(zhuǎn)相同的值。我們可以為此使用鏈接分配韩脑。
Quaternion orbitRotation;
…
void Awake () {
…
transform.localRotation =orbitRotation =Quaternion.Euler(orbitAngles);
}
僅在手動或自動旋轉(zhuǎn)時才需要在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;
…
}
這在手動調(diào)整軌道時有效段多,但是AutomaticRotation
失敗了首量,因為它僅在重力指向下方時才有效。我們可以通過在確定正確的角度之前取消重力對齊來解決此問題进苍。這是通過將反重力比對應(yīng)用于運動增量來完成的加缘,我們可以通過該方法Quaternion.Inverse獲得。
Vector3 alignedDelta =
Quaternion.Inverse(gravityAlignment) *
(focusPoint - previousFocusPoint);
Vector2 movement = new Vector2(alignedDelta.x,alignedDelta.z);
球形重力
我們支持任意重力觉啊,但仍然限于統(tǒng)一矢量Physics.gravity拣宏。如果我們想支撐球形重力并在行星上行走,那么我們必須提出一個定制的重力解決方案柄延。
自定義重力
在本教程中蚀浆,我們將使用非常簡單的方法。給定在世界空間中的位置搜吧,并使用可返回重力矢量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)我們使用重力來確定球面和軌道攝像機的上軸時摆昧,我們還要添加一個方便的GetUpAxis
方法,再次使用位置參數(shù)蜒程。
public static Vector3 GetUpAxis (Vector3 position) {
return -Physics.gravity.normalized;
}
我們可以走得更遠绅你,并包括一種可以一舉兩得的變型方法GetGravity伺帘。讓我們通過添加向上軸的輸出參數(shù)來實現(xiàn)。我們通過out
在參數(shù)定義的前面編寫來標記它忌锯。
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)鍵字告訴我們张咳,方法負責(zé)正確設(shè)置參數(shù),取代先前的值似舵。不為其分配值將產(chǎn)生編譯器錯誤脚猾。
在這種情況下,其基本原理是GetGravity的主要目的是返回重力矢量砚哗,但您也可以通過輸出參數(shù)同時獲得關(guān)聯(lián)的上軸龙助。
應(yīng)用自定義重力
從現(xiàn)在開始,我們可以依靠CustomGravity.GetUpAxis
在OrbitCamera.LateUpdate
執(zhí)行重力對準蛛芥。我們將基于當(dāng)前焦點進行此操作提鸟。
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();
}
這樣,我們可以在需要時計算重力的大小烘嘱,而不必再次為我們的位置確定重力昆禽。
void Jump (Vector3 gravity) {
…
float jumpSpeed = Mathf.Sqrt(2f *gravity.magnitude * jumpHeight);
…
}
而且由于我們使用的是自定義重力,因此必須確保標準重力不會應(yīng)用到球體上蝇庭。我們可以通過將body的isGravity
屬性設(shè)置為false
?來強制執(zhí)行此操作Awake
醉鳖。
void Awake () {
body = GetComponent<Rigidbody>();
body.useGravity = false;
OnValidate();
}
走向原點
盡管我們已切換到自定義重力方法,但所有操作仍應(yīng)相同哮内。更改Unity的引力矢量會像以前一樣影響所有事物盗棵。為了使重力變?yōu)榍蛐危覀儽仨氝M行一些更改北发。我們將使其保持簡單纹因,并使用世界原點作為重力源的中心。因此琳拨,上軸只是指向位置的方向瞭恰。相應(yīng)地調(diào)整CustomGravity.GetUpAxis
。
public static Vector3 GetUpAxis (Vector3 position) {
returnposition.normalized;
}
真實重力隨距離而變化狱庇。您越遠惊畏,受到的影響就越小恶耽。但是,我們將使用Unity重力矢量的已配置Y分量保持其強度不變颜启。因此偷俭,我們可以按比例放大向上軸。
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;
}
這就是使簡單的球形重力工作所需要的全部缰盏。
請注意社搅,在小行星上行走和跳躍時,有可能最終陷于圍繞它的軌道中乳规。您正在跌倒形葬,但是向前的動量使您像衛(wèi)星一樣掉落在表面上,而不是朝表面傾斜暮的。
可以通過增加重力或行星半徑笙以,允許空氣加速或通過引入使您減速的阻力來緩解這種情況。
推開
我們不必局限于現(xiàn)實情況冻辩。通過使重力為正猖腕,我們最終將球體推離原點,從而可以沿球體內(nèi)部移動恨闪。但是倘感,在這種情況下,我們必須翻轉(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;
}
其他機構(gòu)
我們的球面和軌道攝像頭可以使用自定義重力老玛,但是其他一切仍然依賴于默認重力才能下降。為了使具有對象的任意對象Rigidbody
落入原點钧敞,我們還必須對它們應(yīng)用自定義重力蜡豹。
專門的剛體組件
我們可以擴展現(xiàn)有Rigidbody
組件以添加自定義重力,但是這將使得難以隱藏已經(jīng)配置了Rigidbody的對象溉苛。因此镜廉,我們將創(chuàng)建一個新的CustomGravityRigidbody
組件類型,它需要一個主體愚战,并在其喚醒時檢索對它的引用娇唯。它還會禁用常規(guī)重力。
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class CustomGravityRigidbody : MonoBehaviour {
Rigidbody body;
void Awake () {
body = GetComponent<Rigidbody>();
body.useGravity = false;
}
}
要使物體落入原點寂玲,我們要做的就是在FixedUpdate其上調(diào)用AddForce塔插,并根據(jù)其位置將其自定義重力傳遞給它。
void FixedUpdate () {
body.AddForce(CustomGravity.GetGravity(body.position));
}
但是重力是一種加速度,因此添加ForceMode.Acceleration
第二個參數(shù)。
body.AddForce(
CustomGravity.GetGravity(body.position), ForceMode.Acceleration
);
為什么飛行方塊會抖動次询?
發(fā)生這種情況的原因與我們的球體抖動一樣奏司。當(dāng)相機也在移動時伸刃,對于快速移動的物體尤其明顯谎砾。如果太明顯,則可以使多維數(shù)據(jù)集插值其位置捧颅。也可以添加邏輯以僅在需要時打開插值景图。
睡眠
每次固定更新時Rigidbody
都要自己施加引力的缺點是不再沉睡。PhysX盡可能使body進入睡眠狀態(tài)碉哑,有效地使body處于停滯狀態(tài)挚币,從而減少了要做的工作量。因此扣典,最好限制我們的自定義重力影響多少個body妆毕。
我們可以做的一件事是FixedUpdate
,通過調(diào)用人體的IsSleeping
方法來檢查人體在開始時是否處于睡眠狀態(tài)贮尖。如果是這樣笛粘,它就處于平衡狀態(tài),我們不應(yīng)該打擾它湿硝,所以請立即返回薪前。
void FixedUpdate () {
if (body.IsSleeping()) {
return;
}
body.AddForce(
CustomGravity.GetGravity(body.position), ForceMode.Acceleration
);
}
但是它永遠不會入睡,因為我們對其施加了加速关斜。因此示括,我們必須首先停止這樣做。讓我們假設(shè)痢畜,如果人體的速度很低垛膝,它就靜止了。我們將使用0.0001閾值作為其速度的平方大小裁着。每秒0.01個單位繁涂。它比不施加重力要慢拱她。
void FixedUpdate () {
if (body.IsSleeping()) {
return;
}
if (body.velocity.sqrMagnitude < 0.0001f) {
return;
}
body.AddForce(
CustomGravity.GetGravity(body.position), ForceMode.Acceleration
);
}
那是行不通的二驰,因為尸體開始靜止不動,也可能由于種種原因而仍然停留在空中而暫時懸停在適當(dāng)?shù)奈恢帽印R虼送叭福屛覀兲砑右粋€浮動延遲,在此期間我們假定主體處于浮動狀態(tài)唬复,但可能仍會掉落矗积。除非速度低于閾值,否則它將始終重置為零敞咧。在這種情況下棘捣,我們要等一秒鐘再停止施加重力。如果那還沒有足夠的時間讓body運動休建,那它應(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
);
}
請注意评疗,我們不強迫body自己入睡。我們將其留給PhysX茵烈。這不是支持睡眠的唯一方法百匆,但是對于大多數(shù)簡單情況而言,這是簡單而足夠的呜投。
為什么body有時拒絕睡覺加匈?
發(fā)生這種情況是因為PhysX不斷做出微小的調(diào)整,要么變化非常緩慢仑荐,要么在兩種狀態(tài)之間振蕩雕拼。當(dāng)存在幾乎穩(wěn)定的碰撞狀態(tài)時,可能會發(fā)生這種情況粘招。
保持清醒
我們的方法相當(dāng)強大悲没,但并不完美。我們做出的一個假設(shè)是男图,重力對于給定位置保持恒定示姿。一旦我們停止施加重力,即使重力突然翻轉(zhuǎn)逊笆,物體也會保持原樣栈戳。在其他情況下,我們的假設(shè)也可能失敗难裆,例如子檀,當(dāng)我們漂浮但尚未入睡時,body可能會非常緩慢地移動乃戈,或者地板可能會消失褂痰。另外,如果body短暫存活症虑,例如暫時的碎屑缩歪,我們也不必擔(dān)心睡覺。因此谍憔,讓我們可以配置是否允許body漂浮以使其進入睡眠狀態(tài)匪蝙。
[SerializeField]
bool floatToSleep = false;
…
void FixedUpdate () {
if (floatToSleep) {
…
}
body.AddForce(
CustomGravity.GetGravity(body.position), ForceMode.Acceleration
);
}
下一個教程是“ 復(fù)雜重力”。
資源庫(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-05-custom-gravity/
往期精選
Unity3D游戲開發(fā)中100+效果的實現(xiàn)和源碼大全 - 收藏起來肯定用得著
Shader學(xué)習(xí)應(yīng)該如何切入习贫?
聲明:發(fā)布此文是出于傳遞更多知識以供交流學(xué)習(xí)之目的逛球。若有來源標注錯誤或侵犯了您的合法權(quán)益,請作者持權(quán)屬證明與我們聯(lián)系苫昌,我們將及時更正颤绕、刪除,謝謝。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/custom-gravity/
翻譯奥务、編輯涕烧、整理:MarsZhou
More:【微信公眾號】?u3dnotes