找回密码
 立即注册
查看: 634|回复: 0

[简易教程] Unity基础教程-物体运动(三)——表面接触(Staying in Touch)

[复制链接]
发表于 2020-11-25 11:48 | 显示全部楼层 |阅读模式
200+篇教程总入口,欢迎收藏:
本文重点:
1、保持地面接触,而不要从斜面起飞
2、执行射线检测
3、配置多个层级以及他们的交互。
4、导航楼梯
5、利用陡峭的接触
这是有关控制角色移动的教程系列的第三部分。主要介绍关于改善球体与曲面的交互方式。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
本教程使用Unity 2019.2.14f1创建。它还使用ProBuilder软件包。
(一个跑酷的球)
1 保持地面接触
当球体到达坡道的顶部时,由于其向上的动量,它开始飞行。这是真实的,但可能并不理想。
球体在坡道的顶部飞行
但当球体突然突然出现小的高低差时,也会发生类似的情况。我制作了一个测试场景,以0.1增量的步长演示了这一步。
测试场景
如果步距不太高的话,则以足够的速度接近时,球体会反弹。在测试场景中,这对于平坦车道来说甚至会发生在一些比较平坦的地面上,因为我是通过将步高减小到零而没有合并顶点来实现的。这会产生所谓的幻影碰撞。这应该在设计场景几何物体的时候就避免的,但是我还是将其模拟出来。
在阶梯边弹起
在现实生活中,有多种技术可以将东西固定在地面上。例如,一级方程式赛车可以将气流转换为下压力。因此,为我们的领域做类似的事情是有现实参照的。
1.1 碰撞时间
我们需要考虑一下球体将从坡道发射的那一时刻。为了将其固定在表面上,必须对其速度进行调整,使其与表面重新对齐。让我们检查一下何时可以收到所需的信息。通过在基于OnGround的Update中调整其颜色,让球体不在地面上时将其变为白色,这与上一教程末尾展示的颜色类似。
要观察确切的时间,请暂时减少物理时间步长(physics timestep)和时间缩放(time scale)。
三种物理步长 timestep 0.2; time scale 0.5
球体运动的物理轨道仍然存在碰撞。我们实际上会在下一次步长计算中才能采集到数据,因此球虽然已经不在地面上了,但我们仍然以为它在。但这又是最关键的一个步长采集,后面起飞之后就无法再采集到碰撞数据了。因此,我们总是会落后一步,但是一旦我们能意识到这一点之后,一切就都不是问题了。
1.2 最后一次接触地面的步长
现在,我们追踪一下自从接地以来已经经历了多少物理步长。为其添加一个整数字段,并在UpdateState的开头对其进行递增。然后,如果事实证明我们在地面上,则将其设置回零。我们将使用它来确定何时应该贴紧地面。当然,它对于调试也很有用。
我们不必防范整数溢出吗?
不用担心。整数溢出需要花费几个月的实时时间。
1.3 捕捉
如果需要,添加一个SnapToGround方法,该方法可以使我们始终贴着地面。如果成功,那么我们将被压在地面上。通过返回一个布尔值(最初只是返回false)来指示是否发生了这种情况。
这样,我们可以使用布尔OR方便地将其与UpdateState中的OnGround检查结合起来。之所以可行,是因为只有在OnGround为false时才调用SnapToGround。
SnapToGround仅在我们不接地时才被调用,因此自上次接地以来的Step值会大于零。但是,我们仅应在失去联系后立即尝试捕捉一次。因此,当Step值大于一个时,应该中止。
1.4 射线检测
我们只在球沿着地表的时候进行抓拍。可以通过以下方式进行检查:从球体的位置向下垂直发送射线,并以body.position和下向量作为参数调用Physics.Raycast。物理引擎将执行此射线投射,并返回是否击中某些东西。如果没有,那就没有地面,我们需要中止。
如果射线确实击中了某物,那么我们必须检查它是否算作地面。可以通过第三个RaycastHit结构输出参数检索有关命中信息。
该代码如何工作?
RaycastHit是一个结构体,因此是一个值类型。我们可以通过RaycastHit hit定义一个变量,然后将其作为第三个参数传递给Physics.Raycast。但这是一个输出参数,这意味着它像对象对象引用一样通过引用传递。必须通过向其添加out修饰符来明确指出这一点。该方法负责为其分配值。
除此之外,还可以在参数列表内声明用于输出参数的变量,而不必在单独的行上声明。那就是我们在这里所做的。
hit数据包括一个法线向量,我们可以使用该向量来检查我们命中的表面是否算作地面。如果不是,请中止。请注意,在这种情况下,我们处理的是真实的表面法线,而不是碰撞法线。
1.5 重新与地面对齐
如果我们此时还没有中止,那么我们只是失去了与地面的接触,但仍然在地面之上,因此我们需要抓住它。将地面接触计数设置为1,使用找到的法线作为接触法线,然后返回true。
在我们认为自己已经在地面上了,但其实我们仍在空中。下一步是调整速度,使其与地面对齐。就像对齐所需的速度一样,它的工作原理是必须保持当前速度,并且我们将显式计算该速度,而不是依赖ProjectOnContactPlane。
现在,我们仍然漂浮在地面上,但是重力将有助于将我们拉到地面。而实际上,可能会导致速度下降,在这种情况下,重新调整速度会减慢向地面的收敛速度。因此,仅应在速度与表面法线的乘积为负时调整速度。
这足以使我们的球体在越过顶部时保持在坡道上。它们会漂浮一点点,但是在实践中几乎看不到。即使球体会在某一帧中变成白色,但在FixedUpdate中,我们将球体始终视为接地。只是在我们处于中间状态时才调用Update。
沿着斜坡
它还可以防止球从台阶反弹时导致球起飞。
贴着台阶
请注意,我们仅考虑位于我们下方的单个点来确定我们是否位于地面之上。只要关卡的几何图形不太杂乱或没有太多细节,此方法就可以正常工作。例如,如果射线恰好被投射到一个小的凹陷中,那么很微小的深裂纹可能会导致判定失败。
1.6 最大的捕捉速度
无论如何,过快的速度都是我们的球体被抛起的原因,所以让我们添加一个可配置的最大捕捉速度。默认情况下将其设置为最大速度,以便在可能的情况下始终发生捕捉。
最大的捕捉速度
当当前速度超过最大捕捉速度时,还要中止SnapToGround。我们可以在光线投射之前通过更早地计算速度来做到这一点。
请注意,由于精度限制,将两个最大速度设置为相同的值可能会产生不一致的结果。最好使最大捕捉速度高于或低于最大速度。
相同的最大速度会产生不一致的结果
1.7 探测距离
当球体下方有地面时,无论它有多远,我们都在捕捉,但最好只检查附近的地面。我们通过限制探测范围来做到这一点。没有最佳的最大距离,但是如果捕捉值过低可能会在陡峭的角度或较高的速度下失败,而过高的捕捉则会导致不合理的捕捉到远低于地面的捕捉。使其可配置,最小值为零,默认值为1。由于我们的球体的半径为0.5,这意味着我们要在球体底部以下最多检查半个单位。
探测距离
将距离作为第四个参数添加到Physics.Raycast。
1.8 忽略代理
在检查地面是否贴合时,我们仅考虑可以表示地面的几何图形是有意义的。默认情况下,raycast会检查除置于Ignore Raycast层上的对象以外的所有内容。但是这些不应该检查的对象是会动态变化的,但通常移动的球体是不会的。射线不会偶然碰到要投射的球体,因为我们是从其位置向外发射的,但是我们可能会碰到另一个移动的球体。为避免这种情况,我们可以将其“Layer”设置为“Ignore Raycast”,但现在,让我们为所有活动的对象创建一个新的Layer,并且将其忽略。
通过游戏对象的“Layer”下拉菜单的“Add Layer...”选项或项目设置的“Tags and Layers”部分,转到层设置。然后定义一个新的自定义用户层。我们将其命名为Agent,用于不属于关卡几何体的通用活动实体。
自定义Agent 层 8
将所有球体移动到该层。更改预制层即可。
接下来,向MovingSphere添加一个可配置的LayerMask探测掩码,该掩码最初设置为-1,与所有层匹配。
然后,我们可以配置球体,以便检测除“Ignore Raycast ”和“Agent”之外的所有层。
探测掩码
要应用遮罩,请将其作为第五个参数添加到Physics.Raycast。
1.9 跳跃和捕获
捕捉现在可以使用并且可以配置,但是当我们跳跃时它也会激活,从而消除了向上的动力。为了使跳跃再次起作用,我们必须避免在跳跃后立即弹跳。我们可以通过计算自上次跳跃以来的物理步数来跟踪此情况,就像我们计算自上次停飞以来的步数一样。在UpdateState的开头将其递增,并在激活跳转时将其设置回零。
现在,即使跳转后过早,我们也可以中止SnapToGround。由于碰撞数据的延迟,我们仍然认为启动跳跃后的步骤已接地。因此,如果我们在跳转后走了两个或更少的步骤,就必须中止。
2 楼梯
接下来让我们考虑一种更困难的表面:楼梯。实际上,球体根本无法很好地爬上楼梯,但是无论如何,我们希望它们可以,这它们代表它们能够在楼梯上进行导航。我制作了一个包含五个45°楼梯的测试场景,步长分别为0.1、0.2、0.3、0.4和0.5。
楼梯测试场景
使用默认设置时,球体根本无法处理楼梯。在最大加速度下,大多数设法上升,但是结果不可靠且有弹性,根本没有平稳的运动。试图以一定角度移动而不是直上楼梯几乎是不可能的。
上楼梯,加速度为100
2.1 简单的碰撞体
较大的楼梯台阶使移动几乎无法进行。虽然可以以较小的步长在楼梯弹起,但碰撞会变得随机,运动会变得不稳定和不平滑。
与其尝试与物理引擎战斗,不如我们更加务实并简化工作。即我们要在楼梯上进行平稳,一致,可控制的运动。当我们使用平坦的坡道时,我们可以做到。因此,让我们用坡道代替楼梯的碰撞体。
坡道是楼梯的近似值。最好的折衷方案是设计碰撞体坡道,使其切穿台阶中间。然后,碰撞将发生在可见几何图形的上方和下方。
简化的楼梯
我创建了这样的形状以匹配五个楼梯,首先是作为常规ProBuilder对象。然后,我通过ProBuilder窗口中的Set Collider选项将它们转换为colliders。


