赞
踩
本系列为参照英文教程的翻译笔记,若介意请阅读原文:Physicscatlikecoding.com
控制刚体球体的速度 通过跳跃支持垂直运动 探测地面及其角度 使用ProBuilder创建测试场景 沿着山坡移动
这是关于控制角色移动的系列教程的第二部分。这次我们将使用物理引擎来创建更真实的运动,并支持更复杂的环境。
1.刚体
在之前的教程中,我们将球体限制在一个矩形区域内。通过编码限制是有意义的,因为它很简单。但如果我们想让球体在复杂的3D环境中移动,我们就必须支持任意几何图形的交互。我们将使用Unity现有的物理引擎,也就是NVIDIA的PhysX,而不是自己实现它。
有两种方法可以结合物理引擎来控制角色。首先是刚体方法,这是让角色行为像一个规则的物理对象,同时间接地控制它。要么施加力,要么改变速度。其次是运动学方法,这是直接控制,通过查询物理引擎来执行自定义碰撞检测。
1.1 刚体组件
我们将使用第一种方法来控制球体,这意味着我们必须向它添加刚体组件。我们可以使用rigidbody的默认配置
添加这个组件就足以将我们的球体变成一个物理对象,当然他也必须要先有一个SphereCollider组件。从现在开始,对于碰撞我们遵从物理引擎,因此从Update中删除区域代码。
去掉我们自己的约束后,球体又可以自由地越过平面的边缘,目前它会由于重力而垂直下落。
这是因为我们没有覆盖球体的Y位置。
我们不再需要允许区域的配置选项。我们自定义的弹性也不再需要
如果我们仍然想要约束球体保持在平面上,我们可以通过添加其他物体来阻止它的路径来实现。例如,创建四个立方体,缩放和定位它们,使它们围绕平面形成一堵墙。这将防止球体下落,尽管它在与墙壁碰撞时表现得很奇怪。因为我们现在有了3D的几何图形,所以再次启用阴影也是一个好主意,以获得更好的深度感。
当试图移动到一个角落时,球体会变得非常抖动,因为物理引擎和我们自己的代码会争夺球体的位置,我们把它移到容器壁,然后PhysX通过把它推出来解决碰撞问题。如果我们停止强迫它进入容器壁那么物理量就会使球体由于动量保持运动
1.2 控制刚体速度
如果我们想使用物理引擎,那么我们应该让它控制球的位置。直接调整位置将有效地移动,这不是我们想要的。相反,我们必须间接地控制球体,通过施加力或调整它的速度。
我们已经有了对位置的间接控制,因为我们影响速度。我们所要做的就是更改代码,让它影响刚体组件的速度,而不是自己调整位置。我们需要为此访问组件,因此通过在Awake方法中初始化的body字段来跟踪它。
从Update删除位移代码,将我们的速度到赋值到body中。
但物理碰撞等也会影响速度,所以在调整它以匹配所需的速度之前,把它从body中取回.
1.3 无摩擦运动
我们现在调整球的速度,PhysX使用它来移动它.然后碰撞生效,这可以调整速度,然后我们再调整速度,等等。最终的运动看起来和我们之前看到的一样,尽管球体变得更加缓慢,没有达到它的最大速度。那是因为物理使用摩擦力。虽然这更现实,但它让我们更难配置我们的球体,所以让我们消除摩擦力和弹力。这是通过创造一种新的物理材料来实现的。Asset / Create / Physic Material--菜单上写的是Physic--并将所有值设置为零,并将组合模式设置为最小值。
将这个物理材料分配到球体的碰撞器中。
现在它不再受任何摩擦和弹性的影响。
当球体与墙壁碰撞时,球体似乎仍会弹回一点。这是因为PhysX并不阻止碰撞,而是在碰撞发生后检测碰撞,然后移动刚体,使它们不再相交。在快速移动的情况下,这可能需要不止一个物理模拟步骤,所以我们可以看到这种情况发生。
如果运动非常快,球体最终可能会完全穿过墙,或者朝向另一边,这在薄壁中更可能发生。您可以通过更改刚体的碰撞检测模式来防止这种情况,但通常只有在移动非常快时才需要这样做。
同样,球体现在可以滑动而不是滚动,所以我们可以冻结它在所有维度上的旋转,这可以通过刚体组件的Constraints复选框来实现。
1.4 Fixed Update
物理引擎使用固定的时间步长,不受帧率影响。虽然我们已经把球体的控制权交给PhysX,但我们仍然影响它的速度。为了达到最佳的效果,应使速度与固定的时间步长同步调整。我们将Update方法分为两部分.检查输入和设置所需速度的部分可以继续更新,而速度的调整应该转移到新的FixedUpdate方法.,我们必须把所需的速度储存在一个场中。
FixedUpdate方法在每个物理模拟步骤开始时被调用.发生的频率取决于时间步长,默认0.02 秒- 每秒50次.项目设置中通过Time.fixedDeltaTime
设置时间。
根据您的帧速率,FixedUpdate可以在每次更新调用中被调用0次、一次或多次。每一帧都发生一系列FixedUpdate调用,然后调用Update,然后执行该帧。当物理时间步长相对于帧时间过大时,这可以使物理模拟的离散性质明显。
你可以通过减少固定时间步长或者启用刚体的插值模式来解决这个问题。设置它为Interpolate 使它在它的最后和当前位置之间线性插值,因此它将滞后于它的实际位置。另一种选择是Extrapolate,它根据它的速度插值到它所猜测的位置,这实际上只对具有基本恒定速度的对象可接受。
请注意,增加时间步长意味着每次物理更新球体覆盖的距离更大,当使用离散碰撞检测时,这可能导致它穿过墙壁。
2. 跳跃
我们的球体现在可以在三维物理世界中导航,让我们赋予它跳跃的能力。
2.1 通过指令跳跃
我们可以使用Input.GetButtonDown(“Jump”)来检测玩家是否在该帧按下了跳跃按钮,默认的按键是空格键。但就像调整速度一样,我们将实际跳转延迟到下一次调用FixedUpdate。因此,通过desiredJump布尔值字段跟踪是否需要跳转。
但我们可能最终没有在下一帧调用FixedUpdate,在这种情况下desiredJump被设回false,跳转指令将被丢弃。我们可以通过boolean的或操作来防止这种情况发生。这样,一旦启用,它将保持为True,直到我们显式地将其设置为False。
在调整速度之后并在FixedUpdate中执行之前,检查是否需要跳转。如果是这样,重置desiredJump并调用一个新的Jump方法,该方简单地将5添加到速度的Y分量中,模拟突然的向上加速度。
这将使球体向上运动,直到由于重力的作用而下降。
2.2 跳跃高度
让我们把跳跃高度做成可配置方式。我们可以通过直接控制跳跃速度来做到这一点,但这并不直观,因为初始跳跃速度和跳跃高度并没有直接关系。直接控制跳跃高度更方便,我们就按这样做。
跳跃需要克服重力,所以垂直速度也取决于重力。
速度的推导过程
我们的初始速度为j, 速度由于重力作用开始减小直到为0.,之后下始下落。重力加速度g是个把小球向下拉的固定加速度。在这个推导中我们使用正数因为这样我们就不用写一堆负数了。在任意时间t, 垂直速度 v = j - gt, 当v 为0 时达到最高点,即 j - gt =0, 因此 j = gt. 当和t= j/g时到达最高点。
任意时刻的平均速度, 因此,任意时刻的高度
. 这就意味着,最高点为
. 我们可以重写成
![]()
现在我们知道最高点,因此
,
,当g为负时,
![]()
注意,由于物理模拟的离散性,我们很可能会低于期望的高度。最大值会在时间步长之间的某个地方达到。
2.3 在地面上跳跃
目前我们可以在任何时刻跳跃,即使已经在空中,这使得永远保持空中飞行成为可能. 现实中,只有当球体在地面上时,才能开始真正的跳跃。我们不能直接询问Rigidbody来确定是否接触地面,但当它与什么东西相撞时,我们可以得到通知,所以我们会使用它。
如果MovingSphere有一个OnCollisionEnter方法,那么它将在PhysX检测到一个新的碰撞后被调用。只要物体与另一物体保持接触,碰撞就会继续存在。然后调用OnCollisionExit方法(如果存在的话)。在MovingSphere中添加这两个方法,第一个设置一个新的onGround布尔值为true,第二个设置为false。
现在我们只有在地面上才能跳,现在我们假设是在接触某物时跳。如果我们不接触任何东西,那么期望的跳跃应该被忽略
当球体只接触到地面时,这种方法是可行的,但如果它也短暂地接触到一堵墙,那么跳跃将变得不可能。这是因为在我们还在与地面接触的时候,oncollisionexit被调用对像为墙壁。解决方案是不依赖OnCollisionExit,而是添加一个OnCollisionStay方法,只要碰撞仍然存在,就会在每个物理步骤调用该方法。同时在这个方法中乳清粉onGround设置为true.
每个物理步骤都从调用所有FixedUpdate方法开始,然后PhysX执行它的操作,最后调用碰撞方法。所以当FixedUpdate在地上被调用时如果有任何主动碰撞,在最后一步中会被设为true。要保持onGround有效,我们需要做的就是在FixedUpdate结束时将它设为false。
现在只要我们接触到什么东西,我们就能跳。
2.4 不在墙上跳跃
允许在接触到任何东西时跳跃意味着我们也可以在空中跳跃,即使是碰到的是墙壁而不是地面。如果我们想防止这种情况发生,我们必须能够区分地面和其他东西。
将地面定义为水平面是有意义的。我们可以通过检查碰撞点的法向量来检查碰撞点是否满足这个标准。
一个简单的碰撞有一个单一的点,两个形状接触,例如当我们的球体接触地面。通常球体稍微穿过平面,PhysX通过将球体直接推离平面来解决这个问题。推力的方向是接触点的法向量。因为我们用的是球体,这个矢量总是从球面上的接触点指向它的中心。
实际情况可能会更糟,因为可能会有多次碰撞,但现在我们不需要担心这个问题。我们需要意识到的是,一次碰撞可以包含多个接触点。这在平面-球面碰撞中是不可能的,但在使用凹网格碰撞器时是可能的。
我们可以通过在OnCollisionEnter和OnCollisionStay中添加一个碰撞参数来获得碰撞信息。我们将把这个功能交给一个新的EvaluateCollision方法,将数据传递给它。
通过碰撞的contactCount属性可以找到接触点的数量。我们可以使用它来通过GetContact方法循环所有点,给它传递一个索引。然后我们就可以得到点的法线。
法线是球体应该被推的方向,它直接远离碰撞表面。假设它是一个平面,这个向量匹配这个平面的法向量。如果这个平面是水平的,那么它的法向量就是垂直向上的,所以它的Y分量应该是1。如果是这样的话,我们就接触到了地面。但是我们将y值设置为大于0.9.
2.5 空中跳跃
此时,我们只能在地面上跳跃,但游戏通常允许在空中进行两次甚至三次跳跃。让我们支持它,并使它可配置多少空中跳跃是允许的。
我们现在必须跟踪跳转阶段,这样我们就知道是否允许另一个跳转。我们可以通过一个整数字段来做到这一点我们在FixedUpdate开始时,如果我们在地面上,将它设为0。但是,让我们将代码和速度检索一起转移到一个单独的UpdateState方法中,以保持FixedUpdate较短。
从现在开始,我们在每次跳转时增加跳转阶段。我们可以在地上跳跃,也可以在我们还没有达到最大空中跳跃的时候跳跃。
2.6 限制向上的速度
快速连续的空中跳跃可以实现比单次跳跃更高的上升速度。我们将改变这一点,这样我们就不能超过单次跳跃所需要的跳跃速度来达到期望的高度。第一步是在跳跃中分离计算的跳跃速度。
如果我们已经有一个向上的速度,那么在把它加到速度的Y分量之前,从跳跃速度中减去它。这样我们就永远不会超过跳跃速度。
但如果我们的速度已经超过了跳跃速度那么我们就不希望跳跃减慢我们的速度。我们可以通过确保修改后的跳跃速度永远不会变成负值来防止这种情况。这是通过取修改后的跳跃速度和零取最大值来实现的。
2.7 空中移动
在控制它的时候,我们目前并不关心它是在地面还是在空中,但是空中的球体更难控制,这可能是有道理的。控制的数量可以在none和total之间变化。这取决于游戏。所以让我们让它可配置,通过添加一个单独的最大空中加速度,默认设置为1。这大大减少了飞行时的控制,但并没有完全消除它。
我们在计算FixedUpdate的最大速度变化时使用的加速度取决于我们是否在地面上。
3. 斜坡
我们使用物理在一个小平面上移动我们的球体,与墙壁碰撞,并跳跃。这些都可以正常工作,所以现在应该考虑更复杂的环境了。在本教程的其余部分,我们将研究涉及斜坡时的基本运动。
3.1 ProBuilder 制作测试场景
你可以通过旋转平面或立方体来创建一个斜面,但这是创建关卡的一种不方便的方式。所以我们将导入ProBuilder包并使用它来创建一些斜坡。ProGrids软件包对于模型制作也很方便,但是需要Unity 2019.3才支持。ProBuilder使用起来相当简单,但可能需要一些时间来适应。我不会解释如何使用它,只是记住它主要是操作面,边和顶点是次要的。
我从一个立方体开始创建一个斜面,将它拉伸到10×5×3,在X维度上再挤压10个单位,然后将X面折叠到它们的底边。这就产生了一个三角形的双坡道,两边的坡度是10单位长,5单位高。
我把10个这样的东西挨个放在一个平面上,把它们的高度从1个单位变化到10个单位。包括平地,坡度的角度大致为0.0度、5.7度、11.3度、16.7度、21.8度、26.6度、31.0度、35.0度、38.7度、42.0度和45.0度。
在那之后,我又放置了10个斜面,这次从45度开始,每个斜面将顶端向左拉一个单位,直到我得到了一个垂直的墙。我们得到的角大概是48.0度,51.3度,55.0度,59.0度,63.4度,68.2度,73.3度,78.7度,84.3度和90.0度。
我通过将我们的球体变成一个预制件并添加21个实例完成了测试场景,每个斜坡一个实例,从完全水平到完全垂直。
如果你不想自己设计关卡,你可以从本教程的源码库中获取。
3.2 测试斜坡
因为所有的sphere实例都响应用户输入,所以我们可以同时控制它们。这使得在同时与多个倾斜角度相互作用时测试球体的行为成为可能。对于大多数测试,我将进入播放模式,然后不断地按右方向键。
在默认的球体配置下,我们可以看到前五个球体以几乎完全相同的水平速度移动,而不受倾角的影响。第6个勉强通过,而其余的回滚到原地或完全被陡峭的斜坡阻挡。
因为大多数球体最终都在空中,我们把最大空气加速度设为零。这样一来,我们只会考虑加速的问题。
空气加速度1和0之间的差别对飞起来的球体来说没有太大影响因为它们是从斜坡上起飞的。但是第6个球现在不能到达另一边了,其他的球也因为重力提前停止了运动。这是因为它们的坡度太陡,无法保持足够的动量。在第6个球的情况下,它的空气加速度刚好足以把它推过顶部。
3.3 地面角度
目前,我们使用0.9作为阈值来将某些东西分类为ground或not,但这是任意的。我们可以使用0-1范围内的任何阈值。尝试这两个极端会产生非常不同的结果。
让我们通过控制最大地面角度来设置阈值,因为这比斜率法向量的Y分量更直观。我们使用25度作为默认值。
当一个平面水平时,它法向量的Y分量是1。对于完全垂直的墙,Y分量是零。Y分量在两个极端之间的变化取决于斜率角,它是这个角的余弦。我们处理的是单位圆,Y是纵轴横轴在XZ平面上。另一种说法是我们看的是上向量和法向量的点积。
配置的角度定义了仍然算作地面的最小结果。让我们将阈值存储在一个字段中,在OnValidate方法中 ,通过Mathf.cos计算它。因为。这样当我们在播放模式下通过检查器改变角度时,它会保持与角度同步。还可以在Awake中调用它,以便在开始时就计算。
我们指定的角度是度数,但是是Mathf.Cos希望用弧度表示。我们可以通过乘以math。deg2rad来转换它。
现在我们可以调整最大地面角度,看看这是如何影响球体的运动。从现在开始我将把角度设置为40度。
3.4 斜坡上跳跃
我们的球体总是竖直向上跳,不管它现在所处的地面角度如何。
另一种方法是沿着法向量的方向从地面跳起。每个斜率测试车道会产生不同的跳跃,我们来做一下。
我们需要在一个变量中保持当前接触的正常状态,并在EvaluateCollision中每当遇到地面接触时将其存储起来。
但我们最终可能不会碰到地面。在这种情况下,我们会用向上的向量来表示接触法向量,所以空气跳仍然是竖直向上的。如果需要,在UpdateState中设置它。
现在我们必须将跳跃接触法线按跳跃速度缩放到跳跃时的速度,而不是总是只增加Y分量。这意味着跳跃高度是指我们在平地或空中跳跃的高度。在斜坡上跳跃不会达到那么高,但会影响水平速度。
但这意味着对正垂直速度的检查也不再正确。它必须成为检查速度对准接触法向。我们可以通过投射接触法面上的速度,通过向量Vector3.Dot来计算。
现在跳跃与斜坡对齐了,测试场景中的每个球体都得到了一个独特的跳跃轨迹。在陡坡上的球体不再直接跳跃到它们的斜坡上,但是当跳跃将它们推向与运动方向相反的方向时,它们的速度确实减慢了。你可以更清楚地看到,在所有的斜坡上,尝试它与急剧减少的最大速度。
3.5 斜坡上移动
到目前为止,我们总是在水平XZ平面上定义期望的速度,不管地面角度是多少。如果一个球体向上倾斜是因为PhysX将它向上推来解决碰撞因为我们给了它一个水平速度指向斜率。这在上斜坡的时候可以很好地工作,但是在下坡的时候,球体会离开地面,当它们的加速度足够高时,最终会下落。其结果是难以控制的弹性运动。当你在斜坡上倒车时,你可以清楚地看到这一点,尤其是在滑落时。
我们可以通过调整与地面的速度来避免这种情况。它的工作原理类似于我们将速度投影到法线上以得到跳跃速度,只是现在我们必须将速度投影到平面上以得到新的速度。我们通过对向量和法向量做点积,就像之前一样,然后减去法向量乘以原来的速度向量。我们来创建一个ProjectOnContactPlane方法,它可以用任意的向量参数来调用。
让我们创建一个新的AdjustVelocity方法来调整速度。首先通过在接触平面上投影右矢量和正矢量来确定投影的X轴和Z轴。
这就得到了与地面对齐的向量,但它们只有在地面完全平坦时才是单位长度。通常我们需要对向量进行标准化来得到正确的方向。
现在我们可以把当前的速度投影到两个矢量上,得到相对的X和Z速度。
我们可以用它们来计算新的X和Z速度,就像以前一样,但是现在是相对于地面的速度。
最后,通过在相对轴上添加新旧速度之间的差异来调整速度。
在FixedUpdate中调用这个新方法来代替旧的速度调整代码。
随着我们新的速度调整方法,球体不再失去与地面接触时,突然逆转方向,而向上坡。除此之外,由于期望的速度调整其方向以匹配斜率,绝对期望的水平速度现在每车道都是不同的。
注意,如果斜率不与X轴或Z轴对齐,那么相对投影轴之间的夹角将不会是90度。这不是很明显,除非斜坡很陡。你仍然可以朝各个方向移动,但是精确地控制某些方向比其他方向要困难。这在某种程度上模仿了试图穿过但没有对准陡坡的尴尬
3.6 多个地面法线
当只有一个地面接触点时,使用接触法线来调整所需的速度和跳跃方向很好,但当多个地面接触点同时存在时,行为可能变得奇怪和不可预测。为了说明这一点,我创建了另一个测试场景,在地面上有一些凹陷,允许同时有四个接触点。
当弹跳时,球体会往哪个方向运动?在我的例子中,有四个接触点倾向于选择一个方向,但最终可能会选择四个不同的方向。同样地,有两个接触点的球体在两个方向之间任意选取。有三个接触点的球体始终以相同的方式跳跃,以匹配附近只接触一个斜坡的球体。
这一行为的显现是因为我们在EvaluateCollision
中设置了正常值,每当我们找到地面接触时。如果我们找到多个选择了最后一个。由于移动顺序是任意的,或者由于PhysX计算碰撞的顺序总是相同的。
哪个方向最好?没有一个。最合理的做法是将它们全部组合成一个代表平均地平面的法线。要做到这一点,我们需要累积法向量。这需要我们在FixedUpdate结束时将接触时的法线设置为0。让我们在新的ClearState方法中加入该代码以及onGround重置。
现在在EvaluateCollision 计算法线,而不是覆盖前一个。
最后,在UpdateState中对接触法线进行归一化,使其成为正确的法向量。
3.7 计算地面接触数量
虽然不是必须的,但我们可以计算有多少地面接触点,而不仅仅是追踪是否有至少一个。我们通过用整数替换布尔字段来实现。然后引入一个布尔值OnGround只读属性—注意大写—检查计数是否大于0,替换OnGround字段。
ClearState现在必须将计数设置为零。
并且UpdateState必须依赖于属性而不是字段。除此之外,我们还可以对它进行一点优化,如果它是聚合体,我们只需将法线标准化,否则它已经是单位长度了。
在适当的时候增加Evaluate中的计数。
最后,在AdjustVelocity和Jump方法中用OnGround替换onGround。
除了UpdateState中的优化之外,地面接触计数也可以用于调试。例如,您可以记录计数或基于计数调整球体的颜色,以更好地了解其状态。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。