原文:

https://mp.weixin.qq.com/s/-ERFNB1GRZ6UAkHOhP9UQw

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

  • 检测水量。

  • 施加水阻力和浮力。

  • 在水上游泳,包括上下游泳。

  • 使物体漂浮。

这是关于控制角色移动的系列教程的第九部分。它可以漂浮在水中并在水中移动。

本教程使用Unity 2019.4.1f创建。它还使用ProBuilder软件包。

Unity升级

我已升级到Unity 2019.4 LTS和ProBuilder 4.2.3版本,因此某些视觉效果已更改。

效果之一

很多游戏都含有水,而且通常都可以游泳。但是,没有针对互动式水的开箱即用的解决方案。PhysX不直接支持它,因此我们必须自己创建一个近似的水。

水景

为了演示水,我创建了一个包含游泳池的场景。它具有各种岸边配置,两个水平面,两个水隧道,一个水桥以及可以在水底行走的地方。我们的水也可以在任意重力下工作,但是此场景使用简单的均匀重力。

水面由具有半透明蓝色材料的单面扁平网制成。从上方可见,但从下方看不到。

必须使用设置为触发器的对撞机来描述水的体积。我在大多数体积中都使用了不带网孔的箱式对撞机,缩放比例略大于所需的体积,因此水中不会有任何缝隙。一些地方需要更复杂的ProBuilder网格以适合体积。还必须将其设置为触发器,这可以通过ProBuilder窗口中的“ 设置触发器”选项来完成。请注意,作为触发器的网格碰撞器必须是凸形的。凹面网格会自动生成将其包裹起来的凸面版本,但会导致它戳出所需水量的地方。弯曲的水桥就是一个例子,为此我制作了一个简化的凸对撞机。

忽略触发器碰撞器

所有水体积对象都在“ 水”层上,应将其排除在运动球体和轨道摄影机的所有层蒙版中。即使到那时,通常我们目前拥有的两个物理查询也仅用于常规对撞机,而不是触发器。可以通过“ 物理/查询命中触发器”项目设置来配置是否检测到触发器。但是我们永远都不想使用代码来检测触发器,因为我们现在拥有什么,因此无论项目设置如何,我们都将其明确化。

第一个查询在MovingSphere.SnapToGround中。将

QueryTriggerInteraction.Ignore作为最终参数添加到ray cast。

    if (!Physics.Raycast(      body.position, -upAxis, out RaycastHit hit,      probeDistance, probeMask, QueryTriggerInteraction.Ignore    )) {      return false;    } 

其次,对OrbitCamera.LateUpdate中BoxCast执行相同操作。

    if (Physics.BoxCast(      castFrom, CameraHalfExtends, castDirection, out RaycastHit hit,      lookRotation, castDistance, obstructionMask,      QueryTriggerInteraction.Ignore    )) {      rectPosition = castFrom + castDirection * hit.distance;      lookPosition = rectPosition - rectOffset;    } 

检测水

现在,我们可以移动水,好像它不存在一样。但是要支持游泳,我们必须检测到它。我们将通过检查是否在“ 水”层上的触发区域内来完成此操作。首先,在MovingSphere中添加水面罩以及游泳材料,我们将用它来证明它在水中。

  1.  
    [SerializeField]
  2.  
    LayerMask probeMask = -1, stairsMask = -1, climbMask = -1, waterMask = 0;
  3.  
     
  4.  
    [SerializeField]
  5.  
    Material
  6.  
    normalMaterial = default,
  7.  
    climbingMaterial = default,
  8.  
    swimmingMaterial = default;

然后添加一个InWater指示球体是否在水中的属性。首先,我们将其设为一个简单的get / set属性,并在 ClearState中将其重置为false

bool InWater { get; set; }      void ClearState () {    InWater = false;  } 

如果我们不攀爬,请在Update中使用该属性选择中的游泳材料。

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

最后,通过添加OnTriggerEnterOnTriggerStay方法完成对水的检测。它们的工作方式OnCollisionEnterOnCollisionStay相同,不同之处在于它们适用于对撞机,并且具有Collider参数而不是Collision。两种方法都应检查对撞机是否在水层上,如果设置IsSwimmingtrue

  1.  
    void OnTriggerEnter (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { InWater = true; } }
  2.  
    void OnTriggerStay (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { InWater = true; } }

