用Bolt实现角色运动控制(1):FPS式第一人称角色控制
在这一讲以及接下来的两讲中,我会教大家用Bolt来实现3中常见的角色运动控制方式:第一人称FPS式、第三人称RPG式、点击-移动式。具体的原理在之前的《PlayMaker简单实例(2):角色与摄影机的运动控制》一文中已经说得很多了,这里只是换了一个方式来实现而已。
相关视频教程我发在B站了,但因为没有加速过,所以都还蛮长的,建议还是先看看图文教程熟悉下再去观看视频教程。
FPS式第一人称角色控制
视频教学地址:B站在线观看
工程文件下载地址:https://pan.baidu.com/s/1EvhF1P73ldyOvQRnxxvXoQ 提取码:5m93PS:本教学使用的是Bolt1.4版,并未包含在工程文件包中,请大家自行购买安装
基本需求:
- 鼠标运动控制角色视角移动
- 键盘ASWD控制角色前后左右运动
- 空格键跳起
实现原理:
- 使用Rigidbody来运动角色
- 通过设置Rigidbody.velocity
属性来控制角色移动速度
- 通过Rigidbody.AddForce
函数来让角色跳起 - 将摄影机设置为角色子物体以实现跟随
- 通过
Horizontal
和Vertical
的Input Axis来获取运动控制的输入 - 通过
Mouse X
和Mouse Y
的Input Axis来获取鼠标移动的输入 - 通过
Jump
的Input Button来获得空格键的输入
实现步骤:
首先设置场景:
新建一个4×1×4的Plane当做地面,重置position,添加一个带地面贴图纹理的材质,避免地面太白看不清楚。
新建一个空物体当做Player,放置在(0,1,0)位置。
给Player添加一个Capsule Collider当做碰撞体,设置中心点位置为(0,0,0),高为2,这样这个Player就正好站立在地面上了。
将摄影机设置为Player的子物体,设置position为(0,1,0),rotation为(0,0,0),这样摄影机就在Player的头顶处了。
给Player添加Rigidbody组件。这时可以选择Freeze掉x轴和z轴的旋转,以避免一些奇怪的旋转,但因为我们后面会直接设置Player的旋转属性,所以其实最终是不需要Freeze掉的。
再给Player添加一个Flow Machine组件,使用Embed模式,准备开始制作运动控制的交互逻辑。
完整Graph如下:
然后设置角色移动的交互逻辑
点击Edit Graph
打开Flow Graph窗口,右键单击选择Add Unit...
,搜索“set velocity”,得到相应的Set Velocity
单元。
因为是使用Rigidbody来控制,所以新建一个Fixed Update
单元,将其与Rigibody: Set Velocity
连接起来。
使用Fixed Update
而非Update
是因为前者是每一步物理解算都会更新,而后者只是每帧更新,在这个范例里区别其实不明显,但如果涉及到比较严格的刚体运动、碰撞等计算,还是用Fixed Update
比较保险。
Rigibody: Set Velocity
直接使用Self(自身)作为被设置速度的对象,我们需要再给其输入一个Vector3向量以代表速度(的强度以及方向)。
这里使用了两个Input: Get Axis
单元来分别获得“Horizontal”和“Vertical”两个axis上的输入数值,也就是AWSD或←↑↓→键的“是否被按下”状态,将这两个值分别连给一个Vector3: Create Vector 3
单元的x和z输入port,用来组合一个velocity向量。
这个速度向量的y输入则使用Rigibody: Get Velocity
单元来获得自身的velocity值,并通过Vector3: Get Y
单元来获取这个值在y轴上的分量。也就是说,我们并不想去控制Player的y轴运动。
要注意:我这里
xx: xxx xxx
的写法代表了这个Bolt单元的完整名称,:
之前其实是类名,:
之后才是真正的函数名或属性名(一个“类”里面可能有很多个函数或属性,大家可以自己去查Unity帮助文件)。以后慢慢就不会写得这么麻烦了,经常会省略掉类名,大家要习惯。
Create Vector 3
的输出会传递给Set Velocity
的输入,这样就可以通过键盘的输入来控制Player的移动了。
当然这样控制的移动速度是很慢的,因为Input: Get Axis
的取值范围是(-1,1)。想要更快的速度就需要将这个值放大,所以可以将这两个Input: Get Axis
的输出值乘上一个系数之后再传递给Create Vector 3
:
这里的Multiply
单元相当于乘号,Float
单元的完整名称是“Float Literal”,相当于一个浮点常数4。
要注意,规范的做法是新建一个变量“Speed”,设置“Speed”初始值为4,然后在这里使用这个变量“Speed”,直接在脚本中用常量而不用变量的做法是不太符合规范的,只是本范例中我故意回避了去使用变量,所以就直接用了
Float Literal
。
运行场景,可以在Graph窗口中看到实时的数值传递,这对于我们debug(调试除错)是非常非常有帮助的。我们可以看到,Get Axis输入的1经过Multiply放大后变成了4,然后被组合成了velocity向量(4,0,0)。
PS:在Play状态下,蓝色的单元代表当前帧是活动的,起效的,白色代表不执行。这也很方便我们的调试工作。上面这个Graph因为是从
Fixed Update
开始的,每帧都会运行,所以所有单元都是蓝色活动单元。
接下来是角色及视角旋转的交互逻辑
角色及视角的旋转我准备直接对Transform进行操作,用不到Rigidbody,因此也不需要从Fixed Update
开始,从Update
出发就足够了。
新建一个Transform: Set Euler Angles
单元,连给Update
。
Set Euler Angles
需要输入一个Vector3向量,用来设置rotation的三个角度值,我们可以用Create Vector 3
来创建这个向量,并且用Get Axis
获取“Mouse X”(也就是鼠标x轴方向的位移量),将这个数值传递给Create Vector 3
的y轴输入以达到用鼠标x轴运动来控制自身y轴旋转的目的。
但这样得到的结果只是不断地在偏移一个较小的角度,因为鼠标每帧x轴的位移量有限,且鼠标不移动是,角色自身的旋转就为0了。因此需要将鼠标x轴位移作为角色自身y轴旋转的增量而不是绝对量。
因此,我们需要添加一个变量“Rotation Y”,然后将其与Get Axis
的结果相加,并将相加的结果通过Set Variable单元设置给“Rotation Y”变量本身,这样的结果是变量“Rotation Y”每帧都会根据鼠标x轴位移数值而增加(或减少)一定的量。
然后再将变化过的变量“Rotation Y”输出给Create Vector 3
的y轴输入,这样就可以得到正确的“随鼠标运动而旋转”的结果了。
同理,可以使用来一个变量“Rotation X”来与Get Axis
获取的“Mouse Y”的输入数值相加,得到一个随鼠标y轴移动而不断增加或减少的变量,再用这个变量来控制相机(而不是Player)的x轴旋转即可得到角色视角随鼠标上下移动而变化的交互控制。
注意,这里使用了
Camera: Get Main
单元来获得场景的主摄影机。
只不过,这时候得到的是视角上下变化与我们想象中有所区别,因为鼠标向上移动时Mouse Y为正,那么相机的rotateX为正,相机是往下在旋转。
修正这个问题只需要将前面的Add单元改成Subtract单元,让变量“Rotation X”每帧减去(而不是加上)Mouse Y的输入值即可。
最后,为了防止视角穿帮,我们希望限制相机的rotateX不要超过一定的范围,所以就在Subtract的后面又添加了一个Mathf: Clamp单元,让其输出值保持在-90°到60°之间。
完成旋转的交互操作之后再测试,发现移动的交互操作其实是有问题的,当我们通过移动鼠标改变了角色的y轴旋转方向之后,角色的运动控制就不是我们想象中的“按下w键角色往自己的前方走”了,而是斜着沿世界坐标的z轴正方向运动。这是因为Set Velocity
中设置的速度是基于世界坐标系,而我们想要得到的控制却是基于角色自身坐标系的。
因此,我们需要将我们的输入从自身坐标系转换成世界坐标系,也就是说,当我们按下w键时,不能直接输出一个(0,0,5)的velocity,而是要输入一个相对于自身坐标系(0,0,4)的velocity在世界坐标系中的准确方向。
这一工作可以用一个Transform: Transform Direction单元来完成:
可以看到,最终输出的velocity并不是(0,0,-5),而是(0.9,0,-4.9)。
完整Graph如下:
位移控制Graph 旋转/视角控制Graph
补充说明:其实还有更简单的方法来实现“根据鼠标运动旋转视角”这样交互行为,那就是使用
Transform: Rotate
单元。Transform: Rotate
会根据所输入的数值对对象进行持续的旋转操作,而不仅仅只是改变对象的旋转属性值,这样就不要手动去做每帧累加的操作,仅需要直接将“Mouse X”的输入值倍乘之后传递给Transform: Rotate
即可。
接下来是跳跃的交互逻辑
在Graph的空白处添加一个On Button Input
单元,设置Action为“Down”。这是一个Event类型的单元,其作用是“当xx按钮被按住/按下/松开时,被触发并执行其后连接的单元”。Bolt中有很多这类“On xxx Input”,比如监测鼠标按键的、监测键盘按键的、监测碰撞/触发器的,等等等等。
以这类单元打头的Graph,只有在Action条件被满足之时才会运行。以
On Button Input
为例,如果Action选择“Hold”,则当按钮被按下时会每帧运行,直到按钮被松开,但如果选择“Down”或者“Up”,则只会在按钮被按下或松开的那一帧运行一次。
在On Button Input
单元后添加一个Rigidbody: Add Force
单元,设置Mode为“Impulse”,代表一个突然爆发的作用力,同时创建一个Create Vector 3
单元,设置成(0,4,0),传递给Add Force
的Force端口,代表这个突然爆发的作用力的方向,是沿着世界坐标y轴正方向的。
运行场景,按下空格键时,Player会跳起。但是,如果不停按空格键,Player会不断上弹,完全不会落地。这就暴露出一个问题:我们需要检测Player是否落地,如果没有,就不能让Add Force
起效。
要做到这一点,可以设置一个Bool类型的变量“Grounded”,并在On Button Input
后面添加一个Branch
单元,用变量“Grounded”作为输入条件,并设置当其值为“True”时,才会继续执行Add Force
,否则就什么都不执行。
Branch
单元相当于if... else...
条件语句,通过它,可以改变Graph的“流动”方向,产生“支流”。
但现在变量“Grounded”并不能真的反映角色是否落地面上。我们还需要一个新的Graph来完成检测角色是否落地这一任务,并将结果返还给变量“Grounded”。
通常这种“检测是否落地”的任务都可以用射线(Raycast)来检测,我们可以从略高于角色脚底的位置向下发射一条较短的射线,如果这条射线碰到了地面物体,那么我们可以认为角色落地了,否则角色就是悬空的。Unity甚至允许用一个虚拟的球形来做这类检测工作,可以得到更准确的反馈。相应的单元是Physics: Sphere Cast
。
要注意,
Physics: Sphere Cast
这个单元有非常多的分身,我们一定要选择到有Origin、Radius、Direction、Max Distance、Layer Mask以及Hit Info的那一个。
最后的Graph是这样的:
上图中的意思是:从自身位置(应该是离地1单位高度)向下方(y轴负方向)发射一个半径为0.3的球,球会一直运动1单位长度,如果在球的运动过程中没有检测到“层”设置为“Ground”的碰撞体,则输出值“False”给变量“Grounded”,反之则输出值“True”。
其中的
Layer Mask
单元的全名是“Layer Mask Literal"。
然后将地面物体的Layer设置成“Ground”,如果没有“Ground”这个layer,就自己先点击“Add Layer...”创建一个,然后再设置。
注意,新手常常会犯的两个问题是:1. 忘记设置Layer了,那么会导致怎么着都检测不到地面,因为没有任何碰撞体处于“Ground”层;2. 创建Layer之后就以为已经自动指定给游戏物体了,其实并没有,一定要好好检查。
如果设置没有错误,那么角色没有落地的时候Physics: Sphere Cast
是检测不到任何碰撞体的,那么变量“Grounded”始终为“False”,按空格键不会有任何反应。
至此,我们就完成了这个简单的第一人称视角角色运动控制的交互逻辑。为了增加一点趣味性,我们可以利用Cursor: Set Visible
单元隐藏掉鼠标图案:
并在场景正中间创建一个UI小黑点来模拟武器准心。
准心效果