简化的楼梯,作为普通物体和碰撞体
禁用楼梯的网格碰撞体组件,但此时不要删除它们。然后暂时将最大地面角度增加到46°,以便球体可以在45°楼梯向上移动。
将楼梯视为坡道,最大地面角度为46°
尽管需要一些附加级别的设计工作,但使用简化的碰撞体上楼梯是使它们在物理上可导航的最佳方法。通常,使碰撞形状尽可能简单是一个好主意,从性能和运动稳定性方面考虑,可以避免不必要的细节。因此,我们将一直使用这种方法。但这是一个近似值,因此在仔细检查时,你会发现球体切穿并悬停在楼梯台阶上方。但是,从远处和运动中通常并不太明显。
穿帮
我们能避免慢慢地滚下楼梯吗?
当我们聚焦于于重力时,就可以。我们将在以后进行处理。
2.2 细节和楼梯层级
我们使用简化的对撞机进行球体与楼梯的交互并不意味着我们不能将原始的楼梯碰撞体用于其他碰撞。例如,我们可能希望小碎片正确地降落在各个楼梯台阶上,而不是从坡道上滑下来。让我们通过增加两层来实现这一点:一层用于细节信息,一层用于楼梯对象。
细节和楼梯对象的层
探针遮罩应包括“Stairs”层,但不包括“Detailed”层。
调整探测遮罩
接下来,转到项目设置的“Physics ”部分,并调整图层碰撞矩阵。楼梯仅应与代理交互,而代理不应与“Detailed”交互。
碰撞矩阵
现在,再次启用楼梯网格碰撞器组件。然后添加一些小的刚体对象,使其落在它们之上,以同时查看两种相互作用。如果为这些物体提供足够低的质量(例如0.05),则球体将能够将它们推到一边。
同一个楼梯的两种碰撞方式
2.3 最大的楼梯角度
如果我们能够爬楼梯,那么我们使用的楼梯最大角度与正常地面使用的最大角度应该是有科学依据的。因此,为它们添加一个单独的最大角度,默认情况下设置为50°。
最大的地面和楼梯角度
我们现在必须与那个值进行计算取决于我们所使用的表面类型。我们将为此添加一个可配置的楼梯掩码选项,类似于探针掩码。
楼梯遮罩
为什么不使用LayerMask.NameToLayer(“ Stairs”)?
这也可以,但是通过使用遮罩,我们不再依赖于硬编码的图层名称,并且更加灵活,这也使实验变得更加容易。
创建一个新的GetMinDot方法,该方法返回给定图层的适当最小值,该整数是整数。假设我们可以直接比较楼梯的掩码和图层,然后如果它们不相等,则返回最小地面点积,否则返回最小楼梯点积。
但是,该掩码是位掩码,每层一位。具体来说,如果楼梯是第11层,则它与第11位匹配。我们可以通过使用1  < <层 来创建具有该单个位的值,该值将左移运算符应用于数字1的次数等于层索引(十)。结果将是二进制数10000000000。
如果掩码仅选择了一个图层,这会起作用,但是让我们为任何图层组合支持一个掩码。通过采用掩码和图层位的布尔AND来实现。如果结果为零,则该层不属于掩码的一部分。
在EvaluateCollision的开头检索正确的最小点值,并使用它来检查触点是否算作接地。
检查我们是否在地面上时,也可以在SnapToGround中使用GetMinDot。
3 峭壁接触
除了与地面接触之外,也有可能和其他地方接触。物体移动需要和地面接触,但有时我们可能仅仅与墙壁接触。那样的话我们就会陷入比较被动的局面。如果我们有空气加速功能,在这种情况下仍然可以控制,但是通过一些额外的工作,我们可以做更多的事情。
3.1 检测峭壁的接触
陡峭接触指的是太陡峭而不能算作地面的接触,但不是天花板或悬垂得物体。让我们在字段和属性中跟踪此类接触的法线和计数,就像我们对常规接地接触所做的那样。
还要在ClearState中重置新数据。
在EvaluateCollision中,如果没有接地,请检查它是否为陡峭的接触。完美垂直的墙的点积应为零,但让我们稍微宽一点,接受高于-0.01的所有值。
3.2 裂缝
缝隙是有问题的,因为一旦卡在缝隙中而没有空气跳跃,除非空气加速度很大,否则就不可能逃脱。我创建了一个带有小裂缝的测试场景来演示这一点。
裂缝测试场景
这个想法是,如果我们最终接地,则不需要峭壁接触。但是当连捕获都无法发现地面时,我们的下一个最佳选择就是检查裂缝或类似情况。如果我们发现自己被楔入狭窄的空间中,并且有多个陡峭的接触点,那么可以通过推压那些接触点来移动。
创建一个CheckSteepContacts,返回是否成功将陡峭的接触转换为虚拟地面。如果有多个陡峭的接触点,则将它们归一化,并检查结果是否算作接地。如果是这样,则返回成功,否则返回失败。在这种情况下,我们不必检查楼梯。
将CheckSteepContacts添加为UpdateState中对接地状态的第三次检查。
现在,我们可以在裂隙中以及之前被卡住的类似地方移动并跳跃。
逃出裂缝
3.3 墙跳
再回顾一下跳墙。之前,我们仅将跳跃限制在地面或空中。但是,如果我们将跳跃方向基于陡峭法线而不是接触法线,那么我们也可以支持从墙壁上跳跃。
开始制作跳转方向变量,并删除“Jump”中的当前有效性检查。
相反,请检查我们是否在地面上。如果是这样,请使用常开触点作为跳转方向。如果不是,那么下一个检查是我们是否处在峭壁的状态。如果是这样,请改用峭壁法线。之后,检查空气跳动,为此我们再次使用接触法线,该法线已设置为向上向量。如果所有这些都不适用,则不可能进行跳转,我们将中止。
墙壁跳跃
3.4 空气跳跃
此时,我们应该重新考虑空中跳跃。检查跳跃阶段是否小于最大空中跳跃的唯一作用是,在跳跃之后,该阶段会立即重置为零,因为在下一步中,我们仍然算作接地。因此,只有在启动跳转后下一步时,才应在UpdateState中重置跳转阶段,以避免错误的着陆。
为了使空气跳跃保持正常工作,我们现在必须检查跳跃阶段是否小于或等于“Jump”中的最大值。
但是,这样可以使空气从表面掉下来后再跳一次而不跳动。为防止这种情况,我们在空气跳跃时跳过了第一个跳跃阶段。
但这仅在完全允许空气跳跃的情况下才有效,因此首先检查一下。
最后,让墙跳重设跳跃阶段,以便有可能将墙跳变成新的空中跳跃阶段。
空气-墙-空气 跳跃
3.5 向上跳跃偏差
跳下垂直墙不会增加垂直速度。因此,尽管有可能会在附近的相对壁之间反弹,但重力始终会将球体拉下。我用两个块制作了一个测试场景来演示这一点。
卡在底部;jump 高度 为3
但是,某些游戏将跳墙作为达到高高度的一种手段。我们可以通过在跳转方向上增加一个向上的偏差来支持这一点。最简单的方法是将上矢量添加到跳转方向并将结果标准化。最终方向是两个方向的平均值,因此从平坦地面的跳跃不会受到影响,而从完全垂直的墙壁上跳跃的影响最大,成为45°跳跃。
向上的墙跳;jump 高度为3
这会影响并非在完全平坦的地面或空中的所有跳跃轨迹,这在爬坡时跳跃时最明显。


跳跃带有和不带有 偏差
最后,从Update移除调试球的颜色。
下一节,介绍 轨道摄像机。
本文翻译自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-23 10:31 , Processed in 0.104643 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表