喵的Unity游戲開發之路 - 攀爬


原文:

https://mp.weixin.qq.com/s/-KnmYyaeaXZNkGtA09b-Ww

 

很多童鞋沒有系統的Unity3D游戲開發基礎,也不知道從何開始學。為此我們精選了一套國外優秀的Unity3D游戲開發教程,翻譯整理后放送給大家,教您從零開始一步一步掌握Unity3D游戲開發。 本文不是廣告,不是推廣,是免費的純干貨!本文全名:喵的Unity游戲開發之路 - 移動 - 攀爬 - 貼牆

 

 

 

 

 

 

  • 使表面可攀爬並進行檢測。

  • 即使牆壁在移動,也要貼在牆上。

  • 使用相對於牆壁的控件進行攀爬。

  • 爬上拐角處和懸垂處。

  • 站在斜坡上時要防止滑動。

 

 

 

這是有關控制角色移動的教程系列的第八部分。它增加了對攀爬垂直表面的支持。

 

本教程使用Unity 2019.2.21f1制作。它還使用ProBuilder軟件包。

效果之一

 

 

 

有時您不想觸地。

 

 

 

 

攀爬表面

 

 

除了步行和跑步外,攀爬通常是一種選擇,盡管自由度從僅在梯子上到您想要的任何地方都不同。由於我們的運動基於物理學,因此我們將支持在我們認為可攀爬的所有表面上攀爬。因此,第一步是檢測我們何時與此類表面接觸。

 

 

 

最大爬升角度

 

 

在攀爬過程中,表面的最重要屬性是其方向。如果一個表面算作地面,那么我們就可以在其上行走,因此它不應算作可攀爬的。陡峭的表面可以攀爬,但這只能使我們爬到完全垂直的牆壁上。超出這一點,我們就可以進行懸垂,雖然困難,但仍然可以攀升到一定程度。在最極端的情況下,我們最終懸掛在天花板上。讓我們通過可配置的最大爬升角度(從90°到170°,默認值為140°)(僅超出45°懸垂范圍)來限制MovingSphere的爬升能力。我們不允許攀爬天花板,因為那比攀爬更多。

 

 

 

	[SerializeField, Range(90, 180)] float maxClimbAngle = 140f;

 

 

像其他最小點積一樣,預先計算最小爬升點積。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

 

  float minGroundDotProduct, minStairsDotProduct, minClimbDotProduct;  …  void OnValidate () {    minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad); minStairsDotProduct = Mathf.Cos(maxStairsAngle * Mathf.Deg2Rad); minClimbDotProduct = Mathf.Cos(maxClimbAngle * Mathf.Deg2Rad); }

 

 

 

 

如果我們確實想像蜘蛛一樣爬上天花板怎么辦?

像蜘蛛一樣爬,就像無處不在,無所不在。最好通過使用局部重力進行移動,然后將其拉到接觸面來進行建模。本教程介紹與正常運動明顯不同的攀岩牆。

 

 

 

 

 

 

檢測攀爬表面

 

 

我們將檢測可爬坡的表面,類似於我們識別陡峭表面的方式,但是我們將跟蹤單獨的爬坡接觸計數和法線,必須像其他方法一樣將其在ClearState重置。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    Vector3 contactNormal, steepNormal, climbNormal;
  2.  
    int groundContactCount, steepContactCount, climbContactCount; void ClearState () { groundContactCount = steepContactCount =climbContactCount =0; contactNormal = steepNormal =climbNormal = Vector3.zero; connectionVelocity = Vector3.zero; previousConnectedBody = connectedBody; connectedBody = null; }

 

 

 

然后在EvaluateCollision中,如果一個接觸點不算作地面,則分別檢查陡峭接觸和攀爬接觸。始終使用攀爬觸點連接的物體,這樣我們的球體就有可能攀爬運動中的表面。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
 if (upDot >= minDot) { groundContactCount += 1; contactNormal += normal; connectedBody = collision.rigidbody; } //else if (upDot > -0.01f) { else { if (upDot > -0.01f) { steepContactCount += 1; steepNormal += normal; if (groundContactCount == 0) { connectedBody = collision.rigidbody; } } if (upDot >= minClimbDotProduct) { climbContactCount += 1; climbNormal += normal; connectedBody = collision.rigidbody; } }

 

 

 