何时调用触发方法?

所有触发方法都在所有碰撞方法之前被调用。

淹没

仅仅知道我们的球体是否与水相交,还不足以使其正常游泳或漂浮。我们需要知道其中有多少被淹没,然后我们可以用它来计算阻力和浮力。

浸没程度

让我们添加一个淹没浮点字段来跟踪球体的淹没状态。值零表示没有水接触,而值1表示完全在水下。然后进行更改InWater,使其仅返回淹没是否为正。在ClearState中将其设置回零。

  1.  
    bool InWater=> submergence > 0f;
  2.  
    float submergence; void ClearState () { //InWater = false; submergence = 0f; }
 

更改触发器方法,以便它们调用新EvaluateSubmergence方法,该方法现在仅将淹没设置为1。

  1.  
    void OnTriggerEnter (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(); } }
  2.  
    void OnTriggerStay (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(); } }
  3.  
    void EvaluateSubmergence () { submergence = 1f; }

淹没范围

我们将使淹没范围可配置。这样,我们可以精确地控制何时球体算在水中以及何时完全浸入水中。我们从球体中心上方的一个偏移点开始测量,一直到最大范围。这样一来,即使我们接触水面,也可以在整个球体进入该区域之前将其完全淹没,或者完全忽略水坑之类的低水位。

使偏移量和范围可配置。使用0.5和1作为默认值,以匹配我们的半径0.5球体的形状。范围应为正。

  1.  
    [SerializeField] float submergenceOffset = 0.5f;
  2.  
    [SerializeField, Min(0.1f)] float submergenceRange = 1f;

现在,我们必须在EvaluateSubmergence中使用水罩执行从偏移点一直向下直至浸入范围的射线投射。在这种情况下,我们确实想击中水,请使用QueryTriggerInteraction.Collide。然后,浸入等于1减去击中距离除以范围。

  void EvaluateSubmergence () {    if (Physics.Raycast(      body.position + upAxis * submergenceOffset,      -upAxis, out RaycastHit hit, submergenceRange,      waterMask, QueryTriggerInteraction.Collide,    )) {      submergence = 1f- hit.distance / submergenceRange;    }  }
 

要测试浸水值,请使用它为球临时着色。

  1.  
    void Update () {
  2.  
    meshRenderer.material = Climbing ? climbingMaterial : InWater ? swimmingMaterial : normalMaterial; meshRenderer.material.color = Color.white * submergence; }

这一直到球体完全浸没的那一刻起作用,因为从那时起,我们从已经在水对撞器内部的点开始投射,因此射线投射无法击中它。但这意味着我们已经完全浸入水中,因此我们只要不打任何东西就可以将浸入设为1。

  void EvaluateSubmergence () {    if (Physics.Raycast(      body.position + upAxis * submergenceOffset,      -upAxis, out RaycastHit hit, submergenceRange,      waterMask, QueryTriggerInteraction.Collide    )) {      submergence = 1f - hit.distance / submergenceRange;    }    else {      submergence = 1f;    }  } 

但是,由于身体位置与PhysX检测到触发时的位置不同,因此从水中移出时可能会导致无效的1淹没,这是由于碰撞和触发方法的调用延迟所致。我们可以通过将射线的长度增加一个单位来防止这种情况。这不是完美的,但几乎可以解决所有情况,除非移动速度非常快。退出水时,这将导致浸水变为负值,这很好,因为这不算在水中。

  void EvaluateSubmergence () {    if (Physics.Raycast(      body.position + upAxis * submergenceOffset,      -upAxis, out RaycastHit hit, submergenceRange+ 1f,      waterMask, QueryTriggerInteraction.Collide    )) {      submergence = 1f - hit.distance / submergenceRange;    }    else {      submergence = 1f;    }  } 

现在我们可以摆脱淹没可视化了。

    //meshRenderer.material.color = Color.white * submergence; 

请注意,此方法假定球的中心正下方有水。当球体碰到水体积的侧面或底部时(例如,碰到不真实的水墙时),情况可能并非如此。在这种情况下,我们立即进入完全淹没状态。

