很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发。 本文不是广告,不是推广,是免费的纯干货!本文全名:喵的Unity游戏开发之路 - 移动 - 自定义重力 - 在球体上行走

自定义重力

  • 支持任意重力。

  • 使用可变的上轴。

  • 将所有内容拉到一个点。

  • 将自定义重力应用于任意物体。

这是有关控制角色移动的教程系列的第五部分。它涵盖了使用自定义方法替换标准重力的方法,通过该方法,我们支持在球体上行走。

本教程使用Unity 2019.2.21f1制作。它还使用ProBuilder软件包。

效果之一

探索一个小小的星球。

可变重力

到目前为止,我们一直使用固定的重力矢量:垂直往下9.81。这对于大多数游戏而言已足够,但并非全部。例如,目前无法在代表行星的球体表面上行走。因此,我们将添加对自定义重力的支持,而不必统一。

在变得复杂之前,让我们开始简单地翻转重力,并通过项目设置使重力矢量的Y分量为正,看看会发生什么。这有效地将其变成反重力,这应该使我们的球体向上掉落。

事实证明,我们的球体确实向上飞行,但最初紧贴地面。那是因为我们正在将其吸附到地面,并且我们的代码假定法向重力。我们必须对其进行更改,以便它可以与任何重力矢量一起使用。

向上轴(Up Axis)

我们依靠向上轴始终等于Y轴。为了放开这个假设,我们必须向MovingSphere添加一个上轴字段,并使用该字段。为了支持随时变化的重力,我们必须在FixedUpdate的起点设置上轴。它指向与重力相反的方向,因此它等于取反后的归一化重力矢量。

Vector3 upAxis;  …  void FixedUpdate () {    upAxis = -Physics.gravity.normalized;    …  }

现在我们必须用新的上轴替换所有的Vector3.up用法。首先,在UpdateState中,当球体处于空中时,我们将其用作接触法线。

  void UpdateState () {    else {      contactNormal =upAxis;    }  }

其次,在Jump中偏向跳跃方向时。

  void Jump () {    jumpDirection = (jumpDirection +upAxis).normalized;  }

而且,我们还必须调整如何确定跳跃速度。这个想法是我们抵消重力。我们使用的是重力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;    }  }

点积

当我们需要点积时,我们也不能再直接使用法向向量的Y分量。我们必须使用上轴和法线向量作为参数来调用Vector3.Dot。首先在SnapToGround中,检查我们是否发现地面。

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;      }    }  }

现在,无论朝哪个方向,我们的球体都可以移动。在播放模式下也可以更改重力方向,它将立即适应新情况。

相对控制

但是,尽管将重力倒置完全没有问题,但任何其他方向都会使球体的控制更加困难。例如,当重力与X轴对齐时,我们只能控制沿Z轴的移动。沿Y轴的运动是我们无法控制的,只有重力和碰撞会影响它。由于我们仍然在世界空间XZ平面中定义控件,因此消除了输入的X轴。我们必须在重力对齐的平面中定义所需的速度。

重力可以改变,我们也必须为右轴和前轴添加字段让它们变为相对。

  Vector3 upAxis, rightAxis, forwardAxis;

我们需要项目方向在平面上做这项工作,所以让我们把ProjectOnContactPlane换成一个更一般的方法ProjectDirectionOnPlane,适用于任意正常和正常化还执行。

  1.  
    //Vector3 ProjectOnContactPlane (Vector3 vector) { // return vector - contactNormal * Vector3.Dot(vector, contactNormal); //}
  2.  
    Vector3 ProjectDirectionOnPlane (Vector3 direction, Vector3 normal) { return (direction - normal * Vector3.Dot(direction, normal)).normalized; }
 

用这种新方法在AdjustVelocity中确定X和Z控制轴,给它提供轴和法线变量。

  1.  
    void AdjustVelocity () { Vector3 xAxis =ProjectDirectionOnPlane(rightAxis, contactNormal); Vector3 zAxis =ProjectDirectionOnPlane(forwardAxis, contactNormal);
  2.  
    }