現在,我們假設我們能夠自動爬升。要檢查這一點,請添加一個Climbing屬性,該屬性將返回true是否有任何攀爬接觸。

 

 

 

  •  
bool Climbing => climbContactCount > 0;

 

 

 

 

 

 

不可攀爬的表面

 

 

能夠攀爬一切並非總是可取的。我們可以通過使用圖層蒙版來限制可攀爬對象。我們可以為可攀爬的事物添加一個專用層,或者為不可攀爬的事物添加一個專用層。由於我希望默認情況下所有內容都是可爬的,因此我選擇了后一種方法並添加了不可爬的

 

 

添加爬升Mask配置選項。配置它等於probeMask ,然后添加Unclimbable探測體掩膜的各個領域,通過編輯預制。請注意,您還必須將新圖層添加到軌道攝像機的“ 障礙物蒙版”中,否則它將忽略它。

 

 

 

  •  
  •  
 [SerializeField] LayerMask probeMask = -1, stairsMask = -1, climbMask = -1;

 

 

 

 

現在,我們需要在EvaluateCollision中檢查碰撞的圖層兩次,因此將其存儲在變量中。

 

 

 

  •  
  •  
int layer = collision.gameObject.layer; float minDot = GetMinDot(layer);

 

 

 

然后,僅在未屏蔽的情況下包括攀爬接觸。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
 if ( upDot >= minClimbDotProduct&& (climbMask & (1 << layer)) != 0 ) { climbContactCount += 1; climbNormal += normal; connectedBody = collision.rigidbody; } 

 

 

 

 

 

 

攀岩材料

 

 

步行和爬山是一種非常不同的體育活動。例如,如果我們的化身具有人的形狀,則每個運動模式將具有不同的動畫,從而清楚說明了正在使用哪種模式。為了使模式對於我們的簡單球體在視覺上截然不同,我們將改用其他材料。添加普通材料和攀岩材料的配置字段。我將當前的黑色材料用作普通材料,而將紅色材料用作攀岩材料。

 

 

 

  •  
  •  
[SerializeField] Material normalMaterial = default, climbingMaterial = default;

 

 

 

 

獲取對球體MeshRenderer組件的引用,並將其存儲在Awake的字段中。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
MeshRenderer meshRenderer;  void Awake () { body = GetComponent<Rigidbody>(); body.useGravity = false; meshRenderer = GetComponent<MeshRenderer>(); OnValidate(); } 

 

 

 

然后在Update末尾為其分配適當的材料。

 

 

 

  •  
  •  
  •  
  •  
  •  
  1.  
    void Update () {
  2.  
    meshRenderer.material = Climbing ? climbingMaterial : normalMaterial; }

 

 

 

從現在開始,只要它碰到可攀爬的表面,球體就會變成紅色。

 

 

 

 

 

 

沿着牆壁移動

 

 

現在,我們知道當我們與可攀爬的物體接觸時,下一步就是切換到攀爬模式,這需要粘附在牆壁或其他類型的表面上,並相對於牆壁而不是地面移動。

 

 

 

牆貼

 

 

我們首先添加一個CheckClimbing方法,該方法返回是否在攀爬,如果返回,則使地面接觸計數和法線等於其攀爬等效值。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
bool CheckClimbing () { if (Climbing) { groundContactCount = climbContactCount; contactNormal = climbNormal; return true; } return false; }

 

 

 

UpdateState檢查我們是否有地面接觸時,首先調用此方法,因此攀爬否決其他所有規則。

 

 

 

  •  
  •  
  •  
  •  
  •  
 if ( CheckClimbing() ||OnGround || SnapToGround() || CheckSteepContacts() ) { }

 

 

 

為了防止跌倒,如果我們不攀爬,請在FixedUpdate施加重力。

 

 

 

  •  
  •  
  •  
if (!Climbing) { velocity += gravity * Time.deltaTime; }

 

 

 

 

 

 

 

牆相對運動

 

 