水拖

与水相比,水的运动更为缓慢,因为水比空气造成更大的阻力。因此,加速明显较慢,而减速较快。让我们添加对此的支持,并通过添加水拖动选项(默认设置为1)使其可配置。零到10的范围是可以的,因为10会引起巨大的阻力。

[SerializeField, Range(0f, 10f)]  float waterDrag = 1f;

我们将使用简单的线性阻尼,类似于PhysX。我们将速度缩放1减去阻力乘以时间增量。在FixedUpdate中调用AdjustVelocity之前进行此操作。我们首先应用阻力,所以总是可以加速。

  1.  
    void FixedUpdate () { Vector3 gravity = CustomGravity.GetGravity(body.position, out upAxis); UpdateState();
  2.  
    if (InWater) { velocity *= 1f - waterDrag * Time.deltaTime; }
  3.  
    AdjustVelocity();
  4.  
    }

请注意,这意味着如果水阻力等于1除以固定时间步长,则速度会在单个物理步长中下降为零。如果速度变大,速度将反转。由于我们将最大值设置为10,因此这不会成为问题。为了安全起见,可以确保速度至少缩放为零。

如果我们没有完全淹没,那么我们就不会遇到最大的阻力。因此,因素会浸入阻尼中。

      velocity *= 1f - waterDrag *submergence *Time.deltaTime; 

浮力

水的另一个重要属性是事物倾向于将其漂浮在水中。因此,应向我们的球体添加一个可配置的浮力值,该浮力值的最小值为零,默认值为1。该想法是,浮力值为零的物体像石头一样下沉,只是被水拖慢了速度。浮力为1的对象处于平衡状态,完全消除了重力。浮力大于1的物体会浮到水面。2的浮力意味着它的上升和正常下降一样快。

[SerializeField, Min(0f)]  float buoyancy = 1f;

我们通过在FixedUpdate中检查是否不是在攀登但在水中来实现这一点。如果是这样,请应用按1减去浮力标定的重力,然后再次考虑浸入。这将覆盖重力的所有其他应用。

    if (Climbing) {      velocity -=        contactNormal * (maxClimbAcceleration * 0.9f * Time.deltaTime);    }    else if (InWater) {      velocity +=        gravity * ((1f - buoyancy * submergence) * Time.deltaTime);    }    else if (OnGround && velocity.sqrMagnitude < 0.01f) { … } 

请注意,实际上向上的力会随着深度的增加而增加,而在我们的情况下,一旦达到最大浸入力,向上的力就保持恒定。这足以产生令人信服的浮力,除非在极深的水中玩耍。

浮力似乎失败的唯一情况是球体最终距离底部太近。在这种情况下,地面弹跳被激活,抵消了浮力。如果我们在水中,我们可以通过中止SnapToGround来避免这种情况。

  bool SnapToGround () {    if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2|| InWater) {      return false;    }  } 

游泳

现在我们可以在水中漂浮了,下一步就是支持游泳,其中应该包括潜水和浮潜。

游泳门槛

我们只有在水深的情况下才能游泳,但是我们不需要完全浸入水中。因此,让我们添加一个可配置的游泳阈值,该阈值定义游泳所需的最小浸入度。它必须大于零,因此使用0.01–1作为其范围,默认值为0.5。如果球体的至少下半部在水下,则可以使球体游泳。还添加一个Swimming指示是否达到游泳阈值的属性。

  1.  
    [SerializeField, Range(0.01f, 1f)] float swimThreshold = 0.5f;
  2.  
  3.  
    bool Swimming => submergence >= swimThreshold;

在Update进行调整,以便仅在游泳时使用游泳材料。

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

接下来,创建一个CheckSwimming方法,该方法返回我们是否正在游泳,如果是,则将地面接触计数设置为零,并使接触法线等于上轴。

bool CheckSwimming () {    if (Swimming) {      groundContactCount = 0;      contactNormal = upAxis;      return true;    }    return false;  }

UpdateState中检查我们是否接地时,在CheckClimbing之后直接调用该方法。这样一来,除了攀登外,游泳凌驾一切。

    if (      CheckClimbing() ||CheckSwimming() ||      OnGround || SnapToGround() || CheckSteepContacts()    ) { … } 

