![9947218fee0f6d1b4b1992698b30afd8.png](https://i-blog.csdnimg.cn/blog_migrate/ea44b29709f9e26115ba38c94eb7922c.png)
检测水量。
施加水阻力和浮力。
在水上游泳,包括上下游泳。
使物体漂浮。
这是关于控制角色移动的系列教程的第九部分。它可以漂浮在水中并在水中移动。
本教程使用Unity 2019.4.1f创建。它还使用ProBuilder软件包。
Unity升级
![a6f7aef8177903d3fc720dd84871dde3.png](https://i-blog.csdnimg.cn/blog_migrate/051a2f6a77f79e913ea2d267fa5cbc8c.png)
我已升级到Unity 2019.4 LTS和ProBuilder 4.2.3版本,因此某些视觉效果已更改。
效果之一
![6c3da9d72bb0a58a9ac9faa5167791dc.png](https://i-blog.csdnimg.cn/blog_migrate/7df22520688702f1a6c4ded0d92e83a4.jpeg)
水
很多游戏都含有水,而且通常都可以游泳。但是,没有针对互动式水的开箱即用的解决方案。PhysX不直接支持它,因此我们必须自己创建一个近似的水。
水景
为了演示水,我创建了一个包含游泳池的场景。它具有各种岸边配置,两个水平面,两个水隧道,一个水桥以及可以在水底行走的地方。我们的水也可以在任意重力下工作,但是此场景使用简单的均匀重力。
![41f8344ee19165ce01fdd2ed34e6b135.png](https://i-blog.csdnimg.cn/blog_migrate/81c7cbd880dc1d2cacf3a06677bea5af.png)
水面由具有半透明蓝色材料的单面扁平网制成。从上方可见,但从下方看不到。
![27eea1329893388735fc4e0e6c0d6b97.png](https://i-blog.csdnimg.cn/blog_migrate/181c61d7c3230dad72344d1d9a335d08.png)
必须使用设置为触发器的对撞机来描述水的体积。我在大多数体积中都使用了不带网孔的箱式对撞机,缩放比例略大于所需的体积,因此水中不会有任何缝隙。一些地方需要更复杂的ProBuilder网格以适合体积。还必须将其设置为触发器,这可以通过ProBuilder窗口中的“ 设置触发器”选项来完成。请注意,作为触发器的网格碰撞器必须是凸形的。凹面网格会自动生成将其包裹起来的凸面版本,但会导致它戳出所需水量的地方。弯曲的水桥就是一个例子,为此我制作了一个简化的凸对撞机。
![6ed2094a9f42a3f9a1b1eb48aa1dff87.png](https://i-blog.csdnimg.cn/blog_migrate/3e124e6ebf90479d036686130302a5a1.png)
忽略触发器碰撞器
所有水体积对象都在“ 水”层上,应将其排除在运动球体和轨道摄影机的所有层蒙版中。即使到那时,通常我们目前拥有的两个物理查询也仅用于常规对撞机,而不是触发器。可以通过“ 物理/查询命中触发器”项目设置来配置是否检测到触发器。但是我们永远都不想使用代码来检测触发器,因为我们现在拥有什么,因此无论项目设置如何,我们都将其明确化。
第一个查询在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中添加水面罩以及游泳材料,我们将用它来证明它在水中。
[SerializeField]
LayerMask probeMask = -1, stairsMask = -1, climbMask = -1, waterMask = 0;
[SerializeField]
Material
normalMaterial = default,
climbingMaterial = default,swimmingMaterial = default;
![1a7cd348af6b4cfcc482035670ff71f5.png](https://i-blog.csdnimg.cn/blog_migrate/3863bf1b02258cb9971278f63fd9c142.png)
然后添加一个InWater
指示球体是否在水中的属性。首先,我们将其设为一个简单的get / set属性,并在 ClearState中
将其重置为false
。
bool InWater { get; set; } … void ClearState () { … InWater = false; }
如果我们不攀爬,请在Update中使用该属性选择中的游泳材料。
void Update () { … meshRenderer.material = Climbing ? climbingMaterial : InWater ? swimmingMaterial :normalMaterial; }
最后,通过添加OnTriggerEnter
和OnTriggerStay
方法完成对水的检测。它们的工作方式OnCollisionEnter
与OnCollisionStay
相同,不同之处在于它们适用于对撞机,并且具有Collider
参数而不是Collision
。两种方法都应检查对撞机是否在水层上,如果设置IsSwimming
为true
。
void OnTriggerEnter (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { InWater = true; } } void OnTriggerStay (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { InWater = true; } }
![fb2e92f429d9fe37c18b811d545fbae3.png](https://i-blog.csdnimg.cn/blog_migrate/91e164ec28d57e50d86c6109dd270eb4.png)
何时调用触发方法?
![a6f7aef8177903d3fc720dd84871dde3.png](https://i-blog.csdnimg.cn/blog_migrate/051a2f6a77f79e913ea2d267fa5cbc8c.png)
所有触发方法都在所有碰撞方法之前被调用。
淹没
仅仅知道我们的球体是否与水相交,还不足以使其正常游泳或漂浮。我们需要知道其中有多少被淹没,然后我们可以用它来计算阻力和浮力。
浸没程度
让我们添加一个淹没浮点字段来跟踪球体的淹没状态。值零表示没有水接触,而值1表示完全在水下。然后进行更改InWater
,使其仅返回淹没是否为正。在ClearState中将其设置回零。
bool InWater=> submergence > 0f; float submergence; … void ClearState () { … //InWater = false; submergence = 0f; }
更改触发器方法,以便它们调用新EvaluateSubmergence
方法,该方法现在仅将淹没设置为1。
void OnTriggerEnter (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(); } } void OnTriggerStay (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(); } } void EvaluateSubmergence () { submergence = 1f; }
淹没范围
我们将使淹没范围可配置。这样,我们可以精确地控制何时球体算在水中以及何时完全浸入水中。我们从球体中心上方的一个偏移点开始测量,一直到最大范围。这样一来,即使我们接触水面,也可以在整个球体进入该区域之前将其完全淹没,或者完全忽略水坑之类的低水位。
![c97eaa99e84691f591872d508c6eca79.png](https://i-blog.csdnimg.cn/blog_migrate/6624405f609d6495ee8a90fc4ac531fe.png)
使偏移量和范围可配置。使用0.5和1作为默认值,以匹配我们的半径0.5球体的形状。范围应为正。
[SerializeField] float submergenceOffset = 0.5f; [SerializeField, Min(0.1f)] float submergenceRange = 1f;
![1c54e53317e85a3854d33b695228756d.png](https://i-blog.csdnimg.cn/blog_migrate/ee1868aab36203479e8ddfa9e405c63d.png)
现在,我们必须在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; } }
要测试浸水值,请使用它为球临时着色。
void Update () { … meshRenderer.material = Climbing ? climbingMaterial : InWater ? swimmingMaterial : normalMaterial; meshRenderer.material.color = Color.white * submergence; }
![10aac680c81a87b9368e78d7861003a5.gif](https://i-blog.csdnimg.cn/blog_migrate/d54f74ac8ee9a228c82b25f2269b836b.gif)
这一直到球体完全浸没的那一刻起作用,因为从那时起,我们从已经在水对撞器内部的点开始投射,因此射线投射无法击中它。但这意味着我们已经完全浸入水中,因此我们只要不打任何东西就可以将浸入设为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; } }
![528cb63b975b3c6e36ae4237d3055f7c.gif](https://i-blog.csdnimg.cn/blog_migrate/6795b35f2d5fa741599a0b1f23451e65.gif)
现在我们可以摆脱淹没可视化了。
//meshRenderer.material.color = Color.white * submergence;
请注意,此方法假定球的中心正下方有水。当球体碰到水体积的侧面或底部时(例如,碰到不真实的水墙时),情况可能并非如此。在这种情况下,我们立即进入完全淹没状态。
水拖
与水相比,水的运动更为缓慢,因为水比空气造成更大的阻力。因此,加速明显较慢,而减速较快。让我们添加对此的支持,并通过添加水拖动选项(默认设置为1)使其可配置。零到10的范围是可以的,因为10会引起巨大的阻力。
[SerializeField, Range(0f, 10f)] float waterDrag = 1f;
![c35527e53e2d8bc224210cd2a8ac8eff.png](https://i-blog.csdnimg.cn/blog_migrate/0f185f36c73a33a12c333dee70d7a03d.png)
我们将使用简单的线性阻尼,类似于PhysX。我们将速度缩放1减去阻力乘以时间增量。在FixedUpdate中调用AdjustVelocity之前进行此操作。我们首先应用阻力,所以总是可以加速。
void FixedUpdate () { Vector3 gravity = CustomGravity.GetGravity(body.position, out upAxis); UpdateState(); if (InWater) { velocity *= 1f - waterDrag * Time.deltaTime; } AdjustVelocity(); … }
请注意,这意味着如果水阻力等于1除以固定时间步长,则速度会在单个物理步长中下降为零。如果速度变大,速度将反转。由于我们将最大值设置为10,因此这不会成为问题。为了安全起见,可以确保速度至少缩放为零。
如果我们没有完全淹没,那么我们就不会遇到最大的阻力。因此,因素会浸入阻尼中。
velocity *= 1f - waterDrag *submergence *Time.deltaTime;
![46af8eb5bc15743cee41ea4ee0e846e8.gif](https://i-blog.csdnimg.cn/blog_migrate/10e3063220fa5888d88149bf716c82d8.gif)
浮力
水的另一个重要属性是事物倾向于将其漂浮在水中。因此,应向我们的球体添加一个可配置的浮力值,该浮力值的最小值为零,默认值为1。该想法是,浮力值为零的物体像石头一样下沉,只是被水拖慢了速度。浮力为1的对象处于平衡状态,完全消除了重力。浮力大于1的物体会浮到水面。2的浮力意味着它的上升和正常下降一样快。
[SerializeField, Min(0f)] float buoyancy = 1f;
![7f45a1a0c225363a53d22a377805fab8.png](https://i-blog.csdnimg.cn/blog_migrate/56ce0f997a0fd42f41876dec44f5fda8.png)
我们通过在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) { … }
![71960e1130b178e05146887dbc7f56f0.gif](https://i-blog.csdnimg.cn/blog_migrate/d6ecf320d0d07c80b33f789855b09a5a.gif)
请注意,实际上向上的力会随着深度的增加而增加,而在我们的情况下,一旦达到最大浸入力,向上的力就保持恒定。这足以产生令人信服的浮力,除非在极深的水中玩耍。
浮力似乎失败的唯一情况是球体最终距离底部太近。在这种情况下,地面弹跳被激活,抵消了浮力。如果我们在水中,我们可以通过中止SnapToGround来避免这种情况。
bool SnapToGround () { if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2|| InWater) { return false; } … }
游泳
现在我们可以在水中漂浮了,下一步就是支持游泳,其中应该包括潜水和浮潜。
游泳门槛
我们只有在水深的情况下才能游泳,但是我们不需要完全浸入水中。因此,让我们添加一个可配置的游泳阈值,该阈值定义游泳所需的最小浸入度。它必须大于零,因此使用0.01–1作为其范围,默认值为0.5。如果球体的至少下半部在水下,则可以使球体游泳。还添加一个Swimming
指示是否达到游泳阈值的属性。
[SerializeField, Range(0.01f, 1f)] float swimThreshold = 0.5f; … bool Swimming => submergence >= swimThreshold;
![4d271ca069bbfb3a27b4cadc4f15e3e4.png](https://i-blog.csdnimg.cn/blog_migrate/54ceba1ca64eb135e2822b59ecdce905.png)
在Update进行调整,以便仅在游泳时使用游泳材料。
void Update () { … 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。
[SerializeField, Range(0f, 100f)] float maxSpeed = 10f, maxClimbSpeed = 4f, maxSwimSpeed = 5f; [SerializeField, Range(0f, 100f)] float maxAcceleration = 10f, maxAirAcceleration = 1f, maxClimbAcceleration = 40f, maxSwimAcceleration = 5f;
![be8226120307dcdb02e66a29a9830ad6.png](https://i-blog.csdnimg.cn/blog_migrate/9021e6839fb0b9f7a71cb1cdda6ccd25.png)
在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 );
![46c63bc320304239b81544bab02dce9c.gif](https://i-blog.csdnimg.cn/blog_migrate/04cc70cc4697f21e239f4d695153bbb8.gif)
潜水和堆焊
现在,我们可以像在地面或空中一样在游泳时移动,因此受控的移动被限制在地面上。垂直运动目前仅是由于重力和浮力。为了控制垂直运动,我们需要第三个输入轴。通过将UpDown轴添加到我们的输入设置中(通过复制Horizontal或Vertical)来支持这一点。我将空格(用于跳跃的键)用于正键,将X用作负键。然后将playerInput
字段更改为一个Vector3,并在游泳时将其Z分量设置为UpDown轴,否则在Update将其设置为零。从现在开始,我们必须使用的ClampMagnitude
版本的Vector3
。
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); … }
找到当前和新的Y速度分量,并在AdjustVelocity结尾用它们调整速度。这与X和Z相同,但仅在游泳时才执行。
void AdjustVelocity () { … velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ); if (Swimming) { float currentY = Vector3.Dot(relativeVelocity, upAxis); float newY = Mathf.MoveTowards( currentY, playerInput.z * speed, maxSpeedChange ); velocity += upAxis * (newY - currentY); } }
![2d00e990218133622564345afbbbb6a2.gif](https://i-blog.csdnimg.cn/blog_migrate/4cf45af87d894a7c1e603b20c07b2e88.gif)
爬和跳
淹没时应该很难爬上或跳下。我们可以通过在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并使用其连接的刚体。如果我们在浅水中,我们将忽略它。
void OnTriggerEnter (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(other); } } void OnTriggerStay (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(other); } } void EvaluateSubmergence (Collider collider) { … if (Swimming) { connectedBody = collider.attachedRigidbody; } }
如果我们连接到水体,则不应用EvaluateCollision中的另一个水体代替它。实际上,我们根本不需要任何连接信息,因此我们可以在游泳时跳过EvaluateCollision所有工作。
void EvaluateCollision (Collision collision) { if (Swimming) { return; } … }
![2af6773e99b6c5e41efbca89ee681d22.gif](https://i-blog.csdnimg.cn/blog_migrate/3c7f2104c4487d105aedbacd0c7de6c2.gif)
漂浮物
现在我们的球体可以游泳了,如果有一些漂浮的物体可以互动,那就太好了。再次,我们必须自己对此进行编程,方法是将其支持添加到已经支持自定义重力的现有组件中。
淹没
像一样MovingSphere
,向CustomGravityRigidbody中添加submergenceOffset ,submergenceRange ,buoyancy ,waterDrag 和 waterMask ,除了我们不需要游泳加速度,速度或阈值之外。
[SerializeField] float submergenceOffset = 0.5f; [SerializeField, Min(0.1f)] float submergenceRange = 1f; [SerializeField, Min(0f)] float buoyancy = 1f; [SerializeField, Range(0f, 10f)] float waterDrag = 1f; [SerializeField] LayerMask waterMask = 0;
![506b5260583da34b49e612d43fdcfcae.png](https://i-blog.csdnimg.cn/blog_migrate/bc01d53993541bcedaa21037fd86ff6c.png)
接下来,我们需要一个淹没字段。如果需要,在FixedUpdate中施加重力之前将其重置为零。确定淹没时,我们还需要知道重力,因此也要在野外对其进行跟踪。
float submergence; Vector3 gravity; … void FixedUpdate () { … gravity = CustomGravity.GetGravity(body.position); if (submergence > 0f) { submergence = 0f; } body.AddForce(gravity, ForceMode.Acceleration); }
然后添加所需的触发方法以及EvaluateSubmergence
方法,该方法的工作原理与以前相同,只是我们仅在需要时才计算向上轴,并且不支持连接的物体。
void OnTriggerEnter (Collider other) { if ((waterMask & (1 << other.gameObject.layer)) != 0) { EvaluateSubmergence(); } } 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;
![99b007770234d30f15ceee20bca58955.png](https://i-blog.csdnimg.cn/blog_migrate/b4d29d1aa5d12a1da146ca537d875689.png)
浮动对象现在可以在浮动时以任意旋转结束。通常,物体会以最轻的一面朝上的方式漂浮。我们可以通过添加可配置的浮力偏移矢量(默认设置为零)来模拟这一点。
[SerializeField] Vector3 buoyancyOffset = Vector3.zero;
然后,我们通过调用 AddForceAtPosition
而不是AddForce,在此时应用浮力而不是对象的原点,并将偏移量转换为单词空间作为新的第二个参数。
body.AddForceAtPosition( gravity * -(buoyancy * submergence), transform.TransformPoint(buoyancyOffset), ForceMode.Acceleration );
由于重力和浮力现在作用于不同的点,因此它们会产生角动量,从而将偏移点推到顶部。较大的偏移会产生更强的效果,这会导致快速振荡,因此应将偏移保持较小。
![559d43e89682ab385fc215d047cbdcc7.png](https://i-blog.csdnimg.cn/blog_migrate/4b201cde29b2d321e72db226604a2e7b.png)
![5215e7cbd1abdf16aa4bd9e523490208.png](https://i-blog.csdnimg.cn/blog_migrate/9174b89db722438dbc04a079acf807dd.png)
与浮动对象互动
当在其中漂浮着物体的水中游泳时,轨道摄像机会来回晃动,因为它试图停留在物体的前面。可以通过添加一个与常规图层类似的透视图层来避免这种情况,只是将轨道摄像机设置为忽略它。
![d361274b917fda463a18e8ba883e0cd3.png](https://i-blog.csdnimg.cn/blog_migrate/bd7e5dd3544f62511d2932536a4b44b0.png)
该层仅应用于足够小以忽略或与之交互的对象。
![833e332d43c3f7f142b4b76645ffcd3d.gif](https://i-blog.csdnimg.cn/blog_migrate/fdc605ded7d3b3473c7167471d828e86.gif)
当透视对象遮挡视图时,我们可以使它们不可见吗?
![a6f7aef8177903d3fc720dd84871dde3.png](https://i-blog.csdnimg.cn/blog_migrate/051a2f6a77f79e913ea2d267fa5cbc8c.png)
是的,在这种情况下可以检测到它,可以用来更改对象的可视化。但是,这不是本教程的一部分。
稳定浮动
我们当前的方法适用于小型物体,但不适用于较大且不均匀的物体。例如,大的浮动块在球体与其交互时应保持更稳定。为了增加稳定性,我们必须将浮力作用扩展到更大的区域。这需要更复杂的方法,因此CustomGravityRigidbody
将其复制并重命名为StableFloatingRigidbody
。用偏移矢量数组替换其浮力偏移。将浸入也转换为数组,并以Awake
与偏移数组相同的长度创建它。
public classStableFloatingRigidbody: MonoBehaviour { … [SerializeField] //Vector3 buoyancyOffset = Vector3.zero; Vector3[] buoyancyOffsets = default; … float[]submergence; Vector3 gravity; void Awake () { body = GetComponent(); 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); }
通常,对于任何盒子形状,四个点就足够了,除非它们很大或经常部分掉出水面。请注意,偏移量随对象缩放。同样,增加对象的质量使其更稳定。
![30b363e93f192f10fb91e88f653bbe67.png](https://i-blog.csdnimg.cn/blog_migrate/afc9815b71d99093b24ca0e507f31af0.png)
![2f6faca8c7668ea83398341c051e76d2.gif](https://i-blog.csdnimg.cn/blog_migrate/3ab1ef958e3694342d2e1be633ac4f03.gif)
意外的悬浮
如果一个点最终在表面上方足够高,则其光线投射将失败,这将使其错误地算作完全淹没。对于具有多个浮点的大型物体来说,这是一个潜在的问题,因为有些物体可能最终落在水面之上,而物体的另一部分仍被淹没。结果将是高峰最终浮空。您可以通过将一个较大的轻物体部分地从水中推出来实现此目的。
![5e91b9c220d2962c102102f8add16b31.gif](https://i-blog.csdnimg.cn/blog_migrate/2895114b0427f2a5b9c06cc1e0871403.gif)
该问题仍然存在,因为部分物体仍然接触水。为了解决这个问题,当射线投射无法检查该点本身是否在水量之内时,我们必须执行一个额外的查询。可以通过调用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; } } }
![9c745c1ef9f30a6d90009cf7e85f6800.png](https://i-blog.csdnimg.cn/blog_migrate/1cc0e8bfcabc2232d0a8bf0c3d95869e.png)
![545fb2368895d6c4c0f231c0372fd7f5.gif](https://i-blog.csdnimg.cn/blog_migrate/649a12cb5ba066854e2d0b5d62174db7.gif)
下一个教程是互动环境。
资源库(Repository)![a6f7aef8177903d3fc720dd84871dde3.png](https://i-blog.csdnimg.cn/blog_migrate/051a2f6a77f79e913ea2d267fa5cbc8c.png)
https://bitbucket.org/catlikecodingunitytutorials/movement-09-swimming/
往期精选
Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着
Shader学习应该如何切入?
UE4 开发从入门到入土
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/swimming/
翻译、编辑、整理:MarsZhou
More:【微信公众号】 u3dnotes