本文主要总结 UE4 制作攀爬系统的主要过程,参考教程
运动系统的额外知识会另开专题
准备工作
从第三人称模板新建项目
删掉多余的部分
删除多余输入映射
在项目设置中删除 VR 输入和移动端输入映射
删除多余蓝图
在角色蓝图中删除 VR 输入和移动端输入
删除并调整地图
使用第三人称提供的默认地图,删掉多余的墙壁和平台,调大地图,创建几个几何体用来攀爬测试。
导入资源
下载动画素材资源并导入项目,注意需要绑定骨骼。
修改默认设置
设置飞行模式
打开角色蓝图,选中角色移动组件,在右侧的细节面板输入 "fly" 找到飞行相关的设置。
攀爬实际上是利用角色移动模式的飞行模式实现的,设置 “制动降速飞行” 为 2048,勾选 “可飞行” 选项。
设置根运动
动画资源中有一些使用了根运动,在内容面板选中动画资源中后缀为 “RM” 的几个动画序列,右键选择 “资产操作”->“通过属性矩阵进行批量编辑” 打开属性矩阵。
在右侧的面板中展开 “RootMotion” 选项,勾选 “启用根运动”。
正式开始
设置攀爬的动作
创建动画混合空间
在动画目录下创建一个动画混合空间并打开
再左侧的 “资产详情” 面板设置横纵轴。水平坐标命名为 “deriction”,最小最大轴值分别设为 - 180 和 180,网格分区设为 8。垂直坐标命名为”climb speed“,最小最大轴值分别设为 0 和 200,网格分区设为 4。
水平坐标表示攀爬的移动方向,通过 - 180 到 180 的浮点数来表示,分区为 8 表示 8 个方向。垂直坐标表示攀爬的速度
在打开右下角的资产浏览器,将对应的动画序列拖入坐标轴并保存。
添加状态切换
状态切换在动画蓝图中写。打开动画蓝图,进入默认的状态机里的”Idle/Run“状态,这个预先写好的状态是用于从静止到奔跑的转换,使用的是混合空间 1D
没有加 1D 的混合空间是二维的,接收两个坐标轴上的坐标输入,1D 只有一个坐标轴
为了简单起见,我们就把攀爬的状态写在这里。
添加一个混合空间节点,注意是自己之前建的那个攀爬的混合空间。其接受两个浮点型输入(即上文的横纵坐标)并输出一个状态
由于每个状态只有一个状态可以输出,而混合空间(无论是 1D 还是 2D)均会返回状态,故需要使用混合列表判断输出的是哪个状态。
添加一个根据布尔值的混合列表,其接受两个状态输入分别为” 真状态 “和 “假状态”,当布尔值为真时,输出 “真状态”,当布尔值为假时,输出 “假状态”。
新建一个布尔值类型的变量,命名为”is Climbing“表示玩家是否处于攀爬状态。将 "is Climbing" 连至混合列表的布尔值判断,攀爬混合空间的状态连至 “真状态”,奔跑混合空间的状态连至 “假状态”,混合列表的输出连至最后的输出状态。
新建一个浮点型变量,命名为 “Direction“,表示攀爬时移动的方向。将对应的变量连至攀爬混合空间的输入。
此时攀爬的基本运动的动画部分就解决了,后续会通过蓝图接口传递 “is Climbing"和”Direction" 两个变量。
创建蓝图接口
创建一个蓝图接口,命名为 BPI_Anim,这个蓝图接口就是沟通角色蓝图和动画蓝图的工具,在蓝图接口中新建两个函数,一个函数命名为 INT_Direction,添加一个浮点型输入参数 Direction,另一个命名为 INT_ChangeClimbingPoseture,添加一个布尔型输入参数 is_Climbing,对应上文。
实现攀爬的逻辑
坐标系
在实现攀爬之前,我们需要先确定一件事,就是攀爬状态下坐标系的变化。正常情况下,角色站立在地面上,受到重力的作用,其反方向就是角色的上方,角色面朝的方向就是角色的前方,而 UE4 是左手坐标系,将四指从上方握到前方,大拇指指向的方向就是角色的右方。而在攀爬状态下,角色应该是贴合在攀爬面上的,这也就导致了角色攀爬时的坐标系和正常情况下的坐标系并不相同。
以上图为例,这面墙是倾斜的,那么角色攀爬在这个墙上时,依然需要贴着墙壁,那么此时角色的坐标系其实是跟墙面的法线有关的。墙面的法线方向的反方向是角色的前方,平行于墙面斜向上的方向是角色的上方,同样根据左手系可以确定右方。
在正常情况下,角色接收向前的输入开始前进后退,接收向右的输入开始左右移动,那么在攀爬时,就需要根据攀爬时的坐标系重新确定,接收前的输入开始上下移动,接收向右的输入开始左右移动。
这一点需要通过改变蓝图中预置的 MovementInput 来实现。
如上图所示,新建两个变量 ClimbingUpDirection 和 ClimbingRightDirection。在输入后加一个判断,如果处于攀爬状态,则输入的方向由这两个变量来控制。
确定攀爬方向
综上所述,在攀爬时需要首先确定的就是攀爬的方向。在角色蓝图中新建一个函数,命名为 FindClimbingRotation,同时新建两个向量变量,分别叫 ObstacleLocation 和 ObstacleEndLocation。
在函数中,使用 ObstacleEndLocation 减去 ObstacleLocation,得到一个新的向量,将其标准化,显而易见这得到的就是向上的方向,再新建一个 ObstacleNormal,在函数中将其反向,再标准化,这得到的就是向前的方向,通过向前的方向和向上的方向做叉积,再标准化,就会得到向右的方向。将向上的方向和向右的方向分别设置变量 ClimbingUpDirection 和 ClimbingRightDirection 存储起来,并且将三个单位化(标准化)的向量通过 "Make Rotation from Axes“结点创建 Rotation 并存储为 Climbing Rotation。
上面用到的 ObstacleLocation,ObstacleEndLocation,ObstacleNormal 是后面射线检测时得到并存储的,这里只需要当做已知值使用即可。
Tick 处理
由于攀爬和正常行走是同一级别的运动,所以需要通过 tick 来判断当前的运动状态,如果是在攀爬状态,则需确定攀爬方向便于移动,同时添加一个 do once 结点,开始时关闭,当处于攀爬状态且确定完攀爬方向时将 do once 重置,退出攀爬状态后执行 do once 退出攀爬状态。
另一方面,如何确定攀爬状态?这就是需要在 tick 中处理的另一个部分了,这个部分也是攀爬系统的主要处理部分。在 tick 中我们使用 sequence 结点将该处理置于判断运动状态之后。
Climbing Tick
这里实现上文中的 tick 中需要用到的攀爬系统的主要处理过程。
新建一个自定义事件,命名为 Climbing Tick,并在 tick 处理后的 sequence 的 Then 1 中调用,如上图所示。
游戏中肯定存在一些墙体和障碍物是可以攀爬,另一些则不可攀爬。在进行攀爬处理之前,需要将这些不可攀爬的 Actor 放到一起存储起来,新建一个 Actor 的数组,命名为 NotClimbableActors。为简单起见,这里调用引擎提供的方法 Get All Actors with Tag,将 Tag 设为 Not Climbable,并将返回值设置为 NotClimbableActors。
官方文档中建议不要在 Tick 中使用该方法,这里只是简单起见。实际中请使用通道检测等其他方法。
之后判断一下是否在攀爬状态,这里判断的目的是区分出正常移动状态和攀爬时的状态。在正常移动情况下,需要确定是否要进入攀爬状态,在攀爬状态情况下,由于地形可能有起伏,需要时刻确定当前的旋转和方向。这里只需要找到上文 FindClimbingRotation 中需要的 ObstacleLocation,ObstacleEndLocation,ObstacleNormal 三个变量存储起来即可。另外攀爬状态时可能会提前掉落或是登顶,这些都需要考虑。
先来看简单的正常运动状态的逻辑
正常运动进入攀爬状态
首先进行速度的限制,如果速度过小则不主动进入攀爬状态。
进行射线检测,这里连线比较乱,所以新建了一个函数命名为 ObstacleDetection,输入三个浮点数,分别表示速度标度,最短检测距离和最长检测距离。
获得角色的速度向量,将其标准化获得速度方向,同时获取速度大小并将速度大小按照速度标度在最短检测距离和最长检测距离中插值,获得实际检测距离。实际检测距离乘以速度方向即可得到检测射线。获得角色位置,将角色位置做为起点,角色位置加上检测射线作为终点进行通道射线检测。通道射线检测中忽略掉存储的 NotClimbableActors,之后将命中结果,命中的位置和命中的法线输出。
若命中,则将命中位置保存为 ObstacleLocation,命中法线保存为 ObstacleNormal,未命中则不做处理。
有了命中位置还不够,如果只是小土坡很有可能也会检测到,但大台阶小土坡显然无法进行攀爬,因为它们不够陡峭,所以还要判断障碍物是否足够陡峭。这个比较简单,将前面输出的法线在纵坐标上减 1,再判断新的向量的模是否大于一即可,这个可以通过一个简单的图来表示。
如果障碍物不够陡峭则不进行后续操作,如果足够陡峭,则进行第二次检测。别忘了,第一次检测得到了 ObstacleLocation 和 ObstacleNormal 还有 ObstacleEndLocation 没有获得,这也是第二次检测的目的。当然这里还有另一层含义,ObstacleLocation 到 ObstacleEndLocation 的距离大体上可以看做障碍物的高度,如果障碍物不够高,比如还没角色高,那么此时需要进行的是翻越或者跨过等动作而不是攀爬。
第二次检测射线的起点位置仍然是角色位置,终点位置需要我们计算一下,这里创建了一个纯函数。纯函数意味着这个函数只通过计算等处理即可完成,不需要执行,类似一个公式一样。纯函数的内容如下,主要思想是将角色的向前向量(同之前一样)向上旋转 45°,乘以距离加上起点位置就能得到终点位置。
第二次检测后将检测结果和命中位置返回。未命中不作处理,命中则保存命中位置为 ObstacleEndLocation。
若第二次命中,则说明前方有障碍物且可攀爬,主动进入攀爬状态,设置 isClimbing 为 True。另外,前面提到过攀爬状态实质上是飞行模式,调用引擎提供的 SetMovementMode 方法,设置运动状态为飞行。接着,设置最大飞行速度,也就是攀爬状态的速度,另外调用 setOrientRotationtoMovetment 方法,修改 OrientRotationMovement 属性为 false。再然后调用之前创建的 INT_ChangeClimbingPosture 接口函数,将 is_Climbing 传递给动画蓝图,方便调整动画状态。这些流程我将其放入函数 ChangeMoveModetoClimb 中,便于整理和使用。其实转换到攀爬状态还需要调整两个设置,这个放到下面来说。
简单说明下 OrientRotationtoMovement 是角色移动组件中的一个属性,其为 true 则表示当有移动输入时,移动方向会转向旋转的输入方向,即静止不动时,转动鼠标时角色无反应,但在移动过程中,转动鼠标,角色会朝向鼠标输入的旋转方向移动,在攀爬过程中,这个属性不需要,故将其设为 false。
在进入攀爬状态的最后,为了让效果更加流畅,加入了一个跳跃上墙的动作,这里自然是用蒙太奇来实现的。将预置的跳跃动画创建蒙太奇,此时蒙太奇的插槽为默认插槽。
为了让蒙太奇可以播放,将动画蓝图的状态机的默认状态机后添加一个默认的插槽。
接着转回角色蓝图,添加一个新事件 JumptoClimb,在 SetMovementMode 之后调用。JumptoClimb 事件中先播放蒙太奇,接着将角色的旋转调整为计算出的旋转,对,就是那个在 FindClimbingRotation 中计算出的旋转。
攀爬状态中的处理
先来说大体逻辑,攀爬状态中的处理与正常状态进入攀爬状态一样,都是需要两次检测,但检测的目的并不完全一致,这个稍后细说。两次检测都成功则同样记录那三个变量 ObstacleLocation,ObstacleEndLocation,ObstacleNormal 然后进行一些处理,任意检测不成功就要转换到正常状态。
第一次检测,同样是起点角色位置,终点角色位置 + 一段向前的向量,不过这次不需要速度的判断,向前向量设置为定长即可,将命中结果,命中位置和法线输出。
这里有一个小技巧,ObstacleNormal,也就是法线方向,在正常状态下第一次检测到就直接存储了,因为只需要一次值即可。而在攀爬状态中如果持续运动,会持续产生法线,而攀爬过程中可能会受到地形起伏的影响,故可以利用插值的方式使得地形起伏的变化过渡地更加自然。这里使用了 VInterp to Constant 结点,将上个 ObstacleNormal 当做 Current,新检测到的 ObstacleNormal 当做 target,通过每次 tick 的时间进行插值,结果保存到 ObstacleNormal 用于下次的插值。
VInterp to Constant 是 UE4 众多插值结点中的一个,V 表示插值的数据是向量,Interp to 就是插值的意思,Constant 表示插值是线性的,不加 Constant 则表示非线性,起点和终点的过渡会比较平滑。
第一次检测不成功则说明没有地方可以攀爬了,转换为正常状态即可,这个部分放到后面写。
第一次检测完后进行第二次检测,同样是要判断障碍物是否够高,够高才能继续攀爬,不够高说明爬到顶部应该登陆了。与正常状态下的第二次检测时一样的,输出命中结果和位置,存储命中的位置。
如果第二次检测命中,则应当继续攀爬,此时需要进行一些设置。首先是攀爬方向,需要在动画蓝图中调用。这里使用 RotationFromXVector,从 X 方向创建一个旋转值。
这里打断一下,可能前面的一些铺垫操作你有些模糊了,前面实现攀爬逻辑中一开始我们就对攀爬时的坐标系进行了调整,将那张图再放一下
RotationFromXVector 结点的意思是 rotation 会跟随 XVector,这就导致 XVector 是常量的 roll 是无效的,而 pitch 和 yaw 是有效的。这里将输入的 Vector 和输出的 rotation 均拆分开,将 GetMoveForward 和 GetMoveRight 分别传入 XVector 和 YVector,此时蓝色的向量其实是 XVector,红色的向量其实是 YVector,再将 ZVector 设为 0,则只会产生 XVector 和 YVector 的变化,这其实就是一个 yaw 旋转。
拆分开的 rotation 将有效的 yaw 设置为 ClimbingRotation,攀爬时的方向就确定好了。接着调用之前创建的 INT_Direction 接口函数,将 ClimbingRotation 传递给动画蓝图,随后设置角色的旋转为 FindClimbingRotation 计算出的 Rotation。
最后,给角色移动组件加一个压向障碍物的力,这个力的作用是让角色能够紧贴障碍物。正常状态下,角色受重力影响会一直贴在地面上行走,但攀爬状态(实质上是飞行模式)不受重力这样的外力影响,如果遇到起伏的地形,很有可能会产生角色浮空的现象,可以从下图直观看到。
虽然 tick 一直在计算 Rotation 但角色实际上还是一段一段线段在运动,导致朝向障碍物的距离(图中粉紫色的线,其实是检测射线)越来越大。最直接的办法就是给角色一个外力,让角色能够一直保持紧贴障碍物。
以上这些操作我也将他们放到了一个函数中让蓝图更整洁,具体如下图。
两次检测都成功的处理介绍完了,那失败的呢?两次检测失败的情况是不一样的,先来说第二次检测未命中的情况:
第二次检测未命中说明障碍物不够高了,也就是说当前角色仍然在障碍物上,不过马上就要越过障碍物了,那么此时的处理就是让角色登陆。
登陆要分几步来处理。首先是检查是否有足够的空间来登陆,这里使用胶囊体检测来实现,胶囊体检测和射线检测类似,只不过起点和终点都是胶囊体,中间仍是一条线段,当线段或者胶囊体产生命中 Hit Result 就会为真。具体如下,新建一个函数 CheckSpaceforLanding,在函数中首先获得角色位置和向前向量,角色位置向上平移一段距离 Height 作为起点位置,起点位置向前一段距离 Distance 作为终点距离调用胶囊体检测。这里的 Height 和 Distance 我作为了函数传入的参数。最后输出胶囊体检测的命中结果和终点位置。
胶囊体的半高默认是 96,要想角色可以登陆则至少起点位置应在角色位置上方 192 的位置,考虑到此时胶囊体并不是完全在障碍物的上沿,那么这个距离还应更大一点,这里我设置的是 200,即 Height 为 200。然后就是向前的距离 Distance,这个距离是用来判断是否有地方让角色登陆,后续检测也会用到。这个变量不能太大,太大会让角色登陆后向前位移很多,也不能太小,太小很有可能角色登陆后站不下就又掉下来了,这里我设置的是 50。
跟前面的检测不同,这次的检测只有未命中才说明有足够的位置让角色登陆,这也是为什么需要终点位置。当检测出现命中时,说明要么空间太狭窄,要么登陆的路径上仍然有阻挡(这两种情况地形都很奇怪)而无法登陆。当然此时角色仍然在攀爬状态,可以选择让角色继续留在原地不进行任何操作,也可以像我一样,直接让角色退出攀爬状态。
紧接着是第二次检测,主要目的是找到角色的落脚点,故直接用刚刚得到的终点位置做起点,将他再向下 Height 距离的位置作为终点进行射线检测,如果成功命中则说明此时角色是可以登上去的,而未命中则说明这个障碍太薄了。检测结束后输出命中结果和命中位置。
如果角色可以登陆,那么接下来有这么几个操作:
- 播放登陆的蒙太奇
- 将角色位置移动到登陆的位置
- 改变角色的运动状态和其他细节调整
首先是最简单的蒙太奇,直接在动画序列中找到登陆的那个动画序列创建蒙太奇,再在蓝图这里调用即可。
然后是将角色位置移动到登陆的位置,使用 MoveComponentTo 结点。上面的检测得到了命中位置,要注意这个位置不是登陆的位置。MoveComponentTo 传入的目标位置是胶囊体中心的位置,所以应该将命中位置 Z 坐标增加一个半高 96 再传入。
最后是改变运动状态和其他细节的调整。改变运动状态这里类似前面的 ChangeMoveModetoClimb,创建一个函数命名为 ChangeMoveModetoWalk,里面的结点不变,不过要将一些属性反一下。设置 isClimbing 为假,设置 MovementMode 为行走,设置 OrientRotationtoMovement 为真,最后调用接口函数。
光改变运动状态是不够的,如果角色由一个斜面的障碍物登顶,还需要更改旋转使得登陆后能恢复到直立状态。这里使用的是通过时间轴对旋转进行插值。创建一个时间轴,长度为 0.2,0 处打关键帧值为 0,1 处打关键帧值为 1。关键帧插值设为自动使过渡更平滑。由于需要角色由原来的旋转过渡到直立,故需要 roll 和 pitch 均过渡到 0,而 yaw 不变。添加一个 Rotator 的 lerp 结点,获得角色的旋转赋与 A,再获得角色的旋转的 yaw 值赋予 B,roll 和 ptich 均默认为 0,Alpha 则为时间轴输出的参数。新建一个 SetActorRotation 的结点,将插值的结果作为 NewRotation 传入即可。
上图中除了改变运动状态和调整旋转外还有一个自定义事件 EndClimbing,前文中检测失败(登陆的第一次检测是成功)退出攀爬状态都是直接调用这个事件。
控制结构
正常运动进入攀爬状态和攀爬状态中的处理都写完了,在此之前,还需要一个控制结构来控制什么时候进行哪种情况的处理。最基本的就是判断是否在攀爬状态,如果是则进行攀爬中的处理,不是则进行正常运动的处理。注意,一旦正常运动进入攀爬状态,则正常运动的处理就不应该再进行了,等到开始进行攀爬中的处理时,再重置正常运动的处理使得攀爬结束后可以重新进行正常运动到攀爬的转换。
另外,攀爬中的处理使用了一个门来控制,在正常运动进入攀爬状态的最后打开,在退出攀爬运动时关闭。
评论