喵的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

posted @ 2020-08-31 12:43  MarsZhou  阅读(971)  评论(0编辑  收藏  举报