在PC游戏里,第三人称视角的操纵是通过鼠标;而在主机上,则是通过右摇杆。本节实现的是类似于右摇杆操纵的第三视角,不过不是右摇杆,而是键盘的4个方向键,你问我为什么不实现鼠标操纵视角?因为它用到四元数(Quaterninos),比较复杂,日后再去研究。
如何把camera放在第三人称视角?这其实不难,在PlayerHandle底下创建一个子的empty,命名为CameraHandle,把这个CameraHandle的位置抬到角色的脖子处,然后在往后拉到适当的地方,把已经存在的Camera拖到CameraHandle底下,那么一个标准的第三人称视角相机就ok了:
由于Camera与PlayerHandle的父子关系,就能实现相机跟着角色走:
但是这还不够,我们要做到的是使用方向键控制视角的变化。思路就是先在PlayerInput.cs文件里定义好控制视角变化的按键变量,并在Camera下新增一Component来实现按键与相机之间的联系。
所以第一步就是在PlayerInput.cs里定义四个控制相机方向的变量和两个坐标轴变量(跟角色行走一样的做法,不再赘述)。
//负责Camera移动
[Header("===== Camera Move =====")]
public string keyCUp = "up";
public string keyCDown = "down";
public string keyCLeft = "left";
public string keyCRight = "right";
public float CUp;
public float CRight;
与角色行走如出一辙,以xy坐标轴的方式将这4个string变量与这两个float变量联系起来:
CUp = (Input.GetKey(keyCUp)?1.0f:0) - (Input.GetKey(keyCDown)?1.0f:0);
CRight = (Input.GetKey(keyCRight)?1.0f:0) - (Input.GetKey(keyCLeft)?1.0f:0);
第二步就是在Camera里新增组件,命名为CameraController:
在里面,我们要做的是,将
CUp
与CRight
与相机的旋转欧拉角联系起来。
而对于相机的旋转,我打算通过水平旋转角色胶囊体来水平旋转相机;通过垂直旋转CameraHandle来垂直旋转相机,这样做的好处就是能分开处理两个不同平面的旋转。
为此,我们需要在CameraController里获得cameraHandle和PlayerHandle,因为需要控制它们的旋转角,就需要获得这两个GameObject:
private GameObject cameraHandle;
private GameObject playerHandle;
public PlayerInput pi;
void Awake () {
cameraHandle = transform.parent.gameObject; //负责垂直旋转
playerHandle = cameraHandle.transform.parent.gameObject; //负责水平旋转
}
从上面的GameObject关系图可以看到,Camera和CameraHandle是父子关系,而CameraHandle和PlayerHandle也是父子关系,所以要在Camera下获得CameraHandle就要通过Camera的transform.parent.gameOeject
来获得它的爸爸,而PlayerHandle就要通过CameraHandle的transform.parent.gameOeject
获得。
另外,为了获得CUp
和CRight
,就要通过外部传递的方式获得PlayerHandle下的PlayerInput组件:
直接把PlayerHandle这个GameObject拖到这个框下,就会自动筛选出PlayerInput组件。
Ok,万事俱备,只差一个函数的调用,那么什么函数能旋转游戏里的物体呢?那就是
transform.Rotate()
,这个函数有多个Signature,我们要用到是
public void Rotate(Vector3 axis, float angle, Space relativeTo = Space.Self);
这一个。
第一个参数(Vector3 axis)就是旋转的方向,要注意的是它这个方向指定的是绕哪个方向向量旋转,而不是向哪个方向旋转;第二个参数(float angle)是旋转的角度,这不难理解;第三个参数(Space relativeTo)是指定它在哪个坐标系下旋转,有
Space.World
和Space.Self
两个选择,Space.World
指的是物体在世界坐标系下旋转,而Space.Self
指的是物体在自身坐标系下旋转,至于这两个坐标系有什么区别,日后有机会可以详谈一下,这里用它的默认值就行,就是Space.Self
。
好,现在就用这个函数来实现水平旋转试试看:
public float horizontalSpeed = 20.0f;
void Update () {
playerHandle.transform.Rotate (Vector3.up, pi.CRight * horizontalSpeed * Time.deltaTime);
}
水平旋转绕的是y轴,那就是Vector3.up
的方向;而horizontalSpeed
为的是提高它旋转的速度和找到一个适当的旋转速度;乘上Time.deltaTime
就是要使得这个旋转的过程是渐进和连续的,我试过去掉Time.deltaTime
,结果就是相机疯狂大角度旋转,而且是瞬移式的旋转,令人晕头转向。
可以看看效果如何:
那么我们是否可以故技重施,用这个函数实现垂直旋转呢?如果对垂直旋转的角度没有限制的话,这是可以的;如果有限制,那这个方法恐怕不能奏效,为什么?我们可以试试看:
public float horizontalSpeed = 20.0f;
public float verticalSpeed = 20.0f;
void Update () {
playerHandle.transform.Rotate (Vector3.up, pi.CRight * horizontalSpeed * Time.deltaTime);
cameraHandle.transform.Rotate (Vector3.right, pi.CUp * verticalSpeed * Time.deltaTime);
}
显然这不是不对的,我们需要把垂直旋转限制在某一角度范围内,不能让角色消失在电脑屏幕中。
我们先找一个合适的角度区间:-20到30就感觉可以了
那么怎么把旋转角限制在某个区间呢?首先,我们要找到表示物体旋转角的变量,那就是
transform.eulerAngles
,eulerAngels就是欧拉角的意思,三维空间里物体的旋转角都可以用三个欧拉角来表示,所以这是个Vector3变量(public [Vector3] eulerAngles;
),分别代表绕x/y/z轴旋转x/y/z度,下面是官网的描述:
Description
The rotation as Euler angles in degrees.
The x, y, and z angles represent a rotation z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis.
Only use this variable to read and set the angles to absolute values. Don't increment them, as it will fail when the angle exceeds 360 degrees. Use Transform.Rotateinstead.
其次,我们需要把一个变量限制在某个范围内,那么
Mathf.Clamp()
能帮上忙,它的Signature有两个,分别是对应float类型和int类型,显然我们需要float类型:public static float Clamp(float value, float min, float max);
,它的功能是:
Description
Clamps a value between a minimum float and maximum float value.
把一个值限制在一个最小浮点值和最大浮点值之间。
因为我们要限制的是绕x轴旋转的角度,所以只对transform.eulerAngles
的x分量做限制:
void Update () {
playerHandle.transform.Rotate (Vector3.up, pi.CRight * horizontalSpeed * Time.deltaTime);
cameraHandle.transform.Rotate (Vector3.right, pi.CUp * verticalSpeed * Time.deltaTime);
cameraHandle.transform.eulerAngles = new Vector3 (
Mathf.Clamp (cameraHandle.transform.localEulerAngles.x, -20, 30),
0,0);
}
ok看看效果如何,
你可能会想,啊这什么玩意儿,晃得我头晕。这个图可能有点帧数不够(我减少帧数了,避免文件太大),所以你可能看不太明白发生了什么,我来解释一下:现在相机垂直旋转能很好的限制在30度(不能大于30度),但却不能去到负值的度数,你可以看到这个X并没有出现负值,而在它到达0度时却回到了30度,所以造成了这种上下抖动的现象。为什么会这样呢?为什么没有负值出现?我们或许可以在
transform.eulerAngles
的官方描述中找到一点端倪,在上面我已经给到关于它的描述,里面有这么一句说明:
Only use this variable to read and set the angles to absolute values. Don't increment them, as it will fail when the angle exceeds 360 degrees.
只能读写这个数值的绝对值,且当角度超过360度时不要去增加它们,这样做会失败。这个就是部分人对transform.eulerAngles
的误区,虽然在面板上它确实能显示负值,但实际上它的内部代码实现的欧拉角范围是0-359,并没有负值,例如面板上显示-1,实际上内部表示为359。
据此这个上下抖动就能解释得通了,在角度刚到负值时(例如-1),而它内部表示其实是359,但是现在这个transform.eulerAngles.x
被Clamp了在-20到30了啊,它不能大于30啊,所以此刻它只能赋值为30。当初我想通这里的时候,真的有种醍醐灌顶的感觉。
那么不能故技重施,那要怎么实现垂直旋转的限制呢?虽然这个transform.eulerAngles
它不能连续增加直至超过360,但它是能接受负值的,它会自动转回它内部表示的正数。照这么说,虽然我们不可以限制transform.eulerAngles
在一个有负值的范围内,但可以将这个限制作用于一个临时变量,而且玩家操纵视角的变化也作用于这个临时变量,然后再把这个临时变量赋给transform.eulerAngles
,这样就可以实现垂直旋转的限制了。
private float tempEuler;
void Update () {
playerHandle.transform.Rotate (Vector3.up, pi.CRight * horizontalSpeed * Time.deltaTime);
//cameraHandle.transform.Rotate (Vector3.right, pi.CUp * verticalSpeed * Time.deltaTime);
tempEuler = Mathf.Clamp (tempEuler, -20, 30);
tempEuler += pi.CUp * -verticalSpeed * Time.deltaTime;
cameraHandle.transform.localEulerAngles = new Vector3 (tempEuler, 0, 0);
}
因为将这个Clamp作用于tempEuler,并不会出现-1回到359而再回到30的情况,所以这个-1会确确实实赋值给cameraHandle.transform.localEulerAngles
,虽然它的内部表示是359,但是这两者的实际效果是一样的。
至于什么使用transform.localEulerAngles
而不是刚才的transform.eulerAngles
,是因为我要改变的是它自己在父层坐标系的旋转角,即cameraHandle相对于PlayerHandle的旋转角,所以用的是local。
另外一个需要注意的是在改变tempEuler
时我对verticalSpeed
取了负号,是因为原本是镜头垂直旋转的方向与玩家按的方向键是相反的(即键入↑,相机往下转),所以要取反处理。
现在可以看看效果:
现在可以看到,相机的垂直旋转角度被很好的限制在了(-20, 30)这个区间内。
现在的总体效果是:
现在这个第三人称视角的实现只能说是差强人意,并不是最好的。可以看到,现在当我旋转相机,人物会一起旋转,这并不是我想要,因为我只想操纵视角,并不改变人物的方向。下节将会解决这个问题,并且制造出一种人物移动时,相机在追赶人物的效果(即相机并不是固定在与人物有一段距离的某个点上)。