然后从SnapToGround中取出检查放在水中。这样一来,当我们在水中而不是在游泳时,捕捉动作就会再次起作用。

 
    //if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2 || InWater) {    if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2) {      return false;    } 

游泳速度

添加可配置的游泳最大速度和加速度,默认情况下均设置为5。

  1.  
    [SerializeField, Range(0f, 100f)] float maxSpeed = 10f, maxClimbSpeed = 4f, maxSwimSpeed = 5f;
  2.  
    [SerializeField, Range(0f, 100f)] float maxAcceleration = 10f, maxAirAcceleration = 1f, maxClimbAcceleration = 40f, maxSwimAcceleration = 5f;

在AdjustVelocity中,检查爬升后是否在水中。如果是这样,请使用与通常情况相同的轴使用游泳加速度和速度。

    if (Climbing) {      acceleration = maxClimbAcceleration;      speed = maxClimbSpeed;      xAxis = Vector3.Cross(contactNormal, upAxis);      zAxis = upAxis;    }    else if (InWater) {      acceleration = maxSwimAcceleration;      speed = maxSwimSpeed;      xAxis = rightAxis;      zAxis = forwardAxis;    }    else {      acceleration = OnGround ? maxAcceleration : maxAirAcceleration;      speed = OnGround && desiresClimbing ? maxClimbSpeed : maxSpeed;      xAxis = rightAxis;      zAxis = forwardAxis;    } 

我们在水中越深,我们应该更多地依赖游泳的加速度和速度而不是常规的速度和速度。因此,我们将基于游泳因子在常规值和游泳值之间进行插值,该因子是淹没除以游泳阈值,且最大值限制为1。

    else if (InWater) {      float swimFactor = Mathf.Min(1f, submergence / swimThreshold);      acceleration =Mathf.LerpUnclamped(        maxAcceleration,maxSwimAcceleration, swimFactor      );      speed =Mathf.LerpUnclamped(maxSpeed,maxSwimSpeed, swimFactor);      xAxis = rightAxis;      zAxis = forwardAxis;    } 

其他加速度是正常加速度还是空气加速度取决于我们是否在地面上。

      acceleration = Mathf.LerpUnclamped(        OnGround ?maxAcceleration: maxAirAcceleration,        maxSwimAcceleration, swimFactor      ); 

潜水和堆焊

现在,我们可以像在地面或空中一样在游泳时移动,因此受控的移动被限制在地面上。垂直运动目前仅是由于重力和浮力。为了控制垂直运动,我们需要第三个输入轴。通过将UpDown轴添加到我们的输入设置中(通过复制HorizontalVertical)来支持这一点。我将空格(用于跳跃的键)用于正键,将X用作负键。然后将playerInput字段更改为一个Vector3,并在游泳时将其Z分量设置为UpDown轴,否则在Update将其设置为零。从现在开始,我们必须使用的ClampMagnitude版本的Vector3

  1.  
    Vector3playerInput; void Update () { playerInput.x = Input.GetAxis("Horizontal"); playerInput.y = Input.GetAxis("Vertical"); playerInput.z = Swimming ? Input.GetAxis("UpDown") : 0f; playerInput =Vector3.ClampMagnitude(playerInput, 1f);
  2.  
     
  3.  
    }

找到当前和新的Y速度分量,并在AdjustVelocity结尾用它们调整速度。这与X和Z相同,但仅在游泳时才执行。

  1.  
    void AdjustVelocity () {
  2.  
    velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
  3.  
    if (Swimming) { float currentY = Vector3.Dot(relativeVelocity, upAxis); float newY = Mathf.MoveTowards( currentY, playerInput.z * speed, maxSpeedChange ); velocity += upAxis * (newY - currentY); } }

爬和跳

淹没时应该很难爬上或跳下。我们可以通过在Update中游泳时忽略玩家的输入来禁止两者。必须明确取消攀爬的愿望。跳跃会重置自身。如果在下一次更新之前进行了多个物理步骤,则仍然有可能在游泳时进行攀爬,但这很好,因为在过渡到游泳的过程中会进行攀爬,因此准确的时间无关紧要。要爬出水面,玩家只需在按下爬升按钮的同时向上游泳,爬升就会在某个时候激活。

