
2.4 旋转炮塔
发现敌人!坦克旋转炮塔转向敌军,同时调整炮管方向瞄准目标。为了瞄准目标,坦克的炮塔可以左右旋转,炮管也可以上下滚动。炮塔和炮管及其运动规律如图2-25所示。

图2-25 炮塔和炮管及其运动规律
2.4.1 坦克的层次结构
如图2-26所示,调整Tank的层次结构,将坦克炮塔和炮管放到一个名为turret的空物体中,并使turret的原点位于炮塔的旋转中心(这里暂时将其他部件放在名为other的物体中)。

图2-26 Tank的层次结构
提示:如果使用的是其他坦克模型,原理也一样,可以导出工程文件中的坦克预设,跳过调整坦克层次结构的过程。
下面列出了本书附带坦克模型的调整步骤,读者应举一反三,并通过这次操作熟悉Unity3D的场景编辑。
1)在Tank下创建名为turret的方块,调整它的位置,使它位于炮塔的中心,如图2-27所示。

图2-27 创建名为turret的方块
2)如图2-28所示,将炮塔物件放到turret下,并改名为turretMesh。(改名不是必需的步骤,只是为了看起来协调一些。)添加turret的目的在于设置炮塔的旋转中心,后续会通过代码旋转turret,使炮塔旋转。

图2-28 将炮塔的物件放到turret下
3)如图2-29所示,在turret下添加名为gun的方块(可以适当缩小以便看清中心点),调整它的位置,使它位于炮管的旋转中心。与turret一样,添加gun的目的在于设置炮管的旋转中心,后续会通过代码旋转炮管。特别需要注意的是,gun的z轴(即蓝色表示的轴向)必须位于发射炮弹的方向(如果不是就旋转它),因为后续的代码会使用这个方向来发射炮弹。

图2-29 在turret下添加名为gun的方块
4)将炮管物件放到gun下面,并改名为gunMesh(改名不是必需的步骤),如图2-30所示。

图2-30 将炮管物件放到gun下面
5)拉长并调整gunMesh的位置,使它嵌入到炮塔里面,如图2-32所示。这一步不是必须的,但可以防止后续旋转炮塔后出现穿帮,如图2-31所示。

图2-31 旋转炮管后的穿帮现象

图2-32 拉长并调整gunMesh的位置,防止穿帮
6)调整gunMesh的角度,使炮管的方向与gun的z轴重合,如图2-33所示。这一步是也不是必须的,但调整后可以让子弹更像是从炮管里打出去的。

图2-33 使炮管的方向与gun的z轴重合
7)上面建立的立方体只是为了定位旋转中心。勾去turret和gun中Box Cillider和Mesh Renderer组件前面的方框,使它不被渲染出来,如图2-34所示。

图2-34 勾去turret和gun的碰撞体和渲染器
技巧:要设定空物体的坐标,可以先建立一个方块,调整后再勾去它的碰撞体和渲染组件。
2.4.2 炮塔
由于射击游戏的准心一般都在屏幕中心,因此可使炮塔朝着相机的方向转去(屏幕中心),直到两者y轴的角度重合(如图2-35所示)。由此,可以定义一个变量用于指明炮塔的目标角度,然后让炮塔不断往这个角度靠近。