重力相对轴在Update中派生。如果一个玩家输入空间存在,我们在重力平面上设置它的右轴和前轴以找到重力对齐的X和Z轴。否则我们赋值为世界坐标轴。现在所需的速度是相对于定义这些轴,所以不需要将输入向量转换为一个不同的空间。

  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");  }

这仍然不能解决控制轴与重力对齐时被消除的问题,但是当使用轨道摄像机时,我们可以对其进行定向,以便重新获得完全控制权。

对准轨道摄像机

轨道摄像头仍然很笨拙,因为它始终将世界Y轴用作其向上方向。因此,当向上或向下看时,我们仍然可以消除控制轴。理想情况下,轨道摄像机将自身与重力对准,这既直观又确保相对运动始终如预期那样起作用。

我们使用轨道角度来控制相机的轨道并对其进行约束,以使其不会太高或太低。无论采用哪种方式,我们都希望保留此功能。这可以通过应用第二次旋转来完成,该旋转使轨道旋转与重力对齐。为此给 OrbitCamera 添加一个Quaternion gravityAlignment字段,并使用身份轮换进行初始化。

Quaternion gravityAlignment = Quaternion.identity;
 

LateUpdate调整开始时,它与当前的向上方向保持同步。为了使轨道在需要调整时不会发生不规则的变化,我们必须使用从当前路线到新路线的最小旋转。可以通过Quaternion.FromRotation找到最小旋转,这会产生从一个方向到另一个方向的旋转。我们的原因是从最后对齐的向上方向到当前的向上方向。然后,将其与当前对齐方式相乘,最后得到新的对齐方式。

  void LateUpdate () {    gravityAlignment =      Quaternion.FromToRotation(        gravityAlignment * Vector3.up, -Physics.gravity.normalized      ) * gravityAlignment;      }

轨道旋转逻辑必须保持不知道重力对准。为此,请添加一个字段以单独跟踪轨道旋转。该四元数包含轨道角度旋转,应在Awake中初始化,并将其设置为与初始摄像机旋转相同的值。我们可以为此使用链接分配。

Quaternion orbitRotation;      void Awake () {    transform.localRotation =orbitRotation =Quaternion.Euler(orbitAngles);  }

仅在手动或自动旋转时才需要在LateUpdate中更改。外观旋转然后变为重力路线乘以轨道旋转。

  1.  
    void LateUpdate () { //Quaternion lookRotation; if (ManualRotation() || AutomaticRotation()) { ConstrainAngles(); orbitRotation= Quaternion.Euler(orbitAngles); } //else { // lookRotation = transform.localRotation; //} Quaternion lookRotation = gravityAlignment * orbitRotation;
  2.  
    }

