用Bolt实现角色运动控制(1):FPS式第一人称角色控制

在这一讲以及接下来的两讲中,我会教大家用Bolt来实现3中常见的角色运动控制方式:第一人称FPS式、第三人称RPG式、点击-移动式。具体的原理在之前的《PlayMaker简单实例(2):角色与摄影机的运动控制》一文中已经说得很多了,这里只是换了一个方式来实现而已。

相关视频教程我发在B站了,但因为没有加速过,所以都还蛮长的,建议还是先看看图文教程熟悉下再去观看视频教程。


FPS式第一人称角色控制

视频教学地址:B站在线观看
工程文件下载地址:https://pan.baidu.com/s/1EvhF1P73ldyOvQRnxxvXoQ 提取码:5m93

PS:本教学使用的是Bolt1.4版,并未包含在工程文件包中,请大家自行购买安装

基本需求:

  • 鼠标运动控制角色视角移动
  • 键盘ASWD控制角色前后左右运动
  • 空格键跳起

实现原理:

  • 使用Rigidbody来运动角色
    - 通过设置Rigidbody.velocity属性来控制角色移动速度
    - 通过Rigidbody.AddForce函数来让角色跳起
  • 将摄影机设置为角色子物体以实现跟随
  • 通过HorizontalVertical的Input Axis来获取运动控制的输入
  • 通过Mouse XMouse 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小黑点来模拟武器准心。

准心效果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容