只要我們碰到牆,重力就會被忽略,只要我們保持在平坦區域,我們就會堅持下去。但是由於與我們在不重新調整相機方向而改變重力的情況下所做的相同原因,我們也基本上失去了對球體的控制。在這種情況下,我們不希望更改相機的向上矢量,因為它應該始終與重力匹配,否則它會變得非常混亂。因此,我們要做的是相對於牆壁和重力進行移動,而忽略攝像機的方向。

 

在AdjustVelocity中,首先檢查我們是否正在爬山。如果是這樣,在投影到接觸平面上之前,請勿對X和Z使用默認的左右輸入軸。取而代之的是,使用Z的上軸和X的接觸法線與X的上乘積。因此,控件在觸摸牆時會切換方向。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
 void AdjustVelocity () { //Vector3 xAxis = ProjectDirectionOnPlane(rightAxis, contactNormal); //Vector3 zAxis = ProjectDirectionOnPlane(forwardAxis, contactNormal); Vector3 xAxis, zAxis; if (Climbing) { xAxis = Vector3.Cross(contactNormal, upAxis); zAxis = upAxis; } else { xAxis = rightAxis; zAxis = forwardAxis; } xAxis = ProjectDirectionOnPlane(xAxis, contactNormal); zAxis = ProjectDirectionOnPlane(zAxis, contactNormal);  } 

 

 

 

 

直視牆壁時,此方法效果很好,但以其他角度查看牆壁時,其直觀性會降低,因為控制方向無法完美對齊。例如,當按向右以筆直地走到牆壁上時,當觸摸牆壁時,右將在視覺上變為向后,向前則向上。

 

 

最極端的情況是將視線從牆壁上移開,在這種情況下,左右控件會翻轉。但這首先是一個尷尬的視角。這個想法是,當玩家准備好攀爬時,他會改變其視角。或者,可以將攝像機編程為自動執行此操作,但是在任意情況下都很難做到這一點,並且常常導致玩家感到沮喪。高級相機自動化不是本教程的一部分。

 

 

 

當我們移至不可攀爬的地面時,為什么會立即跌倒?

因為我們使用物理學來運動,所以球體只在您指向球體的位置。如果這樣做會導致爬升失敗,它可能不會決定不繼續前進。因此,一旦您從常規表面爬到不可攀爬的表面,球體就會掉落。玩家必須停留在可攀爬的表面上,因此重要的是可攀爬和不可攀爬的區域在視覺上有所不同。

 

 

 

 

 

 

爬升速度和加速度

 

攀爬通常比跑步慢得多,並且還需要更精確的控制,因為輕微的失步會導致跌倒,無論是在現實生活中還是對於我們的地球而言。同樣,放慢速度會使突然的控制方向切換更易於管理。因此,添加最大爬升速度和最大爬升加速度配置選項。我們希望低速和高加速度來實現最大控制,所以讓我們使用2和20作為默認值。通常,您希望將速度保持在較低水平,但我將使用默認值的兩倍進行快速測試。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    [SerializeField, Range(0f, 100f)] float maxSpeed = 10f, maxClimbSpeed = 2f;
  2.  
    [SerializeField, Range(0f, 100f)] float maxAcceleration = 10f, maxAirAcceleration = 1f, maxClimbAcceleration = 20f;

 

 

 

 

哪個最大速度合適,可以隨每個物理步驟而變化,這與更新循環不同步,因此我們再也無法確定Update中的所需速度。因此,注釋該desiredVelocity字段,將playerInput變量提升為一個字段。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    Vector2 playerInput;
  2.  
    //Vector3 velocity, desiredVelocity, connectionVelocity; Vector3 velocity, connectionVelocity; void Update () { //Vector2 playerInput; playerInput.x = Input.GetAxis("Horizontal"); playerInput.y = Input.GetAxis("Vertical"); playerInput = Vector2.ClampMagnitude(playerInput, 1f);
  3.  
    //desiredVelocity = // new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed; desiredJump |= Input.GetButtonDown("Jump");
  4.  
    meshRenderer.material = Climbing ? climbingMaterial : normalMaterial; }

 

 

 