if (Swimming) {      desiresClimbing = false;    }    else {      desiredJump |= Input.GetButtonDown("Jump");      desiresClimbing = Input.GetButton("Climb");    }

虽然站在浅水里有跳的可能,但这使它变得困难得多。我们通过将跳跃速度减小1减去浸没除以游泳阈值,以最小为零来模拟这一点。

    float jumpSpeed = Mathf.Sqrt(2f * gravity.magnitude * jumpHeight);    if (InWater) {      jumpSpeed *= Mathf.Max(0f, 1f - submergence / swimThreshold);    }

在流水中游泳

在本教程中,我们将不考虑水流,但是我们应该处理整个运动的水量,因为它们具有动画效果,就像我们站立或攀爬的常规运动几何一样。为了使这种可能成为可能,如果我们结束游泳,将对撞机传递给EvaluateSubmergence并使用其连接的刚体。如果我们在浅水中,我们将忽略它。

  1.  
    void OnTriggerEnter (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(other); } }
  2.  
    void OnTriggerStay (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(other); } }
  3.  
    void EvaluateSubmergence (Collider collider) { if (Swimming) { connectedBody = collider.attachedRigidbody; } }

如果我们连接到水体,则不应用EvaluateCollision中的另一个水体代替它。实际上,我们根本不需要任何连接信息,因此我们可以在游泳时跳过EvaluateCollision所有工作。

  void EvaluateCollision (Collision collision) {    if (Swimming) {      return;    }  } 

漂浮物

现在我们的球体可以游泳了,如果有一些漂浮的物体可以互动,那就太好了。再次,我们必须自己对此进行编程,方法是将其支持添加到已经支持自定义重力的现有组件中。

淹没

像一样MovingSphere,向CustomGravityRigidbody中添加submergenceOffset ,submergenceRange ,buoyancy ,waterDrag 和 waterMask ,除了我们不需要游泳加速度,速度或阈值之外。

  1.  
    [SerializeField] float submergenceOffset = 0.5f;
  2.  
    [SerializeField, Min(0.1f)] float submergenceRange = 1f;
  3.  
    [SerializeField, Min(0f)] float buoyancy = 1f;
  4.  
    [SerializeField, Range(0f, 10f)] float waterDrag = 1f;
  5.  
    [SerializeField] LayerMask waterMask = 0;

