Unity基础教程-物体运动(九)——游泳(Moving through and Floating in Water)

2020-11-25 11:33:44 浏览数 (1)

本文重点内容: 1、检测水体 2、应用水的阻力和浮力 3、在水中游泳,包括水面上和水面下 4、让物体漂浮

这是关于控制角色移动的系列教程的第九部分。它让物体能够漂浮在水中并在水中移动。

本教程是CatLikeCoding系列的一部分,原文地址见文章底部。

本教程使用Unity 2019.4.1f制作。它还使用了ProBuilder包。

Unity升级我已经升级到Unity 2019.4 LTS和ProBuilder 4.2.3版本,所以一些视觉效果有所改变。

(泳池里玩的愉快)

1 水

很多游戏都有水,并且大都是可以游泳的。然而,对于交互式水没有现成的解决方案。PhysX并不直接支持它,所以我们必须自己创造一个水的近似值。

1.1 水场景

为了演示水,我创建了一个包含游泳池的场景。它有多种岸形,两个水面,两个水隧道,一座水桥,还有一些你可以在水下行走的地方。我们的水也可以在任意重力下工作,但这个场景使用简单的均匀重力。

(泳池)

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

(水表面)

水的体积必须用设置为触发器的碰撞器来描述。我在大部分的体积中使用了没有网格的盒碰撞器,比需要的尺寸稍微大一些,所以水中不会有任何缝隙。一些地方需要更复杂的ProBuilder网格来建造适当的体积。这些也必须设置为触发器,这可以通过ProBuilder窗口中的set Trigger选项来完成。注意,作为触发器的网格碰撞器必须是凸的。

而凹面网格会自动生成将其包裹起来的凸面版本,但是会导致它超出所需水体积的地方。弯曲的水桥就是一个例子,为此我制作了一个简化的凸碰撞体。

(水碰撞体)

1.2 忽略触发器的碰撞

所有水体积对象都在Water层,应将其排除在运动球体和轨道摄影机的所有layer mask中。常规情况下,我们目前拥有的两个物理查询也仅用于常规碰撞器,而不是触发器。可以通过“Physics / Queries Hit ”项目设置来配置是否检测触发器。无论我们现在有什么,我们都不想使用代码来检测触发器,因将可以将配置明确化。

第一个查询在MovingSphere.SnapToGround中。将QueryTriggerInteraction.Ignore添加为射线投射的最终参数。

其次,对OrbitCamera.LateUpdate中的box进行同样的操作。

1.3 检测水

我们现在可以在水里移动,就好像水不存在一样。但是为了支持游泳,我们必须检测它。通过检查我们是否处于Water层上的触发区来做到这一点。首先在MovingSphere中添加一个Water Mask,以及一个swimming材质,我们将使用它来显示它在水中。

(Water mask 和swimming 材质设置)

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

如果我们不攀爬,则在“Update”中选择swimming材质。

最后,通过添加OnTriggerEnter和OnTriggerStay方法完成对水的检测。它们像OnCollisionEnter和OnCollisionStay一样工作,但它们做用于碰撞器,并且具有Collider参数而不是Collision。两种方法都应检查碰撞器是否在Water层上,如果是,请将IsSwimming设置为true。

(当球在谁中的时候显示蓝色材质)

何时调用trigger 方法? 所有on-trigger方法都在所有on-collision方法之前被调用。

2 浸入

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

2.1 浸入深度

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

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

2.2 浸入范围

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

(偏移和范围)

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

(浸入的偏移和范围)

现在,我们必须使用Water Mask在EvaluateSubmergence中执行射线检测,从偏移点一直向下直至浸入范围。因为我们需要检测是否hit了水,因此请使用QueryTriggerInteraction.Collide。然后,浸入等于1减去击中距离除以范围。

要测试浸入值,为球执行临时着色。

(浸入 不正确)

到球刚好完全浸入的那一刻都是没有问题的,但从那之后,因为我们从一个点投射的射线已经在水的碰撞器里面了,所以它会检测失败。但那也表面球体已经完全的浸入水中了,所以,如果射线没有击中任何东西的话,就设置submergence 为1。

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

(浸水,正确)

现在我们可以去掉浸水的可视化了。

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

2.3 水阻力

在水中的运动比在陆地上更缓慢,因为水产生的阻力比空气大得多。加速度明显较慢,减速较快。让我们添加对阻力的支持,并通过添加一个water drag选项来进行配置,默认设置为1。范围从0到10是可以的,因为10会造成巨大的阻力。

(水阻力)

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

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

如果我们没有完全浸没在水里,我们就不会有最大的阻力。所以需要把阻尼的浸没系数引入其中。

(水阻力10)

2.4 浮力

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

(浮力)

我们通过检查不是攀爬但在水中并在FixedUpdate中实现这个功能。如果满足条件,则应用按1减去浮力的比例缩放的重力,再次将其考虑在内。这将覆盖重力的所有其他应用。

(浮力1.5)

