【Vic的小课堂】Unity游戏功能(2)—第一人称镜头

·序言

“你最早喜欢上的游戏类型是什么?”

如果询问每一位游戏爱好者和游戏开发者,那么FPS(First-Person Shooting,第一人称射击)类型游戏必定是一个高频次出现的答案。甚至说,若要首次感受到游戏的乐趣,甚至不必去体验“第一人称射击”的纷飞战火。

当你在屏幕前移动鼠标,旋转视角,向周围的世界好奇张望的时候,游戏的乐趣和精彩,已经足以在你心中埋下梦的种子。

  

【自由旋转视角】是一项十分常见的游戏功能;这项功能允许玩家移动鼠标来使视角转向不同的方向,以此支持各类游戏中的观望、射击和行进能力。这项功能描述起来只需寥寥数语,理解起来也非常容易——因为这项功能的体验,本身就与我们的生物本能高度一致。”自由视角“是游戏沉浸感最主要的来源之一。

然而,对于Unity游戏开发新手来说,这项功能却时常成为初学阶段的第一个”拦路虎“,甚至是”劝退“级别的高难度障碍。究其原因,一方面是Unity老生常谈的问题(缺乏”轮子“和蓝图),另一方面则是,这项功能所涉及的一些3D数学知识,确实难度较大且相对生僻难解。至于说,为什么一项在数学上不那么容易理解的功能反而是我们的生物本能?那……只能感慨自然造物的神奇了呀。

 

一.似易实难的数学大坑

玩家在游戏中旋转视角的过程,就是代表玩家的摄像机(Camera)不断发生姿态改变的过程。要想在游戏中模拟出自由视角的效果,就必须设法用数学语言描述出玩家的姿态和姿态变化,并根据玩家的操作进行适当的计算。

那么,什么是姿态?当玩家正在望向某个方向时,如何描述摄像机在空间中的姿态呢?

这是一个似易实难的问题——很多萌新都会产生一种典型的错误理解,我们来看一下问题是如何产生的。

 

如图是一个朴实无华的空间直角坐标系,x轴指向右边,y轴指向上方,Z轴指向远离自己的方向。

(此坐标系即为Unity使用的坐标系)

假设玩家(自己)处于该坐标空间中,且面朝Z轴正方向,那么此时,玩家正在注视的方向可以用空间向量表示为(0,0,1)。

于是,玩家在空间中的姿态可以用空间向量(0,0,1)来唯一确定。这样说对吗?答案是不对

想象一架在空中正常向前飞行的飞机。飞行员在座舱中向前观望,可以看到上方是天空,下方是地面,如图——

然后,假设飞行员开始做“倒飞”特技动作,让飞机上下颠倒过来继续飞行。此时飞行员视野中的景象应该是地面在上,天空在下。

此时,飞机飞行的方向没有改变,两种情况下的飞行方向的空间向量表示完全相同;但飞机的姿态和飞行员所看到的场景都是不同的。这两种姿态,显然不能认为是同一种姿态;假设玩家在游戏中想要正常向前走动,屏幕上却显示出了地面在上、天空在下的画面,那玩家肯定会抓狂的。

 

到这里,问题就体现出了它的严重性——要准确地对玩家的姿态进行数学描述,并不是那么容易的事情;高中和大学基础课程中的空间向量在这个问题中派不上用场。

二.偏转、俯仰和桶滚

那么,怎样正确地描述物体的空间姿态和姿态变化呢?我们需要引入一些全新的3D数学概念,来解决这个问题。

一个物体在三维空间中姿态的改变,可以被描述为3种基本行为:【偏转(yaw)】【俯仰(pitch)】和【桶滚(roll)】。任何形式的姿态改变,都可以被拆分为这3种行为的适当组合。

物体绕着竖直轴的旋转行为,称为【偏转】。

物体绕着水平(左右方向)轴的旋转行为,称为【俯仰】;

物体绕着前后方向轴的旋转行为,称为【桶滚】。

这些行为的含义是非常容易理解的,因为它们都可以在现实生活中找到典型的例子。

以下行为属于【偏转】:

·军训时的“向左转”“向右转”指令;

·汽车的左转弯和右转弯;

·水平放置的指南针,在被拨动后重新指向北方;

偏转示意图: 

以下行为属于【俯仰】:

·飞机的向上爬升和向下俯冲;