接下来,我们需要一个淹没字段。如果需要,在FixedUpdate中施加重力之前将其重置为零。确定淹没时,我们还需要知道重力,因此也要在野外对其进行跟踪。

  • float submergence;
    Vector3 gravity; void FixedUpdate () { gravity = CustomGravity.GetGravity(body.position); if (submergence > 0f) { submergence = 0f; } body.AddForce(gravity, ForceMode.Acceleration); }

    然后添加所需的触发方法以及EvaluateSubmergence方法,该方法的工作原理与以前相同,只是我们仅在需要时才计算向上轴,并且不支持连接的物体。

    1.  
      void OnTriggerEnter (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(); } }
    2.  
      void OnTriggerStay (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(); } } void EvaluateSubmergence () { Vector3 upAxis = -gravity.normalized; if (Physics.Raycast( body.position + upAxis * submergenceOffset, -upAxis, out RaycastHit hit, submergenceRange + 1f, waterMask, QueryTriggerInteraction.Collide )) { submergence = 1f - hit.distance / submergenceRange; } else { submergence = 1f; } }

    即使漂浮在水中,物体仍然可以进入睡眠状态。如果是这种情况,那么我们可以跳过评估淹没程度。因此,如果身体正在睡觉,请不要调用OnTriggerStay中的 EvaluateSubmergence 。我们仍然在OnTriggerEnter中这样做,因为这保证了更改。

      void OnTriggerStay (Collider other) {    if (      !body.IsSleeping() &&      (waterMask & (1 << other.gameObject.layer)) != 0    ) {      EvaluateSubmergence();    }  } 

    漂浮

    在FixedUpdate中,必要时应用水的阻力和浮力。在这种情况下,我们通过单独的AddForce调用而不是将其与法向重力结合来应用浮力。

        if (submergence > 0f) {      float drag =        Mathf.Max(0f, 1f - waterDrag * submergence * Time.deltaTime);      body.velocity *= drag;      body.AddForce(        gravity * -(buoyancy * submergence),        ForceMode.Acceleration      );      submergence = 0f;    } 

    我们还将拖动应用于角速度,以使对象在漂浮时不会保持旋转。

          body.velocity *= drag;      body.angularVelocity *= drag;

    浮动对象现在可以在浮动时以任意旋转结束。通常,物体会以最轻的一面朝上的方式漂浮。我们可以通过添加可配置的浮力偏移矢量(默认设置为零)来模拟这一点。

    [SerializeField]  Vector3 buoyancyOffset = Vector3.zero;

    然后,我们通过调用 AddForceAtPosition而不是AddForce,在此时应用浮力而不是对象的原点,并将偏移量转换为单词空间作为新的第二个参数。

          body.AddForceAtPosition(        gravity * -(buoyancy * submergence),        transform.TransformPoint(buoyancyOffset),        ForceMode.Acceleration      ); 

    由于重力和浮力现在作用于不同的点,因此它们会产生角动量,从而将偏移点推到顶部。较大的偏移会产生更强的效果,这会导致快速振荡,因此应将偏移保持较小。

    与浮动对象互动

    当在其中漂浮着物体的水中游泳时,轨道摄像机会来回晃动,因为它试图停留在物体的前面。可以通过添加一个与常规图层类似的透视图层来避免这种情况,只是将轨道摄像机设置为忽略它。

    该层仅应用于足够小以忽略或与之交互的对象。

    当透视对象遮挡视图时,我们可以使它们不可见吗?

    是的,在这种情况下可以检测到它,可以用来更改对象的可视化。但是,这不是本教程的一部分。

    稳定浮动

    我们当前的方法适用于小型物体,但不适用于较大且不均匀的物体。例如,大的浮动块在球体与其交互时应保持更稳定。为了增加稳定性,我们必须将浮力作用扩展到更大的区域。这需要更复杂的方法,因此CustomGravityRigidbody将其复制并重命名为StableFloatingRigidbody。用偏移矢量数组替换其浮力偏移。将浸入也转换为数组,并以Awake与偏移数组相同的长度创建它。

    1.  
      public classStableFloatingRigidbody: MonoBehaviour {
    2.  
    3.  
      [SerializeField] //Vector3 buoyancyOffset = Vector3.zero; Vector3[] buoyancyOffsets = default; float[]submergence;
    4.  
      Vector3 gravity;
    5.  
      void Awake () { body = GetComponent<Rigidbody>(); body.useGravity = false; submergence = new float[buoyancyOffsets.Length]; } }

    进行EvaluateSubmergence调整,以便分别评估所有浮力偏移量的淹没度。

      void EvaluateSubmergence () {    Vector3 down = gravity.normalized;    Vector3 offset = down * -submergenceOffset;    for (int i = 0; i < buoyancyOffsets.Length; i++) {      Vector3 p = offset + transform.TransformPoint(buoyancyOffsets[i]);      if (Physics.Raycast(        p,down, out RaycastHit hit, submergenceRange + 1f,        waterMask, QueryTriggerInteraction.Collide      )) {        submergence[i] = 1f - hit.distance / submergenceRange;      }      else {        submergence[i] = 1f;      }    }  } 

    然后FixedUpdate中还要对每个偏移量应用阻力和浮力。阻力和浮力都必须除以偏移量,以使最大效果保持不变。对象所经历的实际效果取决于淹没的总数。

      void FixedUpdate () {        gravity = CustomGravity.GetGravity(body.position);    float dragFactor = waterDrag * Time.deltaTime / buoyancyOffsets.Length;    float buoyancyFactor = -buoyancy / buoyancyOffsets.Length;    for (int i = 0; i < buoyancyOffsets.Length; i++) {      if (submergence[i]> 0f) {        float drag =          Mathf.Max(0f, 1f -dragFactor * submergence[i]);        body.velocity *= drag;        body.angularVelocity *= drag;        body.AddForceAtPosition(          gravity *(buoyancyFactor * submergence[i]),          transform.TransformPoint(buoyancyOffsets[i]),          ForceMode.Acceleration        );        submergence[i]= 0f;      }    }    body.AddForce(gravity, ForceMode.Acceleration);  } 

    通常,对于任何盒子形状,四个点就足够了,除非它们很大或经常部分掉出水面。请注意,偏移量随对象缩放。同样,增加对象的质量使其更稳定。

    意外的悬浮

    如果一个点最终在表面上方足够高,则其光线投射将失败,这将使其错误地算作完全淹没。对于具有多个浮点的大型物体来说,这是一个潜在的问题,因为有些物体可能最终落在水面之上,而物体的另一部分仍被淹没。结果将是高峰最终浮空。您可以通过将一个较大的轻物体部分地从水中推出来实现此目的。

    该问题仍然存在,因为部分物体仍然接触水。为了解决这个问题,当射线投射无法检查该点本身是否在水量之内时,我们必须执行一个额外的查询。可以通过调用Physics.CheckSphere位置和小半径(例如0.01)作为参数,然后调用遮罩和交互模式来完成此操作。仅当该查询返回时true,我们才应将淹没设置为1。但是,这可能会导致大量额外的查询,因此,通过添加可配置的安全浮动切换项,使其变为可选。仅对于可以充分推入水中的大型物体才需要。

    [SerializeField]  bool safeFloating = false;      void EvaluateSubmergence () {    Vector3 down = gravity.normalized;    Vector3 offset = down * -submergenceOffset;    for (int i = 0; i < buoyancyOffsets.Length; i++) {      Vector3 p = offset + transform.TransformPoint(buoyancyOffsets[i]);      if (Physics.Raycast(        p, down, out RaycastHit hit, submergenceRange + 1f,        waterMask, QueryTriggerInteraction.Collide      )) {        submergence[i] = 1f - hit.distance / submergenceRange;      }      elseif (        !safeFloating || Physics.CheckSphere(          p, 0.01f, waterMask, QueryTriggerInteraction.Collide        )      ){        submergence[i] = 1f;      }    }  } 

    下一个教程是互动环境

    资源库(Repository)

    https://bitbucket.org/catlikecodingunitytutorials/movement-09-swimming/


    往期精选

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

    Shader学习应该如何切入?

    UE4 开发从入门到入土


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

    原作者:Jasper Flick

    原文:

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

    翻译、编辑、整理:MarsZhou


    More:【微信公众号】 u3dnotes

