角色控制是游戏设计中必不可少的一个设计环节,这一节我们讲一讲如何制作基本的角色运动控制交互逻辑。
因为是简单实例教程,所以一律不涉及角色动画控制,只谈运动控制。
Demo演示:Simple Movement Control
常见的运动控制交互设计有这么几种:
- 方向控制类
- 使用单方向控制角色在特定平面内自由运动,角色始终面朝运动方向
- 使用双方向分别控制角色在特定平面内自由运动的位置以及运动方向(“双摇杆”射击)
- 使用单方向控制角色在棋盘格内非自由运动移动
- 目的地控制类
- 设置目的地让角色自动运动过去(“点击移动”式)
与运动控制息息相关的是摄影机运动控制,通常会有这么几种形式:
- 大鸟瞰视角摄影机不运动
- 固定视角摄影机(正俯视、斜俯视、正平视)跟随或半跟随角色
- 第三人称可变视角跟随摄影机
- 第一人称主视角摄影机
在Unity3D中,控制游戏物体的运动主要有这样几种实现方法:
- 完全依靠物理解算,基本不控制:比如我们在PlayMaker简单实例(1):PM_Cube中最后做的发射小方块的例子,设置力量、初始速度,然后就都交给物理引擎去计算了。
- 完全通过transform参数控制位移和旋转:比如我们让游戏物体永远跟随鼠标运动。
- 借助物理引擎进行部分控制:
- 对Rigidbody持续施加动力(force)或持续设定速度(velocity);
- 对Character Controller设置运动(move);
- 对NavMesh Agent设置运动(move)或设置目的地(set destination)。
完全依靠物理解算不适合用来进行准确操控,完全通过位移和旋转参数进行控制很难处理碰撞问题,所以最后一种方式是我们常用的。方向控制类通常借助Rigidbody或者Character Controller,目的地控制类通常借助NavMesh Agent。
范例01:单方向运动控制
分析:
单方向运动控制是持续不断给运动物体指明其运动方向和速度,且运动物体始终面向运动方向的一种控制方法,我们用键盘方向键、手柄摇杆、甚至用鼠标指针位置来为物体指引方向。
“速度”本质上是一个矢量,其方向代表了运动的方向,其长度代表了运动的快慢。
如果是键盘或者手柄的话,使用input axis就可以获取一个二维的矢量输入作为方向指示,再添加一个自定义的speed属性就可以了;如果是鼠标,则可以用鼠标相对于运动物体的位置作为其方向指示,鼠标离运动物体的距离长短作为速度指示。
准备场景
依旧沿用PM_Cube的项目文件,新建一个名叫Movement的场景。创建地面,并拼一个简易的Player角色出来。
Player角色由一个方块身体,一个方块头部,再加一个黑球以指示正面方向。其实用什么模型都可以,我习惯于用一个Player
空物体作为顶级节点,把其他的视觉元素都放在Player
下方。三个视觉元素的Collider
组件我都删掉了,不需要它们进行碰撞解算,然后在Player
上添加了一个Box Collider
组件,调整到合适位置。
FSM_Movement
为Player
添加Fsm,改名为FSM_Movement
。
在State 1
中添加一个Get Axis Vector
的行为以获得Axis Input,再添加一个Set Velocity
的行为给Player设置速度。
出现这个提示代表我们缺少Rigidbody
组件,Set Velocity
只能作用于Rigidbody,点击提示自动添加。
在Get Axis Vector
中,默认已经把Horizontal Axis
和Vertical Axis
填好了,这个名称是和Input面板中的设置一一对应的,大家可以从菜单Edit
> Project Settings
> Input
查看。设置Store Vector
参数为一个新的变量input axis
,让Get Axis Vector
把输入矢量储存到这个变量中。
比如我们按下AWSD键,由于A
和D
被map到了Horizontal Axis
,W
和D
被map到了Vertical Axis
,因此会造成Horizontal Axis
和Vertical Axis
的输入值从0快速变成1或者-1(W和D代表正向,为1,A和S代表反向,为-1),然后由于Get Axis Vector
中Map To Plane设置的是“XZ”,所以输出矢量的X值会取得Horizontal Axis
值,输出矢量的Z值会取得Vertical Axis
值,输出矢量的Y为0。
在Set Velocity
中可以直接将input axis
变量赋给Vector
参数,然后设置运动坐标系(Space
)为World,勾选上Every Frame
选项。这样,游戏每时每刻都会监控Horizontal Axis和Vertical Axis的输入,并以此调整游戏物体的速度。
Get Axis Vector
的Multiplier
参数会将输出矢量放大,所以我们也可以为Multiplier
设置一个新变量speed
,并在Variable面板中设置speed
为10,并使其在Inspector中可见。
测试场景,Player在场景中翻滚,这是因为我们给了速度,但物体很轻,地面摩擦力造成其重心不稳容易倾倒(毕竟是刚体物理解算啊)。
一个解决方法是对其Rigidbody的旋转予以约束,不让其沿着x轴和z轴旋转。
另一个办法是让地面变得没有摩擦力。新建一个Physic Material,命名为slippery
,拖到场景中赋给Ground
地面物体,然后修改slippery
的运动摩擦(Dynamic Friction
)和静态摩擦(Static Friction
)为0。
我这里选择第一种方法,也就是约束Rigidbody的x轴和z轴旋转。同时我把Player
的speed
值修改为5,10米/秒的运动速度对于一个1米25高的Player来说有点太快了。
接下来添加Player旋转的控制。
在Action Browser里输入“rotate”,会出现很多一些关于旋转的Action,但这都不是我们会用到的。实际上,适合我们需求的Action的名称并不包含“rotate”,而是包含“look at”。所以说,对于常用的Action命令还是要记忆一下,否则连关键字都打不出来就不好了。
在Transform类别下有很多种“Look At”,看名字知道带“2d”字眼的肯定是用于二维游戏制作,而带“Smooth”字眼的很有可能是平滑过渡,而不带“Smooth”的则可能是突然变化。
选择Look At
或者Smooth Look At
都可以。
Look At
:
让Game Object
的正面(Z轴正方向)立刻指向目标物体(Target Object
)或目标坐标(Target Position
),如果Keep Vertical
被勾选,则保证该物体仅绕自身纵轴旋转,否则使用Up Vector
中设置的方向作为上方。
勾选Draw Debug Line
的话,会在场景视图中显示一条Debug Line Color
中所指定颜色的直线以方便判断是否正确。
Smooth Look At
:
让Game Object
的正面(Z轴正方向)按一定速度(Speed
)转向指向目标物体(Target Object
)或目标坐标(Target Position
),如果Keep Vertical
被勾选,则保证该物体仅绕自身纵轴旋转,否则使用Up Vector
中设置的方向作为上方。
当游戏物体正向与目标所在方向达到Finish Tolerance
允许的接近程度时,触发Finish Event
中指定的事件。
勾选Debug
时,会在场景视图中显示调试用指向箭头。
现在的问题是如何获得这个Target Object
或者Target Position
。提供一个思路给大家参考:
如果将Player
自身位置作为坐标原点的话,这个方向实际就是我们Input Axis所指向的方向。所以Target Position = Player Position + Input Axis。在Get Axis Vector
中我们已经得到了一个放大过的input axis
变量了,所以这里直接使用这个值。
在Set Velocity
下方添加一个Get Position
和一个Vector3 Add
。在Get Position
中获取Player
的位置,储存在player position
中,然后在Vector3 Add
中把input axis
加给player position
,现在这个player position
值就是我们的目标点位置。
PlayMaker中用Action来进行数学计算就是这么混乱,习惯了就好了。
注意,这个Get Position
和Vector3 Add
都是每帧运行的。
最后,将这个player position
值设置成Smooth Look At
的Target Position
。
更简单一点的办法是使用
Smooth Look At Direction
行为,直接把input axis
指定给Target Direction
就好了。
摄影机跟随
通常这样的操作方式都会采用俯视摄影机跟随的方式来设置视角。
最简单的一个做法其实是把主摄像机当做Player的子物体就好了,但这样做没有扩展性,比如以后Player加上跳跃的话,相机视角就会跟着跳起来。
于是更常见的操作是实时根据Player位置去设置主摄像机的位置,直接给Player Position加上一个偏移量(offset)就好了。
选择Main Camera,添加Fsm(修改名称为“FSM_Follow”)
State 1
是用来做初始设置的,State 2
才是真正用来设置相机跟随的状态。
用专门的初始状态来做预设定是PlayMaker的常见做法,比如这个例子里面我们需要在游戏场景中寻找一个叫做“Player”的游戏物体,这个操作只需要在游戏初始化的时候做一次就行了,所以放在专门的State里面,执行完毕就转换到其他State,以后就不用反复执行这种查找操作了。
Find Game Object
常常被用作在场景中根据名称(Object Name
)来寻找特定游戏物体,同时还可以按照特定标签(With Tag
)来过滤。找到的游戏物体可以保存(Store
)在一个变量中(这个例子里面我新建了一个player
变量)。
但要记住的是,如果场景中同时有多个同名游戏物体(比如实时生成的克隆物体就都是一样的名字),
Find Game Object
只会返回所找到的第一个游戏物体,而不会把所有的物体信息都保留下来。所以一定要确保场景中不会出现同名的查找对象。
在State 2
中:
- 添加
Get Position
以读取player
物体的位置信息,储存在player position
中; - 然后使用
Vector3 Operator
来计算player position
变量和一个新建的camera offset
变量的相加结果,并储存在一个新建的变量camera position
中; - 最后添加
Set Position
把变量camera position
变量指定给当前物体(也就是Main Camera
)。
Vector3 Operator
可以对两个Vector3类型的变量做很多操作:相加、相减、获取夹角、获取距离等等,并将结果储存于第三个变量。我们以后可能会经常用到这个行为。具体这个例子中,也可以直接用
Vector3 Add
来做这个相加的计算,但Vector3 Add
不能指定第三个变量来储存结果,而是直接改变第一个变量值,所以如果使用Vector3 Add
的话,后面的Set Position
就需要使用player position
这个变量了(现在的player position
变量所代表的位置,已经不是Player所处的位置了,而是偏移之后的位置。
运行测试,在测试状态下可以实时修改camera offset值(因为之前已经设置其在Inspector中可见了),实时调整合适的相机位置。
一个比较理想的相机位置是Y = 10,Z = -5,然后相机自身的Rotation X = 60。
(红色界面表示当前处于运行模式)
要注意,运行模式下所做的修改都在退出运行模式后都不会得到保存。一个不是特别完美的解决方案是在点击相应组件(Component)的设置按钮(小齿轮图标),然后选择
Copy Component
,等退出运行模式以后再同样点击“设置”,选择Paste Component Values
,这样就可以把运行模式下该组件的参数信息应用到正常模式下了。
范例02:“双摇杆”式运动控制
分析:
“双摇杆”是单方向控制的升级版,单独将运动物体的面对方向拿出来用第二个摇杆或者鼠标指针来予以控制。
摇杆的话,还是使用input axis就可以了;鼠标的话,大多利用鼠标相对于运动物体的位置矢量做指示,让物体始终朝向这个鼠标的方向。
鼠标是在摄影机平面运动的,要用鼠标来指示运动物体的朝向需要将鼠标平面坐标转换成三维坐标,因此需要使用Raycast。
如果角色有动画,“双摇杆”式控制就要根据两个方向之间的角度来切换运动动画,比如人往前走往后走以及侧身走的动画都是不一样的。
Rigidbody + 双摇杆
我们可以在上一个范例的基础上将其改造成“双摇杆”式运动控制。
将Player
保存成一个prefab,更名为“Player_A”,这时候场景中的Player
也被自动更名为“Player_A”了。
断开场景中Player_A
与prefab的联接(菜单GameObject
> Break Prefab Instance
),再将其名称改回“Player”。
在FSM_Movement中,为State 1
添加一个Mouse Pick
行为。
Mouse Pick
可以帮助我们获取鼠标指针下的游戏物体的相关信息,我们这里需要获得的是鼠标指针下地面物体上对应点的三维空间位置。
所以我们设置Mouse Pick
中Store Point
为新建变量mouse point
,设置Layer Mask
为1,并在新出现的Element 0
参数中选择“Add Layer...”以新建一个叫做“Ground”的“层”,并选择这个Ground
层为Element 0
参数的值。
当然我们也可以直接在工具栏的Layer设置中直接选择
Edit Layers...
,或者选择Ground
物体之后在其Layer设置中选择Add Layer...
,都可以打开Layer编辑面板。
别忘了,不管用什么办法打开层编辑面板添加的新层,都需要将
Ground
物体指定到这个层中才会起作用。初学者经常指定了新层,然后设置了层遮罩,却忘记将游戏物体指定到新层中。
做这样的设置是为让Mouse Pick
行为中发射的ray只探测Ground层中的物体,这样避免探测到我们的主角、房屋、甚至UI物体,造成混乱。
此外,这里这样做是比较简化的设计,默认鼠标永远都会探测到地面物体,永远都会有数值返回给
Store Point
。
假如鼠标探测不到任何有效的物体,会将Store Point
数值设置为<<0, 0, 0>>
,出现跳帧现象。
想偷懒的话,就把地面设大一点,或者干脆设置一个很大的专门的地面物体不显示,只用来接受探测。
Mouse Pick
需要被设置成每帧运行。
获得了鼠标位置点以后,就可以将mouse point
变量赋给一个Look At
行为的Target Position
参数,让Player永远面向这个位置。
使用Smooth Look At
也可以,但鼠标的移动和角色的转向之间就有一点延迟效果了,如果需要这种效果可以选择Smooth Look At
行为。
我将原来的Smooth Look At Direction
前面的小√去掉了,这样这个行为就不会起作用,大家也可以将它删掉。
运行测试,感觉使用Smooth Look At
的效果更顺滑一些,觉得转向太慢可以将Speed
值设置大一些。为了显示方便,我勾选了Smooth Look At
的Debug
选项,并做了一个小球当做指示物体(再添加一个Set Position
,用来指示小球的位置为mouse point
即可)。
最终完整的Action结构如下图:
Character Controller + 双摇杆
Character Controller是Unity3D提供的一个专门用于角色控制的组件:
-
Slope Limit
:角色能够爬上多大角度的坡 -
Step Offset
:多高(小于设定值)的平台会被认为是台阶 -
Skin Width
:两个角色的碰撞体(Capsule Collider)之间能够相互穿透多大距离 -
Min Move Distance
:可以移动的最小距离(用来避免轻微抖动) -
Center
:角色的Capsule Collider中心高度 -
Radius
:角色的Capsule Collider的截面半径 -
Height
:角色的Capsule Collider的高度
Character Controller实际上就是一个不可见的Capsule物体,角色模型则被放置在这个Capsule物体内部,随之而动。
下面我们把上面的例子改造成由Character Controller
组件来控制的角色。
将上面例子中的Player
做成Player_B.prefab
,然后断开场景中Player的Prefab联接,名称改回Player
。
删除掉Box Collider
组件和Rigidbody
组件,添加Character Controller
组件,设置好Capsule碰撞体的大小和位置(按上图)。
然后在FSM_Movement
中,删除Set Velocity
行为,换成Controller Simple Move
行为,把input axis
变量指定给Move Vector
参数,然后把speed
变量指定给Speed
参数。
Controller Simple Move
给Character Controller指定一个方向Move Vector
,并设定一个速度Speed
,Character Controller就可以匀速运动了。
角色指向部分的设置不需要改变。
(折叠的Action都没有修改)
运行测试,发现角色一跳一跳的,但从场景视图来看,角色本身的运动是平滑的,问题貌似发生在摄影机的跟随交互逻辑中。
但是,摄影机的逻辑很清晰啊:获取Player位置,添加offset,然后设置Camera位置。
真正的问题出在Character Controller
组件的设置里。Min Move Distance
= 0.001的默认设置让角色的真实运动和其Position属性并不是完全一致的,这样摄影机在运动刚开始的几帧里面就会比角色快。修改Min Move Distance
参数为0就可以保持同步了。
另一个解决方案是在Player内添加一个空物体
Camera Point
,然后让摄影机不指定Player
而指定这个Camera Point
为其跟随对象。
运行测试场景,一切正常。
在场景中新建一个Cube物体当做障碍物,Player可以和障碍物的Collider相互碰撞。这是因为Character Controller
组件自己就可以被当成是一个Capsule Collider。只不过以后要做碰撞检测的时候需要使用专门的Controller Collision行为。
为了不要频繁地修改
Player
名字,我把Main Camera
的Fsm中的State 1
中的Find Game Object
修改了一下,不指定名字,而是指定Tag,意思是让其寻找场景中有Player
这个Tag的物体,通常这个物体都只会有一个。这样一来,只要我将Player的Tag指定为Player,不论它具体名称是什么,都能被这个行为找到。
为场景物体或者Prefab添加Tag都是在Inspector中左上角进行添加的,如果需要增加新的Tag,点击
Add Tag...
,跟添加新的Layer是一样的操作。
把这个Player保存成Player_C.prefab
。
范例03:“点击移动”式运动控制
分析:
“点击移动”是给定一个目的地,然后让运动物体自动移动过去。如果不考虑障碍物的因素,这个目标很容易达成,但如果场景中有障碍物,“点击移动”就变得很复杂了。
这是因为如果希望运动物体绕开障碍物,就必须让其具有一定的智能,在游戏设计中我们称之为“寻路系统”。Unity3D内置一个比较简单的“寻路系统”:Navigation,主要由Nav Mesh和Nav Agent两个部分组成。Nav Mesh生成一个可以行走的范围区域,Nav Agent为运动物体计算出一条可行的路线。
要实现“点击移动”,需要将Player设置成一个Nav Agent,然后鼠标点击实际上是告诉这个Nav Agent你的目的地在哪里。
Nav Agent不是Rigidbody,不能使用Set Velocity这样的行为,但Nav Agent自身也提供了很多方便的函数给我们使用。
要在PlayMaker中运用这套“寻路系统”,需要加载一套专门的命令库。
老规矩先准备场景。断开Player的prefab联接,然后删掉Character Controller组件和Fsm组件,并在场景中用大方块拼一些障碍物出来。
为Player添加Fsm,建立如下Graph:
在State 1
中添加Get Mouse Button Down
,设置当鼠标左键按下时,触发事件LMB Down
。
在State 2
中添加Mouse Pick
,设置从鼠标位置对Ground
层中的物体发射Ray,将获取的点位置储存在mouse point
中,这个mouse point
就是我们设置的“目的地”。
再添加一个Move Towards
,设置Target Position
为mouse point
,设置Finish Event
为FINISHED
,就可以实现“点击鼠标左键后Player移动到相应点的位置”的需求了。
但是,Player是沿着直线运动的,不会回避障碍物。
Move Towards
是一个不依赖任何物理学计算纯粹改变物体位移属性的Action。它会在物体靠近目的地(距离 <Finish Distance
参数值)时触发Finish Event
中所指定的事件以跳转到其他状态。一旦跳转状态,
Move Towards
就不再起效果了,物体会立刻停止移动。
删掉这个Move Towards,下面我们来建立一个简单寻路系统。
首先给Player添加一个Nav Mesh Agent组件。
-
Agent Type
:设置Agent类型,主要是控制Agent能跨上多高的台阶,爬上多陡的坡 -
Base Offset
:Agent离地高度 -
Speed
:运动速度 -
Angular Speed
:转身速度 -
Acceleration
:最快加速度(值越大,启动越快) -
Stopping Distance
:接近目标距离多近时停止运动 -
Auto Braking
:如果勾选,Agent在接近目标时会自动减速 -
Radius
:Agent碰撞体的截面半径 -
Height
:Agent碰撞体的高度 -
Quality
:回避障碍物的计算精度,这个质量设置越高越耗费CPU资源,设置成None
就不会回避障碍物而是直接撞上去了 -
Priority
:计算资源不够时优先度低的Agent会降低回避障碍质量 -
Auto Traverse Off Mesh
:如果需要自行处理角色在Off Mesh Link处的行为的话,不勾选此选项 -
Auto Repath
:让Agent到达分路径末端时会自动重新计算路径 -
Area Mask
:可以设置部分区域不允许该Agent通过
这个组件中可以通过Radius
和Height
参数设置Agent的碰撞体大小。与Character Controller不同,Nav Mesh Agent的碰撞体是圆柱体。
从菜单Window
> Navigation
打开Navigation面板,
选择地面,勾选Navigation Static
,并将其设置成Walkable
。
选择所有的障碍物,勾选Navigation Static
,并将其设置成Not Walkable
。
到Bake面板中点击Bake
按钮进行Nav Mesh的烘焙。
-
Agent Radius
:这个值越高,障碍物边缘不可行走的空档就越大,最好设置成所有Agent中最小的那个的半径值 -
Agent Height
:这个值越高,半空中有障碍物的区域就越可能不可走过,最好设置成所有Agent中最高的那个的高度 -
Max Slope
:最大可行走坡度 -
Step Height
:最大可行走台阶高度 -
Drop Height
:如果这个值为正,就在小于这个高度的两块Nav Mesh之间创建“掉落”类型的Off Mesh Link -
Jump Distance
:如果这个值为正,就在小于这个距离的两块Nav Mesh之间创建“跳跃”类型的Off Mesh Link
按照上述设置而烘焙成的Nav Mesh如下图所示:
蓝色区域就是可以行走的区域。
要注意,参与创建Nav Mesh的可行走表面和不可行走表面都需要设置成
Navigation Static
,一旦被设置成Navigation Static
,该物体就不能再移动了,也不能播放动画。
在场景中选择Player
,继续设置Fsm。
想要Nav Mesh Agent运动,可以直接给它设置一个目标,它就会自动寻找合适路线走过去。我们在State 2
中的Mouse Pick
后面添加一个Set Agent Destination
,将Destination
参数指定为mouse point
变量。
注意,
Set Agent Destination
这个Action并不包含在PlayMaker的初始安装文件中,需要自己去官方WIKI下载适合版本的PathFinding.unitypackage
安装以后才能正常使用。此外,如果希望能够在
5.6
版本中就使用最新2017.1
的Navigation系统,可以去Github上相关页面去下载核心脚本,解压后放进工程文件夹就可以了。
运行场景,我们可以看到Player现在可以自己找路了。
最终的Demo场景我添加了一个Manager,让每次载入场景的时候都会随机选择不同的Player.prefab来载入,这一部分的具体制作过程就不写了,搞成随机的主要原因还是我懒得分成4个不同的场景而已。
项目工程文件(不包括PlayMaker插件及其uGUI扩展Action包)