喵的Unity游戲開發(fā)之路 - 在球體上行走

????????很多童鞋沒有系統(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.GetUpAxisOrbitCamera.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)該如何切入?

    UE4 開發(fā)從入門到入土

    聲明:發(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

  • 最后編輯于
    ?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
    【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
    平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

    友情鏈接更多精彩內(nèi)容