对 Unity 太空射击游戏的实践

写在前面 #

本次 Space Shooter 实践通过实现以下功能达到加深对 U3D 游戏开发的认知.

  • 键盘控制飞船移动;
  • 发射子弹设计目标;
  • 随机生成大量障碍物;
  • 计分;
  • 实现游戏对象的生命周期管理;

同时进一步练习场景元素的编辑, 脚本文件的创建和 GUI 的处理, 以及音频文件的添加等方法.

最终效果:

1. 导入模型,贴图和材质 #

步骤要注意的几点 :

导入的资源包中有可以正确运行已做好的 Done_Main 场景, 将其删除, 创建一个全新的空场景文件 Main, 实践复原 Done_Main 的功能;

将 File>>Build Settings>>Player Settings>>Default Is Full Screen 取消勾选, 宽高设置为 400x600;

飞船模型拖至 Hierarchy 命名为 Player, Reset Transform 组件;

添加 Rigidbody, 不希望飞船受重力影响而下坠, 取消勾选 Use Gravity 选项;

添加碰撞体组件 Mesh Collider, 这是一个网格碰撞体, 使飞船能够与随机出现的障碍物发生碰撞, 并在碰撞后触发销毁飞船和障碍物的事件, Mesh Collider 的 Mesh 属性为模型 vehicle_playerShip 的网格, 该网格模型包含许多细小的三角形面片

为了提高游戏的执行效率, 飞船网格模型不应该过于复杂, 不必进行如此精确的碰撞检测, 应该建立一个简化的模型, 减少不必要的碰撞计算;

最后还要勾选 Convex 和 Is Trigger 选项框, 将 Mesh Collider 设置为触发器, 如图;

添加飞船尾部的火焰粒子效果, 要是 Player 的子对象;

使摄像机正对着飞船, Rotation(90,0,0). 使飞船处于 Viewport 视图窗口的下半部分, Position(0,10,4). 摄像机为正交投影;

添加背景图片, GameObject>>3D Object>>Quad 创建一个平面命名为 Background, 移除 Mesh Collider, 此时垂直于飞船;(Quad 默认情况下为背向剔除模式, 因此可能需要调整视角才能看到 Quad 平面) Quad 的 Position(90,0,0);

设置 Background 的纹理图片 Shader 模式为 Unlit/Texture;

为背景添加粒子效果繁星点点;

至此动图效果:

2. 编写脚本代码 #

2.1 控制飞船移动 ##

PlayerController.cs 实现方向键控制飞船移动的功能;

 using UnityEngine;
 using System.Collections;

 public class PlayerController : MonoBehaviour
 {
 	// 想在 Inspector 视图显示, 就需要为 Boundary 类添加可序列化的属性 [System.Serializable]
 	[System.Serializable]
 	public class Boundary
 	{
 		// 用于管理飞船活动的边界值, XZ 平面
 		public float xMin, xMax, zMin, zMax;
 	}

 	// 速度控制变量
 	public float speed;
 	public Boundary boundary;
 	// 飞船倾斜系数
 	public float tilt = 4.0f;

 	void FixedUpdate ()
 	{
 		// 得到水平方向输入
 		float moveHorizontal = Input.GetAxis ("Horizontal");
 		// 得到垂直方向输入
 		float moveVertical = Input.GetAxis ("Vertical");
 		// 用上面的水平方向和垂直方向输入创建一个 Vector3 变量, 作为刚体速度, 是一个矢量
 		Vector3 movement = new Vector3 (moveHorizontal, 0.0f, moveVertical);
 		Rigidbody rb = GetComponent<Rigidbody> ();
 		if (rb != null) {
 			rb.velocity = movement * speed;
 			// Mathf.Clamp 限定刚体的活动范围
 			rb.position = new Vector3 (
 				Mathf.Clamp (rb.position.x, boundary.xMin, boundary.xMax),
 				0.0f,
 				Mathf.Clamp (rb.position.z, boundary.zMin, boundary.zMax)
 			);
 			// 飞船左右移动时有一定的倾斜效果,
 			// 绕 Z 轴旋转, 往左运动 X 轴上速度为负值, 旋转的角度为逆时针正值, 所以要乘以一个负系数
 			rb.rotation = Quaternion.Euler (0.0f, 0.0f, rb.velocity.x * -tilt);
 		}

 	}
 }

至此动图效果为

2.2 实现射击行为 ##

步骤需要注意的几点

新建立一个空的游戏对象 Bolt, 添加 Rigidbody 取消勾选 Use Gravity 选项框.

为 Bolt 新建一个子对象 Quad 命名为 VFX, Rotation(90,0,0), 移除 Mesh Collider, 添加材质 fx_bolt_orange.

为 Bolt 添加一个胶囊碰撞体, 勾选 Is Trigger 设为触发器, 设置 Capsule Collider 的 Direction 属性值为 Z-Axis, 设置半径和高度.

为 Bolt 添加一个脚本 Mover.cs. 此段代码放在 Start() 函数里, 因为在脚本的生命周期中只需要调用一次, 不需要每一帧都调用.

将 Bolt 拖至 Prefabs 文件夹成为预制体, 预制体做好后将原本的 Bolt 删除.

using UnityEngine;
using System.Collections;

public class Mover : MonoBehaviour
{
	// 子弹的速度
	public float speed;

	void Start ()
	{
		GetComponent<Rigidbody> ().velocity = transform.forward * speed;
	}

}

脚本控制发射子弹, 为 Player 新建空的子对象 Shot Spawn, Position(0,0,0.7), 在此位置发射子弹

管理光电子弹的生命周期, 子弹在飞出有效区域之后自行销毁, 为游戏区域添加触发器, 当电光子弹飞出区域时触发事件, 在实践响应函数中调用 Destroy.

设置 Boundary 为触发器, 由于不需要在场景中显示 Boundary 对象, 移除 Mesh Renderer 组件.

为 Boundary 添加脚本 DestoryByBoundary.cs

using UnityEngine;
using System.Collections;

public class DestoryByBoundary : MonoBehaviour {

	void OnTriggerExit(Collider other){
		Destroy (other.gameObject);
	}
}

注意的 :

  • 若要处理游戏对象移出触发器时的事件, 应该重载事件函数 OnTriggerExit;
  • OnTriggerExit 的参数 Collider 表示移出触发器的对象, 这里就是飞出边界的子弹对象上的碰撞体;

2.3 添加小行星障碍物 ##

要注意的几点

小行星随机生成, 随机的角度旋转;

射击击中小行星时, 小行星爆炸并销毁;

飞船碰上小行星, 飞船爆炸, 游戏结束;

新建空对象 Asteroid Position(0,0,9) Rigidbody 取消 Use Gravity 添加 Capsule Collider 勾选 Is Trigger.

模型 prop_asteroid_01 添加为 Asteroid 的子对象.

Capsule Collider 属性 Radius = 0.5, Height = 1.6, Direction 为 Z-Axis

为 Asteroid 添加脚本 RandomRotator.cs;

using UnityEngine;
using System.Collections;

public class RandomRotator : MonoBehaviour
{
	// tumble 是旋转系数
	public float tumble;
	void Start ()
	{
		// angularVelocity 表示刚体的角速度;  insideUnitSphere 表示单位长度半径球体内的一个随机点(向量)
		// 乘积结果描述了在半径长度为 tumble 的球体中的随机点
		// 由此就可以实现刚体以一个随机的角速度旋转
		GetComponent<Rigidbody> ().angularVelocity = Random.insideUnitSphere * tumble;
	}
}

设定 Asteroid 对象的角阻力为0;

添加控制射击小行星的功能, 为小行星 Asteroid 添加一个脚本来控制碰撞事件 DestroyByContact.cs

using UnityEngine;
using System.Collections;

public class DestoryByContact : MonoBehaviour
{
	// 小行星爆炸时的粒子对象
	public GameObject explosion;
	// 飞船与小行星碰撞飞船爆炸的粒子对象
	public GameObject playerExplosion;

	void OnTriggerEnter (Collider other)
	{

		if (other.tag == "Boundary" || other.tag == "Enemy") {
			return;
		}
		if (explosion != null) {
			// 在小行星销毁的位置生成一个爆炸效果, explosion 是小行星的位置
			Instantiate (explosion, transform.position, transform.rotation);  
		}

		if (other.tag == "Player") {
			// 在玩家飞机销毁的位置生成一个爆炸效果, playerExplosion 是飞船的位置
			Instantiate (playerExplosion, other.transform.position, other.transform.rotation);  
		}
		// 销毁跟小行星碰撞的物体
		Destroy (other.gameObject);  
		// 销毁小行星
		Destroy (this.gameObject);   
	}
}

Boundary 的 Tag 设为 Boundary; Player 的 Tag 设为 Player

至此动图效果为

2.4 控制小行星运动和随机生成 ##

让小行星以一定的速度飞向飞船, 为 Asteroid 添加脚本 Mover.cs 设置 speed 属性值为 -5; 速度设为负值, 因为小行星与子弹的运动方向相反

需要先制作 Asteroid 预制体, 创建 Project>>GameController 空游戏对象, Tag 为 GameController, 并为之创建脚本 GameController.cs

using UnityEngine;
using System.Collections;

public class GameController : MonoBehaviour
{
	// 小行星数组
	public GameObject[] hazards;
	// 随机生成小行星的位置
	public Vector3 spawnValues;
	// 每一波小行星生成的数量
	public int hazardCount;
	// 每次生成小行星对象后延迟的时间, 单位秒
	public float spawnWait;
	// 表示开始生成小行星对象前等待的时间
	public float startWait;
	// 表示两批小行星阵列间的时间间隔
	public float waveWait;

	void Start ()
	{
		StartCoroutine (SpawnWave ());
	}

	// 一波一波地生成小行星
	IEnumerator SpawnWave ()
	{
		yield return new WaitForSeconds (startWait);

		while (true) {
			for (int i = 0; i < hazardCount; i++) {
				GameObject hazard = hazards [Random.Range (0, hazards.Length)];
				Vector3 spawnPosition = new Vector3 (Random.Range (-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
				Instantiate (hazard, spawnPosition, Quaternion.identity);  // 生成随机的小行星
				yield return new WaitForSeconds (spawnWait);
			}

			yield return new WaitForSeconds (waveWait);
		}
	}
}

有一个要注意的地方, 对数组 Hazards 的内容不能拖成 model ,要是预制体, 否则生成的小行星无效导致不会运动, 如图

防止小行星数量太多, 距离近以致小行星之间相互碰撞销毁, 需要使用 协程类 WaitForSeconds

让爆炸后的粒子实例 explosion_asteroid 自动销毁, 建立脚本 DestroyByTime.cs 绑定到 explosion_asteroid 和 explosion_player 上

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyByTime : MonoBehaviour
{

	public float lifeTime = 2.0f;

	void Start ()
	{
		Destroy (gameObject, lifeTime);
	}
}

3. 添加音频 #

将音频文件添加至预制体
是否勾选 Play On Awake 表明音频文件在唤醒时自动播放;

4. 添加积分文本 #

新版 Text 组件的使用方法, GameObject>>UI>>Text 生成 Canvas>>Text 和 EventSystem. 调整 Text 位置, Anchor Presets 选择 top-left.

积分功能包括以下作用 :

飞船发射子弹击中小行星后分值增加;
分值增加后更新 Text 组件的显示;

在 GameController.cs 脚本添加变量 scoreText 和 score

// 更新计分 Text 的组件
public Text scoreText;
// 保存当前分值
private int score;

void Start ()
{
  score = 0;
  UpdateScore ();
  StartCoroutine (SpawnWave ());
}
void UpdateScore ()
{
  scoreText.text = "Get Score : " + score;
}

public void AddScore (int newScoreValue)
{
  score += newScoreValue;
  UpdateScore ();
}

脚本 DestoryByContact.cs 可以调用 AddScore 函数.

// 表示小行星被击中后玩家分值增加的数量
public int scoreValue;
// 表示在游戏对象 GameController 上绑定的脚本 GameController.cs
private GameController gameController;

void Start ()
{
  GameObject go = GameObject.FindWithTag ("GameController");
  if (go != null) {
    gameController = go.GetComponent<GameController> ();
  } else {
    Debug.Log ("Cannot Find a tag of GameController");
  }
  if (gameController == null) {
    Debug.Log ("Cannot Find the Script of GameController.cs");
  }
}
if (explosion != null) {
			// 在小行星销毁的位置生成一个爆炸效果, explosion 是小行星的位置
			Instantiate (explosion, transform.position, transform.rotation);  
			gameController.AddScore (scoreValue);
		}

5. 游戏结束与重新开始 #

添加游戏结束的 Text 组件

添加游戏结束的脚本

GameController 添加变量

// 更新 Text 组件的显示
public Text gameOverText;
// 游戏是否结束
private bool gameOver;
public void GameOver ()
{
  gameOver = true;
  gameOverText.text = "游戏结束";
}
while (true) {
			if (gameOver) {
				break;
			}
  // ... ...
}

在 DestroyByContact.cs 脚本加入对 GameOver() 函数的调用.

if (other.tag == "Player") {
	// 在玩家飞机销毁的位置生成一个爆炸效果, playerExplosion 是飞船的位置
	Instantiate (playerExplosion, other.transform.position, other.transform.rotation);  
	gameController.GameOver ();
}

添加重新开始的 Text 组件, 按[R]键重新开始.

// 更新添加的 Text 组件
public Text restartText;
// 是否重新开始游戏, 只有游戏结束时重新开始
private bool restart;

void Start ()
{
  score = 0;
  UpdateScore ();
  gameOverText.text = "";
  gameOver = false;
  restartText.text = "";
  restart = false;
  StartCoroutine (SpawnWave ());
}

void Update ()
{
  if (restart) {
    if (Input.GetKeyDown (KeyCode.R)) {
      Application.LoadLevel (Application.loadedLevel);
    }
  }
}

Application.LoadLevel(Application.loadedLevel) 是 Unity 中重新加载场景的常用方法.

三个文本

至此完毕.

End.

学习自 Book《Unity 官方案例精讲》

导出包 →GitHub

posted @ 2017-03-17 16:36  Hu&Fei  阅读(4944)  评论(0编辑  收藏  举报