然后選擇適當的加速度和速度,並在AdjustVelocity需要時計算所需的速度分量。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    void AdjustVelocity () { float acceleration, speed; Vector3 xAxis, zAxis; if (Climbing) { acceleration = maxClimbAcceleration; speed = maxClimbSpeed; xAxis = Vector3.Cross(contactNormal, upAxis); zAxis = upAxis; } else { acceleration = OnGround ? maxAcceleration : maxAirAcceleration; speed = maxSpeed; xAxis = rightAxis; zAxis = forwardAxis; }
  2.  
    //float acceleration = OnGround ? maxAcceleration : maxAirAcceleration; float maxSpeedChange = acceleration * Time.deltaTime;
  3.  
    float newX = Mathf.MoveTowards(currentX,playerInput.x * speed, maxSpeedChange); float newZ = Mathf.MoveTowards(currentZ,playerInput.y * speed, maxSpeedChange);
  4.  
    velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ); }

 

 

 

 

 

 

 

在拐角處爬

 

 

在這一點上,已經可以圍繞內壁拐角爬升,其中可爬升的表面朝向球體彎曲。但是任何角度的外角都無法攀爬,因為經過它們會導致球體與牆失去接觸並掉落。我們可以通過始終使球體向其爬升的表面加速來解決此問題。這代表了攀岩者的抓地力,為此,我們將簡單地使用最大攀岩加速度。在FixedUpdate攀爬時進行此操作,而不要施加重力。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
 //if (!Climbing) { if (Climbing) { velocity -= contactNormal * (maxClimbAcceleration * Time.deltaTime); } else { velocity += gravity * Time.deltaTime; } 

 

 

 

只要我們沒有太快移動(或者如果動畫的話,牆壁也不會太快),就可以使我們與牆壁保持聯系,但會導致我們陷入90°的內角。我們可以通過稍微降低抓地力(例如最大加速度的90%)來避免這種情況,這只會使我們減速,而不再使我們停在內角。

 

 

 

  •  
  •  
 velocity -= contactNormal * (maxClimbAcceleration* 0.9f* Time.deltaTime);

 

 

 

 

盡管這可行,但抓地力加速度會減慢從牆壁上跳下來的速度。為了防止在剛跳下時關閉攀爬,就像關閉地面捕捉一樣。我們可以通過使該Climbing屬性還檢查自上次跳轉以來是否已經超過兩個步驟來實現。

 

 

 

  •  
 bool Climbing => climbContactCount > 0&& stepsSinceLastJump > 2;

 

 

 

請注意,需要相對於最大爬升速度的高最大爬升加速度才能可靠地附着在表面上。除此之外,速度不能太高,否則球體可能會在單個物理步驟中最終以太遠的距離將其自身發射到離牆太遠的地方。

 

 

 

 

可選攀爬

 

 

現在,攀岩作品讓我們使其成為可選項。我們通過Climb按鈕進行控制,您可以通過以下步驟進行配置:進入Input項目設置,通過其上下文菜單復制Jump條目,將其重命名為Climb,然后將其分配給其他按鈕。

 

只要按住按鈕,我們就盡可能攀爬,因此我們通過Input.GetButton而不是Input.GetButtonDown在Update中進行檢查。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  1.  
    bool desiredJump, desiresClimbing;
  2.  
    void Update () { desiredJump |= Input.GetButtonDown("Jump"); desiresClimbing = Input.GetButton("Climb");
  3.  
    meshRenderer.material = Climbing ? climbingMaterial : normalMaterial; }

 

 

 

現在,我們只應在EvaluateCollision檢查是否需要攀爬即可。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
 if ( desiresClimbing &&upDot >= minClimbDotProduct && (climbMask & (1 << layer)) != 0 ) { climbContactCount += 1; climbNormal += normal; connectedBody = collision.rigidbody; } 

 

 

 

 

 

 

攀爬欲望減慢運動

 

 

我們可以做的另一件事是,當仍希望在地面上攀爬時減慢運動速度。如果我們要接近隔離牆,那就像放慢腳步,期待攀爬。如果我們要到達牆頂,這也可以防止我們突然跑開,從而改善控制能力。它還可以有效地使“攀爬”按鈕起到慢速按鈕的雙重作用,如果您使用鍵而不是操縱桿來控制球體,這將很方便。

 

我們可以通過使用AdjustVelocity中的最大爬升速度來做到所有這些,即使我們沒有爬升,但我們在地面上並希望爬升。

 

 

 

  •  
  •  
 acceleration = OnGround ? maxAcceleration : maxAirAcceleration; speed =OnGround && desiresClimbing ? maxClimbSpeed :maxSpeed;

 

 

 