请注意,实际上,向上的力随着深度的增加而增加,而在我们的例子中,一旦达到最大淹没深度,向上的力就会保持不变。这足以创造出令人信服的浮力,除非在极深的水域。

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

3 游泳

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

3.1 游泳阈值

我们只有在水足够深的时候才能游泳,但我们不需要完全沉入水中。因此,让我们添加一个可配置的游泳阈值,它定义游泳所需的最小潜水深度。它必须大于0,所以使用0.01 1作为它的范围,0.5作为默认值。如果球体的下半部分在水下,它就能游动。还要添加一个游泳属性,指示是否达到游泳阈值。

(游泳阈值)

调整Update,以便我们仅在游泳时使用游泳材质。

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

检查我们是否接触地面时,在CheckClimbing之后立即在UpdateState中调用该方法。这样一来,除了攀登外,游泳优先。

然后从SnapToGround中删除检查是否在水中。当我们在水中而不是在游泳时,这使得捕捉动作再次起作用。

3.2 游泳速度

为游泳增加一个可配置的最大速度和加速度,默认设置为5。

(最大的游泳速度和加速度)

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

我们在水里的部分越深,就越应该依靠游泳的加速和速度而不是常规的速度。因此,我们将根据一个游泳参数在规则值和游泳值之间进行插值,这个参数就是潜水深度除以游泳阈值,其最大值被限制为1。

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

(游泳,浮力1.1)

3.3 潜水和浮潜

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

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

(上下游泳,浮力为1)

3.4 攀爬和跳跃

在水下攀爬或跳跃应该很困难。我们可以通过在游泳时忽略玩家的更新输入来拒绝这两种情况。必须明确地抑制攀爬的欲望。跳跃会重置本身。如果在下一次Update之前出现了多个物理步长,攀爬运动在游泳时仍然有可能保持活跃,但这也没关系,因为这是在过渡到游泳时发生的,所以准确的时间并不重要。为了爬出水面,玩家只需在按下攀爬按钮的同时游上去,然后攀爬就会在某个时候启动。

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

3.5 在流动的水中游泳

在本教程中我们不会考虑水流,但我们应该处理整体移动的水体积,因为它们是动画的,就像我们所站或攀爬的常规移动的几何体。为了使之成为可能,我们通过碰撞器来评估碰撞收敛,如果我们最终在游泳,就使用它的附着刚体作为连接体。如果在浅水区,我们会忽略它。

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

(在运动的立方体“水”中,游泳加速度为10)

4 漂浮物

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

4.1 浸没

像MovingSphere一样,向CustomGravityRigidbody添加可配置的浸入偏移,浸入范围,浮力,水阻和水面罩,就像MovingSphere一样,但我们不需要游泳加速度,速度或阈值。

(立方体的Submergence 设置,scale为0.25)

接下来,我们需要一个submergence 字段。如果需要,可在应用重力之前在FixedUpdate的末尾将其重置为零。确定浸入时,我们还需要知道重力,因此也要在域上对其进行跟踪。

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

即使漂浮在水中,物体仍然可以进入休眠状态。如果是这种情况,那么我们可以跳过评估浸没程度。因此,如果物体正在休眠,请不要在OnTriggerStay中调用EvaluateSubmergence。我们仍然在OnTriggerEnter中执行此操作,因为这样可以确保进行更改。

4.2 漂浮

在FixedUpdate中,如果需要的话,应该应用水的阻力和浮力。在本例中,我们通过单独的AddForce调用来应用浮力,而不是将其与普通重力结合使用。

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

(漂浮物)

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

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

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

(轻微的浮力偏移)

4.3 和漂浮物交互

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

(See-through透视层)

这一层应该只用于小到可以忽略的对象,或者与很多对象交互的对象。

(把漂浮物推开)

当透明的物体挡住视线时,我们能让它们隐形吗? 是的,我们可以检测到它,可以用来更改对象的可视化。但是,这不是本教程的一部分。

4.4 固定漂浮物

我们目前的方法对于小的对象很有效,但是对于较大的和不统一的对象看起来就不那么好了。例如,当球体与大型浮动块相互作用时,它们应该保持更稳定。为了增加稳定性,我们必须在更大的区域内扩展浮力效应。这需要更复杂的方法,因此复制CustomGravityRigidbody并将其重命名为StableFloatingRigidbody。用偏移向量数组替换浮力偏移量。将submergence 转换成一个数组,并在Awake中创建它,其长度与偏移数组相同。

调整EvaluateSubmergence,以便它分别评估所有浮力偏移的浸入度。

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

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

(4个稳定点的漂浮物)

4.5 加速升空

如果一个点在离水面足够高的地方结束,那么它的光线投射将失败,这使得它被错误地认为完全淹没了。对于拥有多个浮力点的大型物体来说,这是一个潜在的问题,因为有些物体可能会浮在水面上,而另一部分仍在水下。结果就是最高点最终会悬浮起来。你可以通过将一个大而轻的物体部分推出水面来看到现象。

(被推离之后变为悬浮状态)

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

(安全的浮动表现)

下一章节,环境交互。

0 人点赞