这在手动调整轨道时有效,但是AutomaticRotation失败了,因为它仅在重力指向下方时才有效。我们可以通过在确定正确的角度之前取消重力对齐来解决此问题。这是通过将反重力比对应用于运动增量来完成的,我们可以通过该方法Quaternion.Inverse获得。

 
  •     Vector3 alignedDelta =      Quaternion.Inverse(gravityAlignment) *      (focusPoint - previousFocusPoint);    Vector2 movement = new Vector2(alignedDelta.x,alignedDelta.z);          
    

    球形重力

    我们支持任意重力,但仍然限于统一矢量Physics.gravity。如果我们想支撑球形重力并在行星上行走,那么我们必须提出一个定制的重力解决方案。

    自定义重力

    在本教程中,我们将使用非常简单的方法。给定在世界空间中的位置,并使用可返回重力矢量CustomGravity的公共方法GetGravity创建静态类。最初,我们将返回未修改的内容Physics.gravity。

     
    1.  
      using UnityEngine;
    2.  
      public static class CustomGravity {
    3.  
      public static Vector3 GetGravity (Vector3 position) { return Physics.gravity; }}
     

    当我们使用重力来确定球面和轨道摄像机的上轴时,我们还要添加一个方便的GetUpAxis方法,再次使用位置参数。

    public static Vector3 GetUpAxis (Vector3 position) {    return -Physics.gravity.normalized;  }
     

    我们可以走得更远,并包括一种可以一举两得的变型方法GetGravity。让我们通过添加向上轴的输出参数来实现。我们通过out在参数定义的前面编写来标记它。

    public static Vector3 GetGravity (Vector3 position, out Vector3 upAxis) {    upAxis = -Physics.gravity.normalized;    return Physics.gravity;  }
     

    输出参数如何工作?

    它的工作方式类似于Physics.Raycast,它返回是否有人命中并将相关数据放入RaycastHit作为输出参数提供的结构中。

    该out关键字告诉我们,方法负责正确设置参数,取代先前的值。不为其分配值将产生编译器错误。

    在这种情况下,其基本原理是GetGravity的主要目的是返回重力矢量,但您也可以通过输出参数同时获得关联的上轴。

    应用自定义重力

    从现在开始,我们可以依靠CustomGravity.GetUpAxisOrbitCamera.LateUpdate执行重力对准。我们将基于当前焦点进行此操作。

        gravityAlignment =      Quaternion.FromToRotation(        gravityAlignment * Vector3.up,        CustomGravity.GetUpAxis(focusPoint)      ) * gravityAlignment;

    并且在MovingSphere.FixedUpdate中我们可以使用CustomGravity.GetGravity基于body的位置来获取重力和上轴。我们必须自己施加引力,只需将其添加到最终速度作为加速度即可。另外,让我们将重力向量传递给Jump

    1.  
      void FixedUpdate () { //upAxis = -Physics.gravity.normalized; Vector3 gravity = CustomGravity.GetGravity(body.position, out upAxis); UpdateState(); AdjustVelocity();
    2.  
      if (desiredJump) { desiredJump = false; Jump(gravity); }
    3.  
      velocity += gravity * Time.deltaTime;
    4.  
      body.velocity = velocity; ClearState(); }

    这样,我们可以在需要时计算重力的大小,而不必再次为我们的位置确定重力。

      void Jump (Vector3 gravity) {    float jumpSpeed = Mathf.Sqrt(2f *gravity.magnitude * jumpHeight);  }

    而且由于我们使用的是自定义重力,因此必须确保标准重力不会应用到球体上。我们可以通过将body的isGravity属性设置为false 来强制执行此操作Awake

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

    走向原点

    尽管我们已切换到自定义重力方法,但所有操作仍应相同。更改Unity的引力矢量会像以前一样影响所有事物。为了使重力变为球形,我们必须进行一些更改。我们将使其保持简单,并使用世界原点作为重力源的中心。因此,上轴只是指向位置的方向。相应地调整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;  }

    这就是使简单的球形重力工作所需要的全部。

    请注意,在小行星上行走和跳跃时,有可能最终陷于围绕它的轨道中。您正在跌倒,但是向前的动量使您像卫星一样掉落在表面上,而不是朝表面倾斜。

    可以通过增加重力或行星半径,允许空气加速或通过引入使您减速的阻力来缓解这种情况。

    推开

    我们不必局限于现实情况。通过使重力为正,我们最终将球体推离原点,从而可以沿球体内部移动。但是,在这种情况下,我们必须翻转上轴。

      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;  }

    其他机构

    我们的球面和轨道摄像头可以使用自定义重力,但是其他一切仍然依赖于默认重力才能下降。为了使具有对象的任意对象Rigidbody落入原点,我们还必须对它们应用自定义重力。

    专门的刚体组件

    我们可以扩展现有Rigidbody组件以添加自定义重力,但是这将使得难以隐藏已经配置了Rigidbody的对象。因此,我们将创建一个新的CustomGravityRigidbody组件类型,它需要一个主体,并在其唤醒时检索对它的引用。它还会禁用常规重力。

     
    1.  
      using UnityEngine;
    2.  
      [RequireComponent(typeof(Rigidbody))]public class CustomGravityRigidbody : MonoBehaviour {
    3.  
      Rigidbody body;
    4.  
      void Awake () { body = GetComponent<Rigidbody>(); body.useGravity = false; }}
     

    要使物体落入原点,我们要做的就是在FixedUpdate其上调用AddForce,并根据其位置将其自定义重力传递给它。

    void FixedUpdate () {    body.AddForce(CustomGravity.GetGravity(body.position));  }
     

    但是重力是一种加速度,因此添加ForceMode.Acceleration第二个参数。

        body.AddForce(      CustomGravity.GetGravity(body.position), ForceMode.Acceleration    );

    为什么飞行方块会抖动?

    发生这种情况的原因与我们的球体抖动一样。当相机也在移动时,对于快速移动的物体尤其明显。如果太明显,则可以使多维数据集插值其位置。也可以添加逻辑以仅在需要时打开插值。

    睡眠

    每次固定更新时Rigidbody都要自己施加引力的缺点是不再沉睡。PhysX尽可能使body进入睡眠状态,有效地使body处于停滞状态,从而减少了要做的工作量。因此,最好限制我们的自定义重力影响多少个body。

    我们可以做的一件事是FixedUpdate,通过调用人体的IsSleeping方法来检查人体在开始时是否处于睡眠状态。如果是这样,它就处于平衡状态,我们不应该打扰它,所以请立即返回。

    1.  
      void FixedUpdate () { if (body.IsSleeping()) { return; }
    2.  
      body.AddForce( CustomGravity.GetGravity(body.position), ForceMode.Acceleration ); }

    但是它永远不会入睡,因为我们对其施加了加速。因此,我们必须首先停止这样做。让我们假设,如果人体的速度很低,它就静止了。我们将使用0.0001阈值作为其速度的平方大小。每秒0.01个单位。它比不施加重力要慢。

    1.  
      void FixedUpdate () { if (body.IsSleeping()) { return; }
    2.  
      if (body.velocity.sqrMagnitude < 0.0001f) { return; }
    3.  
      body.AddForce( CustomGravity.GetGravity(body.position), ForceMode.Acceleration ); }

    那是行不通的,因为尸体开始静止不动,也可能由于种种原因而仍然停留在空中而暂时悬停在适当的位置。因此,让我们添加一个浮动延迟,在此期间我们假定主体处于浮动状态,但可能仍会掉落。除非速度低于阈值,否则它将始终重置为零。在这种情况下,我们要等一秒钟再停止施加重力。如果那还没有足够的时间让body运动,那它应该休息了。

    1.  
      float floatDelay;
    2.  
    3.  
      void FixedUpdate () { if (body.IsSleeping()) { floatDelay = 0f; return; }
    4.  
      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。这不是支持睡眠的唯一方法,但是对于大多数简单情况而言,这是简单而足够的。

    为什么body有时拒绝睡觉?

    发生这种情况是因为PhysX不断做出微小的调整,要么变化非常缓慢,要么在两种状态之间振荡。当存在几乎稳定的碰撞状态时,可能会发生这种情况。

    保持清醒

    我们的方法相当强大,但并不完美。我们做出的一个假设是,重力对于给定位置保持恒定。一旦我们停止施加重力,即使重力突然翻转,物体也会保持原样。在其他情况下,我们的假设也可能失败,例如,当我们漂浮但尚未入睡时,body可能会非常缓慢地移动,或者地板可能会消失。另外,如果body短暂存活,例如暂时的碎屑,我们也不必担心睡觉。因此,让我们可以配置是否允许body漂浮以使其进入睡眠状态。

    1.  
      [SerializeField] bool floatToSleep = false;
    2.  
    3.  
      void FixedUpdate () { if (floatToSleep) { } body.AddForce( CustomGravity.GetGravity(body.position), ForceMode.Acceleration ); }

    下一个教程是“ 复杂重力”。

    资源库(Repository)

    https://bitbucket.org/catlikecodingunitytutorials/movement-05-custom-gravity/

    往期精选

    Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着

    Shader学习应该如何切入?

    UE4 开发从入门到入土

    声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。

    原作者:Jasper Flick

    原文:

    https://catlikecoding.com/unity/tutorials/movement/custom-gravity/

    翻译、编辑、整理:MarsZhou

    More:【微信公众号】 u3dnotes

喵的Unity游戏开发之路 - 在球体上行走的更多相关文章

  1. 喵的Unity游戏开发之路 - 玩家控制下的球的滑动

  2. 喵的Unity游戏开发之路 - 推球:游戏中的物理

    很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学.为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发. 本文不 ...

  3. 喵的Unity游戏开发之路 - 轨道摄像机

    前言        很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学.为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3 ...

  4. 喵的Unity游戏开发之路 - 游泳

    原文: https://mp.weixin.qq.com/s/-ERFNB1GRZ6UAkHOhP9UQw 很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学.为此我们精选了一套国外优秀 ...

  5. 喵的Unity游戏开发之路 - 互动环境(有影响的运动)

    如图片.视频或代码格式等显示异常,请查看原文: https://mp.weixin.qq.com/s/Sv0FOxZCAHHUQPjT8rUeNw 很多童鞋没有系统的Unity3D游戏开发基础,也不知 ...

  6. 喵的Unity游戏开发之路 - 多场景:场景加载

    如果丢失格式.图片或视频,请查看原文:https://mp.weixin.qq.com/s/RDVMg6l41uc2IHBsscc0cQ 很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始 ...

  7. 关于Unity游戏开发方向找工作方面的一些个人看法

     这是个老生常谈,却又是谁绕不过去的话题,而对于每个人来说,所遇到的情况又不尽相同,别人的求职方式和路线不一定适合你,即使是背景很相似的两个人,有时候机遇也很重要. 我本人的工作经验只有一年,就业方式 ...

  8. Unity 游戏开发技巧集锦之创建部分光滑部分粗糙的材质

    Unity 游戏开发技巧集锦之创建部分光滑部分粗糙的材质 创建部分光滑部分粗糙的材质 生活中,有类物体的表面既有光滑的部分,又有粗糙的部分,例如丽江的石板路,如图3-17所示,石板的表面本来是粗糙的, ...

  9. C# Unity游戏开发——Excel中的数据是如何到游戏中的 (二)

    本帖是延续的:C# Unity游戏开发——Excel中的数据是如何到游戏中的 (一) 上个帖子主要是讲了如何读取Excel,本帖主要是讲述读取的Excel数据是如何序列化成二进制的,考虑到现在在手游中 ...

随机推荐

  1. matplotlib常用基础知识

    linestyle(ls)线型参数表 常用color(c)参数表 marker标记符号表 plt常用方法表 plt.legend(loc=0)用于显示图例,图例的位置参数loc matplotlib常 ...

  2. pandas巩固

    导包 import pandas as pd 设置输出结果列对齐 pd.set_option('display.unicode.ambiguous_as_wide',True) pd.set_opti ...

  3. numpy第三方库

    # 导入numpy 并赋予别名 np import numpy as np # 创建数组的常用的几种方式(列表,元组,range,arange,linspace(创建的是等差数组),zeros(全为 ...

  4. PHP sqrt() 函数

    实例 返回不同数的平方根: <?phpecho(sqrt(0) . "<br>");echo(sqrt(1) . "<br>"); ...

  5. PDOStatement::fetch

    PDOStatement::fetch — 从结果集中获取下一行(PHP 5 >= 5.1.0, PECL pdo >= 0.1.0) 说明 语法 mixed PDOStatement:: ...

  6. PDO::quote

    PDO::quote — 为SQL语句中的字符串添加引号.(PHP 5 >= 5.1.0, PECL pdo >= 0.2.1) 说明 语法 public string PDO::quot ...

  7. windows:shellcode 远程线程hook/注入(一)

    https://www.cnblogs.com/theseventhson/p/13199381.html 上次分享了通过APC注入方式,让目标线程运行shellcode.这么做有个前提条件:目标线程 ...

  8. 【SDOI2012】Longge 的问题 题解(欧拉函数)

    前言:还算比较简单的数学题,我这种数学蒟蒻也会做QAQ. --------------- 题意:求$\sum\limits_{i=1}^n gcd(i,n)$的值. 设$gcd(i,n)=d$,即$d ...

  9. hashMap在jdk1.7与jdk1.8中的原理及不同

    在分析jdk1.7中HashMap的hash冲突时,不知大家是否有个疑问就是万一发生碰撞的节点非常多怎么版?如果说成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那就不 ...

  10. JS 本地存储笔记

    本地存储     1.数据存储在用户浏览器中的     2.设置.读取方便.甚至刷新都不会丢失数据     3.容量比较大,sessionStorange约5M,localstorage约20M    ...