最新文章

  1. Servlet 单例多线程
  2. 利用IIS应用请求转发ARR实现IIS和tomcat整合共用80端口
  3. 【bzoj3611】 大工程
  4. jquery验证手机号码和固定电话号码
  5. 构建自己的NSZombie
  6. Nginx优化具体,应对高并发
  7. id class
  8. 2012天津C题
  9. Android Studio非gradleproject编译后的apk文件在哪?
  10. 开发win8 metro monogame,显示pubcenter广告时会使游戏卡住的问题的解决方法。
  11. openwrt的编译环境
  12. JavaScript(4)——闭包与this对象以及window对象
  13. JavaSwing JScrollPane的使用
  14. 构建MHA实现MySQL高可用集群架构
  15. nodejs 第一天
  16. 为什么mysql事务回滚后, 自增ID依然自增
  17. 运用HTML5+CSS3和CSS滤镜做的精美的登录界面
  18. python socket编程函数介绍
  19. Unity3d ugui 实现image代码换图
  20. 引子——从Mac OS X的Lion说起

热门文章

  1. CF724C Ray Tracing 扩展欧几里得 平面展开
  2. 使用DEBUG 读取主引导记录
  3. application.yml使用@符合问题:&#39;@&#39; that cannot start any token. (Do not use @ for indentation)
  4. PHP入门之数组
  5. Hello GCN
  6. Java—API/Obiect类的equals toString方法/String类/StringBuffer类/正则表达式
  7. centos环境 使用kubeadm快速安装k8s集群v1.16.2
  8. python设计模式之状态模式
  9. 前端 go.js 流程图基于vue开发项目案例
  10. mac启动 Apache JMeter 5.3 语言选择中文界面出现乱码 问题解决