[Unity Tutorial 您所在的位置:网站首页 一个小球跳来跳去的游戏 [Unity Tutorial

[Unity Tutorial

2024-06-06 12:24| 来源: 网络整理| 查看: 265

个人渣翻 有错欢迎指出

来源:https://catlikecoding.com/unity/tutorials/movement/physics/

原作者:Jasper Flick

支持原作者:https://www.patreon.com/catlikecoding/posts

catlike-coding

推动小球

·         控制球体刚体的速度

·         通过跳跃来进行垂直移动

·         检测地板以及地板的角度

·         使用ProBuilder创建场景

·         沿着斜坡移动

这是控制角色移动系列教程的第二部分。这次我们会使用物理引擎来创建更加真实的运动同时支持更加复杂的环境。

这个教程使用Unity 2019.2.11f1来制作并使用了ProBuilder包。

在多个不同赛道上的球体

刚体 Rigidbody

在之前的教程里,我们限制小球在一个矩形区域移动。我们用程序这么写是有意义的,因为这很简单。不过如果我们想要让小球在一个更加复杂的3D环境中移动那么我们必须编写更多的代码来支持与更加复杂的几何体互动。但我们需用实现自己的物理引擎,而是用Unity自带的NVDIA PhysX引擎。

有两种方式来用物理引擎控制角色。一种是刚体方式,即让角色表现得像常规物理对象一样,同时通过施加力或改变其速度来间接控制它。第二种是运动学方法,即直接控制,同时仅查询物理引擎来执行自定义碰撞检测。

刚体组件 Rigidbody Component

我们会使用第一种方式来控制小球,也就是说我们得加一个Rigidbody组件。这里用默认配置。

刚体组件

加了刚体组件如果还有SphereCollider就能让小球变成物理对象了。这里我们遵循物理引擎的碰撞效果,删掉Update里的限制移动区域相关的代码。

删掉了哪些代码后,小球现在可以离开地板边界了,然后又因为重力掉了下去。这是因为我们没有修改小球的位置的Y分类。

掉落

我们也不再需要相关的区域限制字段了,这里的自定义弹跳设置相关的也全都删了。

如果我们还想要限制小球只能在平面上移动的话我们可以在周围围上一圈障碍物。比如,创建四个方块,调整大小并放到对应位置来当作墙壁。这能法昂之小球掉出去,虽然小球和墙碰撞时的行为有些奇怪。因为我们这次有3D几何体了,所以我们最好启用阴影投射以获得更好的深度感。

诡异的物理效果

当我们把球往一个角落里移动的时候会看到小球会不断抖动,因为物理引擎和我们的代码在相互争夺修改小球位置的控制权。我们把小球移动到墙上,然后PhysX通过把球往回推来解决碰撞。如果我们停止把球推到墙上,那么因为动量,PhysX让小球保持移动。

控制刚体速度 Controlling Rigidbody Velocity

如果我们要使用物理引擎,那么我们就应该让物理引擎来控制小球位置。直接修改位置会造成瞬移效果,这不是我们想要的。相反,我们得通过施力或者调整速度来间接控制小球的位置。

我们已经对位置进行了间接控制,因为之前的代码我们都只控制速度。我们需要做的就只是修改Rigidbody组件上的速度属性,这样我们就不需要自己去调整小球的位置了。为此,创建一个body字段在Awake方法里访问这个组件。

删掉Update里面Displacement相关的代码,同时把速度传到刚体组件里的速度属性上去。

不过物理碰撞也会影响速度,所以在调整速度到期望速度之前我们先获取刚体的速度。

控制刚体速度

无摩擦的移动 Frictionless Movement

我们现在控制了小球的速度,物理引擎会根据这个速度移动小球。当小球发生碰撞速度变化之后,我们可以再一次调整速度。由此产生的运动看起来就像我们之前的运动一样,尽管球体更加缓慢并且没有达到其最大速度。这是因为PhysX使用了摩擦力。虽然这比较真实,不过会让我们比较难控制小球,所以这里我们删掉摩擦力以及弹力。通过Asset / Create / Physic Material创建一个物理材质,把所有的值都设置为0,然后把Combine模式设置为Minimum。

物理材质

把物理材质拖到小球的碰撞体上。

设置物理材质

现在我们的移动不受摩擦力和弹力影响了。

无摩擦力的移动

不过现在小球碰到墙上的时候好像还有一点点反弹的效果。发生这种情况是因为 PhysX不会阻止碰撞的发生,而是在碰撞发生后检测碰撞,然后移动刚体,使它们不再相交。在快速移动的情况下,这可能需要多个物理模拟步长来执行这个操作,因此我们可以到这类现象的发生。

如果移动的实在是太快了,那么小球可能会完完整整的穿过墙体,这种情况在墙体太薄的时候更容易发生。你可以修改刚体的Collision Detection模式来避免这种情况,不过这设定只在移动特别快的时候使用。

同时,我们的小球现在在滑动而不是滚动,所以这里我们锁住小球的所有旋度。在刚体组件下面的Constraints一栏把所有旋度限制都勾上。

锁住旋度

固定更新 Fixed Update

物理引擎使用固定的时间步长来进行物理模拟而不是帧率。尽管我们已经将球体的控制权交给了PhysX,但我们的操作仍然会影响小球的速度。为了得到最好的运动效果,我们应该以固定的时间步长同步调整速度。这里我们把Update方法里的代码分为两部分。第一部分用于检测输入以及设置期望速度,这部分我们留在原地不动。第二部分用于调整速度,这部分丢到FixedUpdate方法里。为此,我们必须要用字段存储期望速度。

FixedUpdate会方法在每个物理模拟步长开始时被调用。执行频率取决于时间步长,时间步长通常为0.02即每秒50次,不过你可以在Time项目设置里或者Time.fixedDeltaTime来修改步长。

根据你的帧率情况,每次调用Update的时候FixedUpdate可能被调用零次,一次或多次。每一帧都会调用一系列FixedUpdate,然后再调用Update,然后渲染该帧。当物理时间步长相对于帧时间太大时,这会使物理模拟的离散现象变得明显。

0.2物理时间步长

要解决这个问题,要么降低固定时间步长或者启用Rigidbody的插值模式。把插值模式设置为Interpolate那么系统会在物体上一个和当前的位置进行插值 ,所以这会导致小球的位置滞后于实际位置。另一个选项就是Extrapolate,这个设置会根据小球当前速度推测下一个位置,这个设置最好是用于具有恒定速度的物体。

带插值设置的0.2物理时间步长

注意,越大的时间步长意味着小球每个物理帧移动的距离越大,当使用离散碰撞检测的时候,小球可能会穿过墙体。

跳跃 Jumping

我们的小球现在可以在3D物理时间里移动了,我们来让它跳起来吧。

按命令跳跃 Jumping on Command

我们可以根据Input.GetButtonDown("Jump")来建测玩家是否在这一帧按下了跳跃键,默认是空格键。我们在Update里面写下这段代码,不过就和调整速度一样,我们只会在下一个FixedUpdate被调用的时候才真正执行跳跃操作。所以我们声明一个desiredJump的bool字段来跟踪我们是否想让小球跳起来。

不过有可能到下一帧开始的时候FixedUpdate还没被调用,这样的话我们的desiredJump就会被设置回false了,然后这个操作就被系统遗漏掉了。为此,这里我们用一个或操作来给它赋值,这样在desiredJump被设置为false之前就会一直保持true。

在FixedUpdate里调整完球体速度之后,检测是否要跳跃。如果是,则把desiredJump设置为false,然后调用Jump函数,这个函数一开始先直接在速度的y分量上加5,模拟一个瞬时加速度。

跳跃会让小球向上移动,然后因为重力掉下来。

跳跃

跳跃高度 Jump Height

这里我们来配置可跳跃的高度。为此,我们可以直接控制跳跃速度。不过要处理跳跃初速度和跳跃高度之间的关系并不容易。所以我们控制跳跃高度会更加方便。

跳跃高度

跳跃需要克服重力,所以垂直速度需要依赖重力。特别的,,这里g代表重力,h代表期望高度。这里有个符号是因为这里重力是个负数。我们可以通过Physics.gravity.y获取重力,我们也可以在Physics项目设置里调整重力。我们现在用的是默认重力也就是9.81的垂直向下力,和现实一样。

这个初速度是怎么得到的?

 注意这里因为物理模拟的离散性质,所以我们的跳跃高度总是比预期底了那么一点。小球会在某个时间步长之间达到最大高度。

在地面上时跳跃 Jumping While on the Ground

现在我们能在空中跳跃,这不是我们想要的情况。我们只希望小球在地面上时才能执行跳跃操作。我们没法直接访问Rigidbody当前物体是否在地板上,不过我们可以使用碰撞消息来处理这事。

如果MovingSphere有一个OnCollisionEnter函数,那么Physx在检测到碰撞后就会调用这个函数。只要物体和另一个物体保持接触,这个碰撞就会持续存在。在这之后就会调用OnCollisionExit,如果有这个函数的话。我们把这辆个函数都放到MovingSphere里面,里面分别设置新的bool字段onGround为true以及false。

现在我们实现了仅在地板上时才能跳跃的功能了。如果我们没和任何物体接触,那么我们的跳跃指令也就会被忽略。

当小球仅接触地平面时,此方法可行,但如果小球也短暂接触墙壁,那么小球就无法跳越了。发生这种情况是因为当我们仍然与地面接触时,和墙壁碰撞后也调用了小球的OnCollisionExit。 解决方法就是我们不要用OnCollisionExit转而增加OnCollisionStay方法,这个函数会在保持碰撞下的每个物理帧时调用。在这个方法里把onGround设置为true。

每个物理帧都会先从调用所有FixedUpdate方法开始,之后PhysX会执行相关物理计算,最后才会调用各种碰撞方法。所以,当调用FixedUpdate时,如果有什么碰撞发生,onGround就在上一个步长里设置为true了。我们要做的就是在FixedUpdate后面把onGround设置为false。

现在只要我们接触了某个物体,我们的小球就可以跳起来了。

不要蹬墙跳 No Wall Jumping

当小球碰到任何东西时就可以跳跃,这个设定也可以让我们小球在空中碰到墙时可以再次跳跃。要想防止这种情况发生,我们就需要区分地板和墙壁以及其它的什么物体了。

把地面定义为一个水平平面很合理。我们可以检测我们碰撞的物体的接触点法线是否满足某个条件。

简单的碰撞有一个两个图形的接触点,比如,这里小球碰到地面上。通常来说,小球会穿过地板一点点,PhysX会把小球直接往回推来解决这个问题。而这个推的方向就是接触点的法向量。因为我们现在控制的时小球,这个向量总会从接触点指向小球圆心。

接触点和法线

然而现实情况会更加复杂,因为一个模拟步长里可能有不知一个碰撞和穿透现象,不过现在我们先不用太担心。我们要知道的是,一个碰撞可以产生多个碰撞点。不过球体和平面的碰撞不会发生这种情况,不过如果小球陷入到一个凹体里面就另说。

我们可以给OnCollisionEnter和OnCollisionStay增加一个Collision参数来访问碰撞信息。这里我们不再直接把onGround设置为true,我们把这部分工作交到新方法EvaluateCollision上,同时把碰撞数据传过去。

我们可以通过Collision的contactCount属性来访问接触点数量。我们用一个循环和GetContact方法来访问所有的顶点,只需要传入索引即可。然后可以访问每一个点的normal属性。

法线是球体应该被推动的方向,它直接远离碰撞表面。假设这里是一个平面,则该向量与该平面的法线相匹配。如果平面是水平的,那么它的法线会笔直向上,因此它的Y分量应该恰好为1。如果是这种情况,那么我们正在接触地面。但我们要宽容一些,接受0.9或更大的Y分量。

空中跳跃 Air Jumps

现在我们只能在地面时起跳瞒不过有些游戏经常可以让在角色在空中二段跳甚至三段跳。这里我们也加入这个特性,增加最大跳跃次数字段。

最大空中跳跃

我们现在要记录跳跃阶段这样我们才能知道是否可以进行下一次跳跃。这里我们增加一个整数字段,在FixedUpdate里,每当检测到我们在地面上时,把这个字段设置为0。这里我们把获取速度相关的代码和刚才所说的判定放到另一个UpdateState函数里以让FixedUpdate不要太长。

从现在开始,只要我们进行跳跃则增加一次跳跃阶段。同时在跳跃判定条件里多增加一个或条件即当前跳跃阶段小于最大空中跳跃次数。

限制向上的速度 Limiting Upward Velocity

快速连续跳跃可能会让小球突破单次跳跃的最大速度。这里我们改一下代码以让小球不会超过到达预期高度的预期初速度。第一步就是在Jump里单独计算所需要的速度。

如果我们已经有了向上的速度,那么在把计算得到的跳跃速度加到速度y分量上前,它需要减上当前向上的速度。这样我们就不会超过预期的跳跃速度了。

如果我们这里在深入一点,不想让跳跃减缓了我们的速度。我们可以通过一个Max函数保证计算的跳跃速度大于0即可。

空中移动 Air Movement

我们现在不关心控制的小球是在地面上或者空中,不过要控制在空中的小球会更难一些。这里我们增加一个最大空中加速度的字段,默认为1。这个字段可以让我们的小球在空中的时候难以控制但并不完全剥夺控制权。

两个最大加速度

在FixedUpdate里使用哪一个加速度来计算MaxSpeedChange取决于我们是否在地面上。

斜坡 Slopes

我们现在利用物理让小球在一个平面上移动,和墙发生碰撞,以及跳来跳去什么的。现在一切都正常,不过我们现在需要考虑更加复杂的环境了。在接下来的教程里,我们探讨在存在斜坡的环境下的基本移动。

ProBuilder测试场景 ProBuilder Test Scene

你可以通过旋转平面或者方块来创建斜坡,不过对创造关卡来说太麻烦了。所以这里我们引入ProBuilder包,然后用它来创建一些斜坡。这个包很容易使用,不过我这里不多加介绍。

这里我简单的创建了一个10×5×3的斜坡,并用它拼凑了一个坡道。两个斜面长10个单位,高5个单位。

由两个10×5×3斜坡组成的坡道

我在平面上放了10个这种坡道,高度从0到10个单位递增。角度包括0.0°, 5.7°,11.3°,16.7°,21.8°,26.6°,31.0°,35.0°,38.7°,42.0°,以及 45.0°。

之后我又放了10个,角度分别为48.0°,51.3°,55.0°,59.0°,63.4°,68.2°,73.3°,78.7°,84.3°, 以及 90.0°。不过这次我把顶部的交界处往左边拉了对应的数量。

做完测试场景后,我放了21个相同的小球在对应斜坡的入口处。

斜坡测试场景

如果你懒不想自己做这场景,你可以直接在我的仓库里拉下来就可以了。

斜坡测试 Slope Testing

因为所有小球都用的一个控制脚本,所以我们可以让所有小球同时往坡上走来看看小球和不同坡度交互的反应。这大部分测试我都会进入播放模式然后一直按着右键不动。

斜坡测试

在默认配置情况下,我们可以看到前5个小球几乎以相同的水平速度来移动,不管坡度的大小。第六个小球差点没能翻过这个坡,而其他小球都因为坡度太大而滚了回去。

因为大多数球体在空中会受到我们控制的加速度影响,所以我们将最大空中加速度设置为0。这样我们只考虑接地时的加速度。

空中加速度为1和0

这两个结果对前五个小球来说意义不大,因为它们都会从坡道上飞出去。不过第六个小球现在却翻不过去了,同时其它小球也因为重力早早的停止被拦了下来。发生这种情况是因为斜坡太陡峭,小球难以保持足够的动量。这里第六个小球的空中加速度正好可以让它飞过这个坡。

地面角度 Ground Angle

目前我们用0.9作为检测地面的阈值,不过这种判断太武断了。我们可以实时在0到1之间调整阈值,这里我们试试0和1分别有什么结果

地面检测阈值1和0

这里们实现可配置化,增加一个最大地面角度字段,因为角度比地面法线的y分量更直观,所以这里使用角度,默认设为25°。

最大地面角度

当一个表面是水平的,那么其法线Y分量则为1。对于一个完全垂直的墙体,其法线Y分量则为0。Y 分量是在角度的余弦极值之间不断发生变化。这里我们在处理单位圆,其中Y是垂直轴,其水平轴位于 XZ 平面中的某个位置。另一种说法是,我们正在寻找向上向量和表面法线的点积值。

斜坡角度的余弦

这个角度定义了可以被视为地面的最小点积结果。我们把这个阈值存到一个字段了然后在OnValidate方法里使用Mathf.Cos来计算。这样当我们在播放模式用检视窗口修改角度的时候,它会和角度保持同步计算结果。同时我们在构建版本里不要忘记在Awake上补上这个方法。

我们用度数来定义角度,不过Mathf.Cos希望我们使用的是弧度,这里把角度和Mathf.Deg2Rad相乘来把它转为弧度。

现在我们可以调整最大地面角度来看看它怎么影响小球的运动。现在我们设置角度为40°。

最大地面角度25以及40

在斜坡上跳跃 Jumping While on a Slope

我们小球现在不管在多少度的地面上它都会总是往上直着跳跃。

总是往上方跳跃

另一种方式就是朝着所在地面的法线方向进行跳跃。这样每个斜坡的跳跃都会不同,我们来试试。

我们需要创建一个字段记录当前地面的法线,在EvaluateCollision里,当存在有效碰撞点的时候,记录它的法线。

不过我们也存在没有接触地面的情况,这样的话我们就直接使用向上的法线即可,这样我们在空中的时候也能进行跳跃。在UpdateState进行相关设置。

现在我们执行跳跃时需要把接触法线和跳跃速度相乘然后加到当前速度里面,而不是单纯的只加到当前速度的Y分量上。这意味着期望跳跃高度仅仅表示小球在水平面垂直跳跃的高度。在一个斜坡上跳跃并不会达到预期高度,不过这个跳跃会影响水平速度。

同时,在调整跳跃速度的时候检查垂直速度是否大于0也不再正确。这里需要检查相对接触法线上的速度。我们可以通过将当前速度投影到接触法线上同时用Vector3.Dot来得到该速度。

往接触法线方向跳跃

现在我们的跳跃方向已经和斜坡法线对齐了,测试场景里每一个小球的跳跃轨迹都不同。那些在更抖的斜面上跳跃的小球现在不会往坡上跳了而是因为跳跃使它们往反方向移动从而减慢了速度。通过尝试大幅降低最大速度,你可以在所有斜坡上更清楚地看到这一点。

往回跳;最大速度为1

沿着斜坡移动 Moving Along Slopes

到目前为止,不管地面的角度大小,我们总是将期望速度定义在XZ水平面上。目前如果小球能够顺利爬上一段坡,那是因为PhysX正在把小球往坡上推来计算碰撞所发生的结果,之所以会这样是因为我们总是给了小球一个水平的移动速度。上坡还好,如果是下坡且加速度足够大那么小球可能会飞离地面。我们很难控制这种弹跳移动。当角色上坡后再反方向移动时你能清除的看到这一点,特别是加速度足够大的时候。

和地面失去接触:最大加速度100

我们可以把我们的预期速度与地面对齐来避免这种情况,这有点像把向量投影到法线上来获取跳跃速度,不过这次我们需要把速度投影到法线的平面上来获取新的速度。我们和之前一样把速度和法线做一个点积,然后和法线相乘最后将速度和这个计算后的法线投影速度相减来获得平面上的速度。我们创建一个ProjectOnContactPlane方法,传入向量参数。

把向量投影到平面上

创建一个新的方法AdjustVelocity,这个方法主要用来调整速度。这里首先将X和Z轴投影到接触点平面上。

这样我们会得到和地面对齐的向量,不过这个向量并非是单位向量,除非地面特别平。一般来说我们需要对这两个向量单位话以计算合适的方向。

现在我们可以把当前速度投影到这辆个新的轴上来获得相对X轴和Z轴的移动速度。

我们可以和之前一样用它们来获得新的X和Z轴速度,不过现在是相对于接触地面。

最后将新旧速度之间的差与对应轴相乘再把结果加到速度上即可。

我们在FixedUpdate调用这个新方法,把之前调整速度的代码删掉。

与地面对齐:最大加速度100

调整完速度计算后,当我们的小球在坡上突然反方向移动时就再也不会脱离地面了。此外,因为期望速度把当前方向和坡度方向对齐,现在每个坡面的水平速度都不同了。

绝对和相对期望速度

注意,如果斜率未与X轴或Z轴对齐,则相对投影轴之间的角度将不是 90°。 除非斜坡非常陡峭,否则这并不明显。你仍然可以向各个方向移动,但在某些方向上精确驾驶会比在其他方向上更难。这在某种程度上模仿了尝试走过陡坡但未与陡坡对齐的尴尬。

多地面法线 Multiple Ground Normals

当只有一个地面接触点时,使用接触法线来调整预期速度和跳跃方向没有什么问题,不过当同时接触到多个不同平面的点时,事情就怪了起来了。为了解释这种情况,我创建了另一个测试场景,里面有一些坑,且一次可以有4个不同的接触点。

跳跃测试场景

当跳跃的时候,小球会朝着哪个方向?在我这情况下,有四个接触点的小球会倾向使用第一个点作为跳跃方向,不过最后也有可能用了四个不同的方向。同样的,有两个接触点的小球会随机的在两个方向之间做选择。具有三个接触点的球体始终以相同的方式跳跃,以匹配附近仅接触单个斜坡的球体。

任意的跳跃方向

这主要是因为当和地面发生接触时,我们在EvaluateCollision里面设置了对应的表面法线。所以最后一个接触点会覆盖之前的所有接触点。这个顺序要么很随意要么总是一样

哪个方向最好?答案都不是。最合理的做法就是把所有法线加起来求均值,用它来代表地板平面的法线。为此,我们我需要累加法线向量。这要求我们在FixedUpdate最后把这个接触点向量清零。这里我们统一把重设onGround以及重设法线向量的代码放到ClearState方法里。

在EvaluateCollision里面,累加法线向量而不是之前那样直接赋值。

最后,在UpdateState方法里面,如果处于地面时,则标准化接触法线向量。

始终一致的跳跃方向

计算地面接触点 Counting Ground Contacts

虽然没有必要,但我们可以计算我们和地面的接触点而不只是记录是否有一个接触点。这里我们用一个整型来代替onGround字段。然后引入一个OnGround的只读属性,它返回当前接触点是否大于0。

ClearState里面现在要把这个接触点数量重设为0.

同时UpdateState里面要用新的OnGround属性代替老的onGround字段。此外这里做一些小优化,如果接触点数量大于1,我们才需要对接触点法线进行标准化。

然后在Evaluate方法里面,在合适的位置上记录接触点个数。

最后,在AdjustVelocity和Jump方法里用新的OnGround属性替换onGround字段。

除了UpdateState中的优化之外,接触点个数也可用于调试。例如,你可以记录数字或根据其来调整球体的颜色,以更好地了解小球的移动状态。

根据接触点个数修改小球颜色

下一个教程是 表面接触 。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有