但是,這還不足以防止球體在到達牆頂后可能自行發射。為此FixedUpdate,如果我們不是在攀爬,而是希望並在地面上,我們還必須將攀爬抓地加速度與重力一起應用。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
 if (Climbing) { velocity -= contactNormal * (maxClimbAcceleration * 0.9f * Time.deltaTime); } else if (desiresClimbing && OnGround) { velocity += (gravity - contactNormal * (maxClimbAcceleration * 0.9f)) * Time.deltaTime; } else { velocity += gravity * Time.deltaTime; } 

 

 

 

 

現在,我們可以可靠地從牆的頂部移動到牆壁的一側,我們也可以可靠地進入一種情況,在這種情況下,我們正在前進以開始向下爬升,然后又切換為再次向上爬升。只要我們不斷向前推進,就可以反復進行。這是我們的控制切換方法的缺點。最好的攀爬方法是將相機朝向牆壁。

 

 

 

 

 

站在斜坡上

 

 

我們可以使用相同的技巧,使我們在地面上站立時仍能保持攀岩的抓地力。通常,重力應將球體向下拉,以便球體緩慢滑下斜坡,但是當靜止不動時,自動施加力以抵消重力是有意義的。我們可以通過在地面上並且速度非常低(例如小於0.1,或者平方的情況下為0.01)時將重力投影到接觸法線上來進行模擬。這樣就消除了引起滑動的重力分量,同時仍將球體拉到表面。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
 if (Climbing) { velocity -= contactNormal * (maxClimbAcceleration * 0.9f * Time.deltaTime); } else if (OnGround && velocity.sqrMagnitude < 0.01f) { velocity += contactNormal * (Vector3.Dot(gravity, contactNormal) * Time.deltaTime); } else if (desiresClimbing && OnGround) { velocity += (gravity - contactNormal * (maxClimbAcceleration * 0.9f)) * Time.deltaTime; } else { velocity += gravity * Time.deltaTime; } 

 

 

 

 

 

 

爬出裂縫

 

 

不幸的是,當球體卡在縫隙中時,我們的攀爬方法不起作用,這是陡峭的接觸點轉換為地面接觸點的情況。在這種情況下,我們最終會停留在有效的水平面上,這與我們的攀岩控制裝置(主要是垂直表面)不起作用。為了擺脫這種情況,我們將跟蹤我們檢測到的上一次攀爬法線。

 

 

 

  •  
 Vector3 contactNormal, steepNormal, climbNormal, lastClimbNormal;

 

 

 

每次我們在EvaluateCollision累積正常爬坡時都進行設置。

 

 

 

  •  
  •  
 climbNormal += normal; lastClimbNormal = normal;

 

 

 

然后CheckClimbing確定是否有多個攀爬觸點。如果是這樣,請對爬升法線進行歸一化處理,然后檢查結果是否算作地面,這表明我們處在裂縫狀態。要擺脫困境,只需使用最后的攀爬法線而不是合計值即可。這樣,我們最終會爬上一堵牆,而不會卡住。

 

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
 bool CheckClimbing () { if (Climbing) { if (climbContactCount > 1) { climbNormal.Normalize(); float upDot = Vector3.Dot(upAxis, climbNormal); if (upDot >= minGroundDotProduct) { climbNormal = lastClimbNormal; } } groundContactCount =1; contactNormal = climbNormal; return true; } return false; }

 

 

 

下一個教程是游泳

資源庫(Repository)

https://bitbucket.org/catlikecodingunitytutorials/movement-08-climbing/

往期精選

Unity3D游戲開發中100+效果的實現和源碼大全 - 收藏起來肯定用得着

Shader學習應該如何切入?

UE4 開發從入門到入土

聲明:發布此文是出於傳遞更多知識以供交流學習之目的。若有來源標注錯誤或侵犯了您的合法權益,請作者持權屬證明與我們聯系,我們將及時更正、刪除,謝謝。

原作者:Jasper Flick

原文:

https://catlikecoding.com/unity/tutorials/movement/climbing/

翻譯、編輯、整理:MarsZhou

More:【微信公眾號】 u3dnotes


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM