Space Shooter开发全过程记录02
上一节准备了一些场景素材,本节要实现键盘控制飞船行动,射击和敌人(障碍物)。
##2.编写脚本代码
###2.1.键盘控制飞船移动
创建脚本文件夹。在Project视图中选中Assets文件夹,然后Create->Folder,创建文件夹”_Scripts”,之后的所有脚本文件都放在该文件夹中。
在_Scripts文件夹中新建一个C#脚本PlayerController.cs,输入下列代码
void FixedUpdate() {
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3 (moveHorizontal, 0.0f, moveVertical);
GetComponent<Rigidbody> ().velocity = movement;
}
}
选中PlayerController.cs,拖动到Hierarchy视图的Player对象上,完成绑定。
运行游戏,方向键可以控制飞船运动,但有三个不足
- 飞船移动速度过慢
- 飞船运动没有范围限制,可以飞出Viewport
- 左右移动飞船,没有倾斜效果
接下来依次解决三个问题
####2.1.1飞船移动速度慢
在PlayerController类中添加一个成员变量Speed,为了方便,设置初值为5.0f 。
public class PlayerController : MonoBehaviour {
public float speed = 5.0f;
···
}
修改FixedUpdate函数,将刚体的velocity属性设置为:
GetComponent<Rigidbody> ().velocity = movement * speed;
再运行游戏,可以发现飞船运动明显变快了。
####2.1.2.添加运动范围限制
1. 在PlayerController.cs中,在PlayerController类前面声明Boundary类,用于管理飞船活动的边界值。
public class Boundary {
public float xMin, xMax, zMin, zMax;
}
public class PlayerController : MonoBehaviour {
···
}
2. 在PlayerController类中添加一个Boundary类的实例,访问权限为public:
public Boundary boundary;
要将物体位置固定在一个范围内,可以使用Unity提供的Mathf.Clamp函数来实现:
static float Clamp(float value,float min,float max)
若value小于min则返回min;若value大于max,则返回max。
然后更新FiexdUpdate函数,如下:
void FixedUpdate() {
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3 (moveHorizontal, 0.0f, moveVertical);
//GetComponent<Rigidbody> ().velocity = movement * speed;
Rigidbody rb = GetComponent<Rigidbody> ();
if (rb != null) {
rb.velocity = movement * speed;
rb.position = new Vector3(
Mathf.Clamp(rb.position.x, boundary.xMin, boundary.xMax),
0.0f,
Mathf.Clamp(rb.position.z, boundary.zMin, boundary.zMax)
);
}
}
然后为Boundary类添加可序列化的属性,入下所示。
[System.Serializable]
public class Boundary {
···
}
然后返回Inspector视图,可以看到Player的Boundary现在可以修改了,设置Boundary的值为(-6.0f,6.0f,-4.0f,14.0f).
此时再运行游戏,飞船将不会飞出屏幕范围。
####2.1.3添加移动旋转效果
在PlayerController类中添加一个角度倾斜系数,默认设置为4.0f:
public float tilt = 4.0f
修改FixedUpdate函数,添加如下语句
void FixedUpdate() {
···
if (rb != null) {
···
rb.rotation = Quaternion.Euler(0.0f, 0.0f, rb.velocity.x * -tilt);
}
}
运行游戏,左右移动时,飞船出现一定角度的倾斜。
###2.2.实现射击行为
####2.2.1.创建电光子弹
1. 创建一个空的游戏对象,命名为Bolt,重置Transform组件。为Bolt添加一个Rigidbody组件,并取消勾选Use Gravity。
2. 新建一个Quad对象,重命名为VFX,将其设置为Bolt的子对象,重置其Transform组件。设置Transform的Rotation属性为(90,0,0),并移除Mesh Collider组件。
3. 将Project视图Assets/Materials目录下的材质对象fx_bolt_orange拖动到VFX上。
4. 为Bolt添加一个Capsule Collider(胶囊碰撞体)组件,勾选Is Trigger选项,将其设置为一个触发器。碰撞体要挂在Bolt而不是子对象VFX上,否则可能无法完全销毁Bolt。之后设置Capsule Collider的Direction属性为Z-Axis,并设置Radius为0.04,Height为0.65.
5. 在Project视图Assets/_Scripts目录下新建一个脚本Mover.cs,如下
using UnityEngine;
using System.Collections;
public class Mover : MonoBehaviour {
public float speed;
// Use this for initialization
void Start () {
GetComponent<Rigidbody>().velocity = transform.forward * speed;
}
}
在Inspector中设置Bolt的Speed为20
6. 在Project视图的Assets目录下新建一个子目录_Prefabs,用于存储预制体。将对象Bolt从Hierarchy视图拖动到_Prefabs目录下,创建一个新的预制体Bolt。之后可以将Hierarchy视图中的Bolt删除。
7. 运行游戏,将Bolt预制体拖动到Hierarchy视图中,可以看到一个电光子弹飞速向前运动。
至此,电光子弹预制体基本制作完成,还剩两个问题
- 电光子弹应该被键盘或者鼠标控制发射
- 随着发射的子弹越来越多,不会自动销毁。在真实游戏中,数量巨大的子弹必定会消耗非常多的系统资源,影响游戏性能
#####2.2.2.脚本控制发射子弹
1. 为Player新建一个空的子对象Shot Spawn,设置其Position的值为(0,0,0.7),将在此位置发射子弹
2. 双击脚本PlayerController.cs进行编辑。为了实现Fire1触发后即刻实例化Bolt预制体,需要:
- 存储传入Bolt变量,作为Instantiate的第一个参数
- 存储发射器的位置,作为实例化Bolt的位置
- 设置一定的发射频率,只有间隔了一定时间后才继续发射,否则当用户持续按Fire1的时候,会连续的发射子弹。
为PlayerController加入以下变量
public float fireRate = 0.5f;
public GameObject shot;
public Transform shotSpawn;
public float nextFire = 0.0f;
- fireRate表示发射间隔,默认0.5s
- shot表示Bolt预设体
- shotSpwan是Transform类型,这样拖动GameObject类型变量到Inspector上时,会自动提取对象的Transform属性赋值给变量
- nextFire表示下次射击的最早时间
之后修改PlayerController的Update方法:
void Update () {
if (Input.GetButton ("Fire1") && Time.time > nextFire) {
nextFire = Time.time + fireRate;
Instantiate(shot, shotSpawn.position, shotSpawn.rotation);
}
}
3. 为变量赋值,在Hierarchy中选择Player对象,然后将Project中的Bolt预制体拖动到Inspector视图中对应的Bolt变量上;拖动Hierarchy视图中Player下的Shot Spawn子对象到Inspector上对应的shotSpawn变量上
4. 运行游戏,方向控制移动,鼠标左键或者左【Ctrl】键发射子弹。
####2.2.3.管理电光子弹的生命周期
1. 在Hierarchy视图中添加一个Cube对象,命名为Boundary,重置其Transform组件。
2. 调整Boundary的X、Z值为0和5,使Boundary位于游戏区中心
3. 设置Boundary的Scale属性值为(15,1,20)
4. 设置Boundary的Box Collider组件为触发器。由于不需要显示Boundary,移除Mesh Renderer组件
5. 在Project视图中Assets/_Scripts的目录下新建一个脚本DestroyByBoundary.cs,并将其拖到Boundary对象上。
6. 打开脚本DestroyByBoundary.cs脚本,添加如下事件响应函数:
void OnTriggerExit (Collider other) {
Destroy (other.gameObject);
}
7. 运行游戏,在Hierarchy视图中可以发现,当子弹飞出场景后,自动消除了
###2.3.添加小行星(Asteroid)
要实现的目标:
- 小行星随机产生,且以随机的角度旋转
- 当飞船发射子弹击中小行星,小行星会爆炸并销毁
- 若飞船碰撞上小行星,则飞船爆炸,游戏结束
####2.3.1.创建小行星对象
1. 在Hierarchy视图中新建一个空的游戏对象Asteroid,重置其Transform组件。设置其Position为(0,0,10)。添加Rigidbody组件,取消勾选Use Gravity选项框。添加Capsule Collider组件,勾选Is Trigger选项。
2. 从Project视图中Assets/Models的目录下拖动模型prop_asteroid_01到Asteroid上,是之成为Asteroid的子对象。
3. 为了使碰撞体(Capsule Collider)更接近模型的几何体形状,选中Asteroid后在Inspector视图中调整碰撞体的属性值。设置Radius的值为0.5,Height的值为1.6,Direction的值为Z轴
####2.3.2.添加小行星随机旋转的功能
1. 在Project视图中Assets/_Scripts目录下新建一个脚本RandomRotator.cs,并将其绑定到Asteroid对象上。
2. 在脚本中添加一个表示小行星旋转系数的变量tumble,再重载Start函数,为刚体组件的角速度赋上随机值。
public float tumble = 10.0f;
void Start () {
GetComponent<Rigidbody> ().angularVelocity =
Random.insideUnitSphere * tumble;
}
angularVelocity表示刚体的角速度,在3D图形学中,角速度描述的是做圆周运动的物体单位时间转过的角度。Random.insideUnitSphere表示单位长度半径球体内的一个随机点(向量),这个随机点与旋转系数tumble的乘积描述了在半径长度为tumble的球体内的随机点,由此就可以实现刚体以一个随机的角速度旋转。
3. 运行游戏,发现小行星在飞船前方随机旋转。
但仔细观察会发现,旋转越来越慢,这是因为刚体设置了角阻力(angular drag),为了使小行星不受影响,把Asteroid对象Rigidbody中Angular Drag的值设置为0
####2.3.3.添加控制射击小行星的功能
1. 在Project试图中Assets/_Scripts目录下新建脚本DestroyByContact.cs,并绑定到Asteroid上
2. 在脚本中加入事件函数OnTriggerEnter,并在函数中销毁Asteroid对象(即下面脚本追踪的gameObject)和与Asteroid发生碰撞的对象(即other.gameObejct)
void OnTriggerEnter (Collider other) {
Destroy (other.gameObject);
Destroy (gameObject);
}
3. 运行游戏,飞船没有射击,小行星也自动销毁了,原因是小行星和Boundary发生了碰撞
4. 在Hierarchy试图选中Boundary对象,在Inspector中指定其Tag为Boundary(若该Tag不存在则自行创建)
5. 在DestroyByContact.cs的碰撞事件处理函数中加入以下代码
void OnTriggerEnter (Collider other){
if (other.tag == "Boundary")
return;
···
}
6. 运行游戏,发现击中小行星后,子弹和小行星都销毁;飞船与小行星碰撞时,两者也都销毁
####2.3.4.添加小行星爆炸时的效果
1. 打开脚本DestroyByContact.cs,添加量过变量
public GameObject explosion;
public GameObject playerExplosion;
- explosion表示小行星碰撞后爆炸时的粒子对象
- playerExplosion表示飞船与小行星碰撞飞船爆炸时的粒子对象
2. 在碰撞事件函数中加入以下代码
void OnTriggerEnter (Collider other){
···
Instantiate (explosion, transform.position, transform.rotation);
if (other.tag == "Player") {
Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
···
}
3. 在Hierarchy试图中选择对象Player,指定其Tag为Player
4. 从Project视图Assets/Prefabs/VFX的目录下,拖动explosion_asteroid到变量explosion上,拖动explosion_Player到变量player Explosion上,完成赋值。
5. 运行游戏,可以看到子弹击中小行星爆炸效果和飞船撞击小行星的爆炸效果。
####2.3.5.添加控制小行星运动的功能
1. 将Project视图Assets/_Scripts下的脚本Mover.cs拖动到Asteroid对象上,在Inspector视图中设定Speed的值为-5。
2. 运行游戏,小行星向着飞船飞过来,飞出边界时自动销毁。
####2.3.6.添加小行星随机产生的逻辑功能
1. 把Asteroid对象从Hierarchy视图拖动到Project视图的Assets/Prefabs目录下,生成预制体Asteroid,然后从Hierarchy视图中将其删除。
2. 在Project视图创建一个空的游戏对象GameController,重置其Transform组件,指定其Tag为GameController(若不存在该名称Tag则自行创建)
3. 在Assets/_Scripts目录下新建GameController.cs脚本,拖动到GameController上
4. 在脚本中输入以下代码
public GameObject hazard;
public Vector3 spawnValues;
private Vector3 spawnPosition = Vector3.zero;
private Quaternion spawnRotation;
void SpawnWaves() {
spawnPosition.x = Random.Range (-spawnValues.x, spawnValues.x);
spawnPosition.z = spawnValues.z;
spawnRotation = Quaternion.identity;
Instantiate (hazard, spawnPosition, spawnRotation);
}
void Start () {
SpawnWaves ();
}
在上面的代码中
- hazard对象是准备实例化的障碍物对象,这里指Asteroid预制体
- spawPosition表示实例化hazard对象的位置
- Quaternion.identity表示无旋转
- SpawnWaves函数用于在X轴方向上随机实例化hazard对象
5. 在Inspector视图中为各个变量赋值
- 将Asteroid预制体从Project拖动到Hazard变量上
- Spawn Values设置为(6,0,14.5)(也是Asteroid在最右上角的情况)
####2.3.7.添加小行星批量产生功能
1. 打开脚本GameController.cs,加入变量hazardCount,用来表示小行星的数量
public int hazardCount;
2. 修改函数SpawnWaves如下
for (int i = 0; i < hazardCount; ++i) {
spawnPosition.x = Random.Range (-spawnValues.x, spawnValues.x);
spawnPosition.z = spawnValues.z;
spawnRotation = Quaternion.identity;
Instantiate (hazard, spawnPosition, spawnRotation);
}
3. 在Inspector视图中设定hazardCount的值为10
4. 运行游戏,发现同时生成的小行星很多,而且数量近,有些小行星相互碰撞直接销毁了
要解决这个问题,可以是每个小行星生成后等待一段时间,Unity中提供协程类WaitForSeconds可以实现这样的功能
5. 在GameController.cs中添加一个变量spawnWait用于表示每次生成小行星对象后延迟的时间,单位为妙
public float spawnWait;
6. 修改SpawnWaves返回值类型为IEmunerator,然后用yield return语句返回WaitForSeconds的实力,修改代码如下
IEnumerator SpawnWaves() {
for (int i = 0; i < hazardCount; ++i) {
···
yield return new WaitForSeconds(spawnWait);
}
}
7. 修改Start中SpawnWaves的调用方法
void Start () {
StartCoroutine (SpawnWaves ());
}
8. 在Inspector中设定spawnWait的值为0.5
9. 由于不希望一开始就立刻产生小行星,而是等待一段时间,在GameController.cs中再加入一个变量startWait,用来表示开始生成小行星对象前等待的时间
public float startWait = 1.0f;
10. 修改函数SpawnWaves,在for循环之前加上下列语句,表示等待startWait秒
yield return new WaitForSeconds (startWait);
11. 在真实游戏中,小行星应该不断产生,直到游戏结束,在GameController.cs中再加入一个变量waveWait,表示两批小行星阵列间的时间间隔
public float waveWait = 2.0f;
修改SpawnWaves的代码,加入无限循环功能
IEnumerator SpawnWaves() {
yield return new WaitForSeconds (startWait);
while (true) {
for (int i = 0; i < hazardCount; ++i) {
···
}
}
yield return new WaitForSeconds(waveWait);
}
12. 运行游戏,发现不断生成小行星,但随着游戏进行,小行星的爆炸粒子效果一直没有销毁
13. 在Project视图的Assets/_Scripts目录下新建DestroyByTime.cs脚本,并绑定到粒子效果explosion_asteroid和explosion_player上,其代码为:
public class DestroyByTime : MonoBehaviour {
public float lifetime = 2.0f;
void Start () {
Destroy (gameObject, lifetime);
}
}
14. 运行游戏,发现爆炸粒子效果过一会后自行销毁