·驾驶汽车上坡和下坡;

·运动员投掷标枪后,标枪上升和下落时的姿态变化;

俯仰示意图:  

以下行为属于【桶滚】:

·飞机由正常飞行切换为倒飞姿态的过程;

·转动插入锁孔中的钥匙;

·子弹在发射时,在枪管内膛线的约束下作螺旋运动。

桶滚示意图: 

完成对以上三种行为的定义后,我们不难理解,玩家的各种姿态变化,都可以由这三种基本行为组合而成。

例如,玩家瞄准当前视野内左上方的敌人,这个过程中的姿态变化可以分解为:

玩家向左旋转一定角度;(偏转)

玩家抬高枪口以指向高处的敌人。(俯仰)

 

于是,我们可以尝试总结更加一般的情况,将游戏中”自由视角镜头“的姿态变化转化为数学语言。根据前面情境的经验,我们容易知道,玩家在正常走动和观望时,玩家摄像机发生的姿态变化由【偏转】和【俯仰】组成,而不会发生【桶滚】的变化。

(当玩家人物的姿态发生桶滚变换时,游戏中的天空不再位于人物视野的上方,地面不再位于人物视野的下方;这种情况多发生在基于游戏规则的特殊情境中,例如《守望先锋》中卢西奥的贴墙跑就是典型的【桶滚】情形。你只要找一张床并以侧卧姿势躺下,就可以在现实中模拟这个情形:此时在你的视野中,天花板和地板必然分别位于左右两侧。

实际上,《守望先锋》中的镜头并不会真的跟随卢西奥的眼睛进行【桶滚】变换,而且大多数类似游戏的处理都是如此。究其原因,许多玩家的平衡感根本无法承受这样的画面,可能导致呕吐或癫痫发作。

少了【桶滚】这个维度,那么玩家镜头的姿态变化,就可以用【偏转】和【俯仰】两个角度来描述。而玩家的鼠标支持前、后、左、右移动的行为,恰好也可以提供两个不同维度的正值、负值和0,这就为鼠标移动视角提供了数学上的可行性。

你可以打开任意一款FPS游戏来感受这个过程。不难发现,鼠标的向左、向右移动行为,对应了玩家姿态变化中的【偏转】;而向前、向后移动行为对应的则是玩家的【俯仰】。

具体地说,自由视角镜头对于玩家鼠标行为的响应规则是这样的:

当玩家的鼠标有向前移动趋势时,进行向上的俯仰;

当玩家的鼠标有向后移动趋势时,进行向下的俯仰;

当玩家的鼠标有向左移动趋势时,进行向左(逆时针)的偏转;

当玩家的鼠标有向右移动趋势时,进行向右(顺时针)的偏转。

三.在Unity中实现

·获取鼠标的移动趋势

Unity为我们提供了【输入轴】的功能,它使我们能够便捷地获取玩家鼠标在每一帧中的移动趋势。例如,当玩家向前推动鼠标一段距离再停下时,输入轴"MouseY"的值会在这段时间内,从0平缓地增加到某个最大值,然后平缓地回落到0。通过读取输入轴的值,我们就可以编写镜头控制代码,使得玩家摄像机的姿态对玩家的鼠标行为作出正确的响应,从而实现自由视角的功能。

·在Unity中访问物体的旋转角度

Unity使用Transform类来描述物体的位置、旋转和缩放信息,其中“旋转”有两种不同的表现形式,分别是四元数(Quaternion)欧拉角(EulerAngles)

【欧拉角】是较易理解的一种旋转表示方法,它通过物体的【俯仰】【偏转】【桶滚】角度(共3个角度值)来描述物体的旋转姿态。

在Unity中,Transform的第二组数据称为Transform.EulerAngles,也就是物体旋转状态的欧拉角表示形式;Transform.EulerAngles包含x,y,z三项数值,分别表示物体绕下图所示坐标系中x轴、y轴、z轴的旋转角,即俯仰角偏转角桶滚角

·编写代码

现在,我们终于有了足够的知识储备,可以编写代码来实现【自由视角】功能啦。

创建脚本SimpleFreelook.cs,内容如下:

using UnityEngine;public class SimpleFreelook : MonoBehaviour
{public Transform Camera;//应绑定为主摄像机public float yawSensitivity;//鼠标的偏转敏感度public float pitchSensitivity;//鼠标的俯仰敏感度;public float pitchLimit = 75;//俯仰的最大角度private void Update(){float yaw = Input.GetAxis("Mouse X") * yawSensitivity;//计算当前帧的偏转值float pitch = -Input.GetAxis("Mouse Y") * pitchSensitivity;//计算当前帧的俯仰值Vector3 rot = new Vector3(pitch, yaw, 0);//计算出当前帧中摄像机的欧拉角应当发生的改变量Vector3 afterRot;//开始计算摄像机在应用改变量后欧拉角的预期值afterRot.x = Mathf.Clamp(ClampAngle(Camera.localEulerAngles.x) + rot.x, -pitchLimit, pitchLimit);//俯仰角度需要进行约束,避免过度仰视/俯视而导致操作困难afterRot.y = Camera.localEulerAngles.y + rot.y;//偏转角度不需要设限afterRot.z = 0;//桶滚角度始终为0Camera.localEulerAngles = afterRot;//将摄像机的欧拉角设定为预期值}//在访问Unity提供的欧拉角数值时,需要先消除360°角周期的影响,将每个角度的数值约束在[-180,180]范围内,否则会对数学运算造成干扰。public float ClampAngle(float angle){if(angle <= 180 && angle >= -180){return angle;}while(angle > 180){angle -= 360;}while(angle < -180){angle += 360;}return angle;}
}

将脚本挂载到任意物体上,并将Camera字段绑定为主摄像机,即可实现第一人称自由视角的功能。

Tips:上面的代码所表示的算法大致与前面的理论知识一致,但还有两点重要的细节需要讲解。

(1) pitchLimit变量限制了玩家的最大俯仰角度为75度。一方面,这能够避免玩家因过度仰视和俯视而迷失方向,另一方面更可以避免俯仰角度超过90°导致玩家处于异常姿态。

例如,当玩家在90°仰视天空的状态下继续前推鼠标进行仰视,则玩家的视野会转向后方(并且是地面在上,天空在下),此时玩家人物的实际姿势相当于舞蹈动作中的“下腰”状态。

在绝大多数游戏中,这个姿势显然是不可能被允许的。

(2)在访问物体欧拉角的x/y/z数值时,Unity返回的读出值具有周期上的随机性;例如对于俯仰角x = 0,Transform.EulerAngle.x的读出值可能是-360,0,360中的一个。为了克服这个问题,上面的代码段中引入了ClampAngle方法,来将这样的角度值统一归拢到 [-180,180] 区间内。否则,将会产生运算错误。

(以下为难度较高的拓展内容,大家可以选择性了解)

四. 欧拉角和万向节锁

【四元数】和【欧拉角】这两项概念是非常难以在短时间内讲清的,对于很大一部分Unity开发者来说甚至永远都不必理解。

【四元数】是一种超复数,Unity中的四元数采用x,y,z,w四个数值来描述物体的旋转姿态。在实际应用中,这四个数值几乎不会表示任何几何意义,因此我们很少会在代码中直接使用四元数,而是多在Unity辅助运算的基础上,将四元数用于旋转过程的平滑插值;

【欧拉角】比四元数要简单许多,但它的缺点是会受到万向节锁(Gimbal lock)的影响,这会使得物体在面朝方向连续变化的过程中经历不连续的欧拉角取值。

#万向节锁:欧拉角的固有缺陷     

万向节锁是一项不易理解的3D数学概念,但在这里Victor可以给出一个例子由大家去体会。

打开任意一款FPS游戏,让人物面朝北方,向前推动鼠标,直到人物视角竖直指向天空。将天空看成一个高度为1的平面,此时你瞄准的天空坐标是(0,1,0)。

在这个时候,想象一架可疑飞机M从你瞄准的位置出发,向西南方飞行,你需要一直保持对它的瞄准。

尝试移动鼠标,来使瞄准点向天空的西南方向移动,或者说将瞄准点调整为M(-m,1,-m)(m≥0)。你会发现,无论怎样移动鼠标,都无法使瞄准方向立即朝天空中的西南方移动。

想要向点M进行瞄准,你必须向后移动鼠标来使视角下降,同时再向左移动鼠标,进行一个135°的逆时针大角度偏转,才能完成瞄准。

可疑飞机M(-m,1,-m)出发后,m值会从0开始平滑地增加,而人物瞄准动点M(-m,1,-m)的过程,在观感上是一段平滑的旋转过程。然而在这段过程中,m值从0变化到ε(Epsilon,即接近0的小正数)的瞬间,人物的偏转角会发生135°的突变。

这就是万向节锁的一个例子,它意味着物体在面朝方向连续变化的过程中可能经历不连续的欧拉角取值。

通过这个示例,或许你还能对前面的代码加深理解——为什么第一人称镜头的俯仰角度通常需要有限制?上面的例子其实已经展示了一个事实,即一个物体在俯仰角度达到90°时,在欧拉角的体系下会丢失部分旋转维度。也正是这一点,才导致玩家在俯仰角度过大时很容易“晕头转向”,难以控制镜头。

以上的缺陷,使得欧拉角在表示旋转时存在一定的局限性。当物体需要执行某些平滑旋转过程时,此类过程有时难以通过对欧拉角的控制来实现。

要实现类似的功能,可以改用四元数来控制物体的旋转,也可以使用DOTween等动画插件。

五.刨根问底

网上关于前一部分内容的讲解很多,但讲解程度几乎全都会止步于此,并且最终认为【万向节锁】是一项深奥难解,并且在实际应用中为害不小的隐患。但事实上,万向节锁是一个完全可防可控的问题,而不是什么玄学和不稳定性。(笑)

为什么用欧拉角旋转物体,会遇到欧拉角数值突然发生非平滑改变的情况?其实,大学阶段的高等数学教材已经给了我们答案——欧拉角体系可以看成是球面坐标系的变种。

在数学里,球坐标系(Spherical coordinate system)是一种利用球坐标(r,θ,φ)表示一个点 p 在三维空间的位置的三维正交坐标系。下图显示了球坐标的几何意义:原点到 P 点的距离 r ,原点到点 P 的连线与 z轴正方向之间的天顶角(仰角)θ,以及原点到点 P 的连线在 xOy平面的投影线与 x轴正方向之间的方位角φ。

 

 

图1:平面坐标系

是不是和欧拉角极为相似?

不难看出,球坐标系定义中的距离r与我们研究的旋转问题无关,而与天顶角θ类似的是EulerAngles.x,都表示俯仰维度;与方位角φ对应的则是EulerAngles.y,表示偏航维度。这两个维度可以确定一个物体的面朝方向,也就是上图中的向量O–>P。

(在以上两个维度的基础上,欧拉角多出了一个维度【桶滚(EulerAngles.z)】,这个维度的含义是物体在面朝方向确定前提下的自身旋转量。而传统数学中的射线O–>P是没有自身旋转之说的,故这个维度自然不会在球面坐标体系中出现。)

对于一个球心在原点,半径为1的球,球面上的绝大多数点,都拥有一个确定的球坐标(这里忽略角的周期性);但有两个点例外,这两个点正是球的上下两极。

当天顶角θ = 0或θ = π时,无论方位角φ怎么改变,点P对应的都是球的上下两个极点。而当点P从某个极点恰好移开时,P的方位角φ取值会从"任意值"突然变为"确定值"。很明显,欧拉角中的EulerAngles.y也会发生相同的情况。这就是万向节锁,或者说欧拉角的数值突变现象的真正来源。

到这里,我们就揭开了第4节中那个135°诡异旋转的谜团。当玩家的人物竖直望向天空时,其方位角(脚尖指向的方向)是任意的;而当玩家的视角不再竖直向上的瞬间,其方位角立刻被限制成了一个固定值(脚尖必须朝着西南方,才能正常瞄准)。玩家开始瞄准飞机之前,其脚尖的偏转角EulerAngles.y是初始值0(即脚尖朝北),而开始注视飞机之后的偏转角将被强制约束到-135°(即脚尖朝着西南方)。因此,玩家开始瞄准飞机的瞬间必须完成135度的偏转。

这样,我们终于可以得到使用欧拉角旋转物体时的最终结论:

对于一个Unity物体的某段旋转过程,如果其面朝的方向在某一瞬间将会是【正上方】或【正下方】,则在这一瞬间的前后,该物体的欧拉角的y值可能会发生数值突变。其它情况下,不会发生上述事件。

大功告成!通过这一结论和前面的内容,你应当可以准确地判断出,游戏中的某个旋转过程是否适合使用欧拉角进行运算,以及需要的话如何运算。

 

Published by

风君子

独自遨游何稽首 揭天掀地慰生平