寫在前面 #
本次 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