图2-35 炮塔的目标方向
在Tank类中添加Transform类型的变量turret,并在Start中初始化它,使它指向炮塔。定义turretRotSpeed代表炮塔的旋转速度;turretRotTarget代表炮塔的目标角度,即炮塔最终会停在哪个方向,代码如下所示。
//炮塔 public Transform turret; //炮塔旋转速度 private float turretRotSpeed = 0.5f; //炮塔目标角度 private float turretRotTarget = 0;
在Tank类的Start方法中,通过transform.FindChild(因为该脚本附加在坦克模型上,transform即代表坦克身上的Transform组件)查找子物体turret,使turret变量指向炮塔。
//开始时执行 void Start() { //获取炮塔 turret = transform.FindChild("turret"); }
相机方向随着鼠标的移动不断地发生变化,在Update方法中更新相机的目标角度,使它等同于相机方向,代码如下所示。
void Update()
{
//旋转
……
//前进后退
……
//炮塔角度
turretRotTarget = Camera.main.transform.eulerAngles.y;
}
至此,程序已经获取了炮塔物体、知道了目标角度,接着便需要让炮塔朝着目标角度转动,编写TurretRotation方法,实现炮塔的旋转功能。
图2-36下面的代码中,float型变量angle代表相机与炮塔的角度差,因为角度的取值范围是0到360°,所以angle的取值范围是-360°到360°(如∠A为0°, ∠B为360°,角度差为-360°; ∠A为360°, ∠B为0°,角度差为360°)。因为角度是间隔360°的循环(如0°等同于360°, -30°等同于330°),因此可以通过if(angle < 0) angle+=360将角度差的范围控制在0°到360°之内。之后只需根据角度差判断炮塔应该向左转还是向右转(如图2-36所示,小于180°为左转,大于为右转),让它往正确方向旋转即可。

图2-36 目标角度差与炮塔旋转方向的关系
//炮塔旋转 public void TurretRotation() { if (Camera.main == null) return; if (turret == null) return; //归一化角度 float angle = turret.eulerAngles.y - turretRotTarget; if (angle < 0) angle += 360; if (angle > turretRotSpeed && angle < 180) turret.Rotate(0f, -turretRotSpeed, 0f); else if (angle > 180 && angle < 360 - turretRotSpeed) turret.Rotate(0f, turretRotSpeed, 0f); }
最后在Update方法中调用TurretRotation方法,实现炮塔的转动(如图2-37所示)。

图2-37 炮塔朝着屏幕中心的方向转动
2.4.3 炮管
与炮塔相似,在Tank类中定义Transform类型的gun指向坦克炮管。由于炮管的旋转范围有限(如图2-38所示),因此需要对其进行限定,定义maxRoll和minRoll代表炮管的旋转范围。

图2-38 炮管的旋转范围
代码如下所示。
//炮管 public Transform gun; //炮管的旋转范围 private float maxRoll = 10f; private float minRoll = -4f;
在Start方法中,通过transform.FindChild查找子物体gun,使gun变量指向炮管,代码如下所示。
void Start() { //获取炮塔 turret = transform.FindChild("turret"); //获取炮管 gun = turret.FindChild("gun"); }
和炮塔一样,turretRollTarget代表炮管的目标角度,定义代码如下所示。
//炮塔炮管目标角度 private float turretRotTarget = 0; private float turretRollTarget = 0;
在Update中添加如下代码,使turretRollTarget等于相机的x轴旋转角度。
//每帧执行一次 void Update() { …… //炮塔炮管角度 turretRotTarget = Camera.main.transform.eulerAngles.y; turretRollTarget = Camera.main.transform.eulerAngles.x; }
既然已经获取了炮管、也知道了目标角度,接着便需要让炮管转到指定的角度。由于炮管的转动角度较小,因此可以让它直接转到所需的角度,而不是像炮塔一样缓慢地旋转(当然,也可以仿照炮塔的旋转方法,让炮管缓缓转动)。
编写TurretRoll方法,根据目标角度、maxRoll和minRoll确定炮管的旋转角度。因为炮管架设在炮塔之上,所以maxRoll和minRoll是相对于本地坐标系的角度限制,这样一来,就需要先计算炮管在世界坐标系的角度,再转换成本地坐标系计算角度范围,如图2-39所示。

图2-39 计算炮管角度的流程
代码如下所示。

最后在Update中调用该方法,运行游戏后炮管会随着鼠标上下移动。实现效果如图2-40所示。如果角度太小不容易看出来,可以查看gun的Transform属性,看看旋转角度是否发生了变化。

图2-40 不同角度的炮管