由於初學Unity,寫下此文作為筆記,文中難免會有疏漏,不當之處還望指正。
Unity-2017.3官方實例教程Space-Shooter(一)
章節列表:
一、創建小行星Prefab
二、創建敵機和敵機子彈Prefab
三、創建游戲控制器
四、添加音效
五、對象回收
六、創建計分板
一、創建小行星Prefab
a、創建一個空對象,重命名為Asteroid_01,在Models文件夾下找到prop_asteroid_01模型,將模型拖拽到Asteroid_01上,使其成為Asteroid_01的一個子對象。
b、為prop_asteroid_01添加膠囊碰撞器(Capsule Collider),調整碰撞器的檢測范圍:
c、為Asteroid_01添加剛體組件(Rigidbody),去掉使用重力選項(Use Gravity):
d、為Asteroid_01添加隨機旋轉腳本RandomRotator,編輯如下內容:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class RandomRotator : MonoBehaviour { public float tumble; private Rigidbody rg; // Use this for initialization void Start () { rg = GetComponent<Rigidbody>(); rg.angularVelocity = Random.insideUnitSphere * tumble; } // Update is called once per frame void Update () { } }
首先聲明一個公有類型的變量tumble來設置翻滾速度,聲明一個私有類型變量rg便於類內部使用剛體組件,angularVelocity是剛體的角速度向量,在大多數情況下不應該直接修改它,因為這會導致不真實的行為。Random.insideUnitSphere變量隨機返回一個單位球體內的一點(Vector3類型變量,只讀)。
e、為Asteroid_01添加移動腳本。在上一篇中我們創建了一個Mover腳本,在這里我們把Mover腳本拖拽到Asteroid_01上,然后把速度設置為-5,讓Asteroid_01沿Z軸負方向移動。
f、當我們的飛船發射子彈擊中小行星,小行星會被擊毀並產生一個爆炸的特效;當我們的飛船撞向小行星時,飛船會和小行星同歸於盡,並產生另外一個爆炸效果。為了區分是子彈擊中了小行星還是我們的飛船撞向了小行星,我們可以給我們的飛船增加一個tag,通過判斷這個tag就可以知道是不是飛船撞向了小行星。點擊Player,我們在Tag屬性處選擇Player,也可以新建另外的tag:
我們給Asteroid_01添加一個腳本組件,命名為DestroyByContact,編輯內容如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DestroyByContact : MonoBehaviour { public GameObject explosion; public GameObject explosion_player; // Use this for initialization void Start () { } // Update is called once per frame void Update () { } private void OnTriggerEnter(Collider other) { if(explosion) { Instantiate(explosion, transform.position, transform.rotation); } if(other.tag=="Player" && explosion_player) { Instantiate(explosion_player, transform.position, transform.rotation);//產生一個特效 } Destroy(other.gameObject); Destroy(gameObject); } }
聲明兩個公有變量explosion和explosion_player,分別綁定兩種特效對象,用於在小行星和我方飛船爆炸時產生特效。
void OnTriggerEnter(Collider other)函數是碰撞檢測的回調函數,當開始碰撞時調用,要想碰撞函數被調用,需要碰撞器中勾選Is Trigger選項。
Instantiate的函數原型為static Object Instantiate(Object original, Vector3 position, Quaternion rotation)或static Object Instantiate(Object original),函數作用為克隆原始物體並返回克隆物體,original為要拷貝的已經存在的對象,position和rotation分別為拷貝新對象的位置和方向。Instantiate更多通常用於實例投射物(如子彈、榴彈、破片、飛行的鐵球等),AI敵人,粒子爆炸或破壞物體的替代品等。
Destroy的函數原型為static void Destroy(Object obj, float t = 0.0F),在時間t后刪除一個游戲對象,組件或者資源,如果obj是組件,它將從GameObject銷毀組件component。如果obj是GameObject它將銷毀GameObject全部的組件和GameObject全部的transform子物體。實際物體的銷毀總是延遲到當前更新循環后,但總是渲染之前完成。
回到Unity編輯器,點擊Asteroid_01對象,將Prefabs文件夾下的explosion和explosion_player分別拖拽到Explosion和Explosion_player屬性上:
為了后續的使用,我們將Asteroid_01的tag屬性選擇為Enemy:
g、把Asteroid_01制作成一個Prefab。在Prefabs的DonePrefabs文件夾下創建一個Prefab,重命名為Asteroid_01,把Hierarchy視圖中的Asteroid_01拖拽到Asteroid_01預制體上,完成Prefab的制作。
同樣的我們把另外兩個小行星模型也制作成Prefab,分別為Asteroid_02和Asteroid_03。
二、創建敵機和敵機子彈Prefab
和創建Player一樣,敵機也具有船體、引擎特效、子彈掛載點,發射子彈,移動等屬性。
(1)、創建敵機模型。新建一個空對象,重命名為EnemyShip,將Models文件夾下的敵人飛船模型vehicle_playerShip拖到EnemyShip上,調整vehicle_playerShip模型的方向,繞Y軸旋轉180度,使機頭朝下。將Prefabs->VFX->Engines文件夾下的敵機引擎特效engines_enemy拖拽到EnemyShip。再在EnemyShip下建立一個空對象,命名為ShotSpawn,調整ShotSpawn的位置在機頭位置。
(2)、為EnemyShip添加剛體組件和膠囊碰撞器。
(3)、給EnemyShip添加Mover腳本使能向下移動。把Mover腳本拖拽到EnemyShip上,並把Speed屬性值設置為-5:
(4)、給EnemyShip添加DestroyByContact腳本,使其能被我方飛船發射的子彈擊毀,或者和我方飛船同歸於盡,但是不會被敵方物體撞毀(敵方子彈,敵方飛船,小行星),修改DestroyByContact腳本內容:
private void OnTriggerEnter(Collider other) { if(other.tag=="Enemy") { return; } if(explosion) { Instantiate(explosion, transform.position, transform.rotation); } if(other.tag=="Player" && explosion_player) { Instantiate(explosion_player, transform.position, transform.rotation);//產生一個特效 } Destroy(other.gameObject); Destroy(gameObject); }
回到Unity編輯器,點擊EnemyShip對象,將Prefabs文件夾下的explosion_enemy和explosion_player分別拖拽到Explosion和Explosion_player屬性上:
(5)、讓敵機發射子彈。為EnemyShip添加一個新腳本,命名為WeaponController,編輯腳本內容如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class WeaponController : MonoBehaviour { public GameObject shot; public Transform shotSpawn; public float shotRate; public float delay; // Use this for initialization void Start () { InvokeRepeating("Fire", delay, shotRate); } // Update is called once per frame void Update () { } private void Fire() { Instantiate(shot,shotSpawn.position,shotSpawn.rotation); } }
a、shot變量用來實例化子彈,shotSpawn為子彈射出的位置,shotRate記錄敵機發射子彈的頻率,delay為實例化敵機后多少秒敵機開始發射子彈。
b、InvokeRepeat的函數原型為 void InvokeRepeating(string methodName, float time, float repeatRate),在time秒調用methodName方法,然后每repeatRate秒重復調用methodName方法。Fire函數實例化一個子彈對象。
c、創建子彈預制體。
在Assets下新建一個文件夾命名為MyMaterials,在MyMaterials下新建一個材質球,右鍵->Create->Material,命名為mat_bolt,在Inspector視圖中設置Shader屬性為Particles/Additive,選擇紋理為fx_lazer_cyan_dff:
新建一個空對象,重命名為Bolt,在Bolt下新建一個Quat對象,重命名為VFX,刪除VFX上的Mesh Collider。把材質球mat_bolt拖拽到VFX上面,並將VFX繞X軸旋轉90°:
點擊Bolt,設置Bolt的tag屬性為Enemy,添加剛體組件和膠囊碰撞器,為Bolt添加Mover腳本,並設置Speed屬性為-20,為Bolt添加DestroyByContent腳本,設置完成如下:
在Prefabs/DonePrefabs文件夾下新建一個Prefab,重命名為Bolt,將Hierarchy視圖中的Bolt對象拖拽到Prefab Bolt上,完成子彈預制體的制作。然后刪除Hierarchy視圖中的Bolt對象。
d、點擊EnemyShip對象,為Shot、Shot Spawn、Shot Rate和Delay參數賦值:
(6)、讓敵機具有規避機動。為EnemyShip新添加一個腳本,命名為EvasiveManeuver,編輯腳本內容如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EvasiveManeuver : MonoBehaviour { public Boundray boundray; public float tilt; public float dodge; public float smoothing; public Vector2 startWait; public Vector2 maneuverTime; public Vector2 maneuverWait; private float currentSpeed; private float targetManeuver; private Rigidbody rg; // Use this for initialization void Start () { rg = GetComponent<Rigidbody>(); currentSpeed = rg.velocity.z; StartCoroutine(Evade()); } // Update is called once per frame void Update () { } IEnumerator Evade() { yield return new WaitForSeconds(Random.Range(startWait.x,startWait.y)); while(true) { targetManeuver = Random.Range(1, dodge) * -Mathf.Sign(transform.position.x); yield return new WaitForSeconds(Random.Range(maneuverTime.x,maneuverTime.y)); targetManeuver = 0; yield return new WaitForSeconds(Random.Range(maneuverWait.x,maneuverWait.y)); } } private void FixedUpdate() { float newManeuver = Mathf.MoveTowards(rg.velocity.x,targetManeuver,smoothing*Time.deltaTime); rg.velocity = new Vector3(newManeuver,0.0f, currentSpeed); rg.position = new Vector3( Mathf.Clamp(rg.position.x, boundray.xMin, boundray.xMax), 0.0f, Mathf.Clamp(rg.position.z,boundray.zMin,boundray.zMax)); rg.rotation = Quaternion.Euler(0.0f,0.0f,rg.velocity.x*-tilt); } }
設置參數如下所示:
(7)、把敵機對象制作成Prefab。為了后續的使用,我們把EnemyShip的tag屬性宣威Enemy,然后在Prefabs->DonePrefabs文件夾下新建一個Prefab,重命名為EnemyShip,把EnemyShip拖拽到新建的Prefab上,完成Prefab的制作。
三、創建游戲控制器
完成了上述內容我們運行游戲發現小行星和敵機不會自動出現,所以我們需要建立一個腳本來控制敵人不間斷的出現。我們在Scripts文件夾下新建一個腳本文件,命名為GameController,把GameController腳本綁定到Main Camera上,編輯腳本內容如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameController : MonoBehaviour { public GameObject[] hazards; public Vector3 spawnValue; public long hazardCnt; public float spawnWait; public float startWait; public float waveWait; // Use this for initialization void Start () { StartCoroutine(SpawnWaves()); } // Update is called once per frame void Update () { } IEnumerator SpawnWaves() { yield return new WaitForSeconds(startWait); while(true) { for (long i=0;i<hazardCnt;++i) { GameObject hazard = hazards[Random.Range(0,hazards.Length)]; Vector3 spawnPosition = new Vector3( Random.Range(-spawnValue.x,spawnValue.x), spawnValue.y, spawnValue.z); Quaternion spawnRotation = Quaternion.identity; Instantiate(hazard,spawnPosition,spawnRotation); yield return new WaitForSeconds(spawnWait); } yield return new WaitForSeconds(waveWait); } } }
四、添加音效
1、給我方飛船增加發射子彈音效。在Prefabs/DonePrefabs文件夾下找到Player預制體,選中Player預制體,在Inspector視圖底部點擊Add Component->Audio->Audio Source,將Audio文件夾下的weapon_player拖拽到Audio Source組件中的Audio Clip屬性上,去掉Play On Awake選項:
打開PlayController腳本文件,編輯腳本內容,使飛船在發射子彈時產生音效,修改內容如下:
2、給敵機增加發射子彈音效。在Prefabs/DonePrefabs文件夾下找到EnemyShip預制體,選中EnemyShip預制體,在Inspector視圖底部點擊Add Component->Audio->Audio Source,將Audio文件夾下的weapon_enemy拖拽到Audio Source組件中的Audio Clip屬性上,去掉Play On Awake選項:
打開WeaponController腳本文件,編輯腳本內容,使敵方飛船在發射子彈時產生音效,修改內容如下:
3、給小行星、我方飛船、敵方飛船添加爆炸音效。這三種音效不能像上述兩種那樣直接綁定到對象上,因為發生爆炸的時候對象直接被銷毀了,這個時候即使調用了Play()函數也無法播放聲音(因為對象已經銷毀,所以綁定在對象上的AudioSource組件也一起銷毀了)。我們需要把這個音效綁定在爆炸特效上,點擊explosion_asteroid預制體,為預制體添加AudioSource組件,把Audio文件夾下的explosion_asteroid音效文件拖拽到Audio Clip屬性上,注意Play On Awake屬性為勾選狀態:
我方飛船和敵方飛船爆炸音效按照同樣的方法制作。
4、加上背景音效。背景音效我們可以綁定到主相機上,也可以綁定到背景上面,這里我們選擇綁定到背景上。點擊BackGround,為BackGround添加Audio Source組件,並把music_background音效拖拽到Audio Clip屬性上,記得勾選Play On Awake和Loop選項:
五、對象回收
我們發現在我們的游戲運行的過程中,如果小行星和敵機沒有被擊毀並跑到了我們的視野之外,他們就會一直存在,另外子彈和爆炸效果產生的對象也不會自動銷毀,這樣我們的內存消耗就會越來越大。所以我們需要有對象回收機制。
對於跑出視野的小行星和敵機、發射的子彈我們采用的方法是創建一個Cube對象,當這些對象和Cube碰撞時銷毀這些對象。新建一個Cube,重命名為Boundray,去掉Cube(Mesh Filter)和Mesh Renderer組件,在Box Collider組件中選中Is Trigger選項,新建一個Tag,命名為Boundray,在Tag屬性中選中Boundray,最后為Boundray對象添加一個腳本,命名為DestroyByBoundray:
編輯DestroyByBoundray腳本內容如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DestroyByBoundray : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { } private void OnTriggerExit(Collider other) { Destroy(other.gameObject); } }
讓與Boundray對象發生碰撞的游戲對象直接銷毀。(這里有個小錯誤:在我們制做小行星預制體時,我們只給prop_asteroid_01、prop_asteroid_02和prop_asteroid_03模型綁定了膠囊碰撞器,而沒有給它們的父對象Asteroid_01、Asteroid_02和Asteroid_03綁定碰撞器,那么在這里我們直接Destroy(other.gameObject),只是把子對象銷毀了,父對象還一直存在,所以我們還要給父對象綁定一個碰撞器。其實我們只需要給父對象綁定一個碰撞器就可以了,如果兩者都綁定了碰撞器,而prop_asteroid_01、prop_asteroid_02和prop_asteroid_03的Tag屬性又沒有選擇為Enemy,那么小行星會被敵機的子彈打中,原因是當發生碰撞時,DestroyByContent腳本中的OnTriggerEnter會觸發兩次,分別是父對象觸發一次,子對象觸發一次,這樣子對象就會被銷毀,而只剩下一個父對象。)
在DestroyByContent腳本中我們編寫了與小行星或敵方飛船發生碰撞的對象都會被銷毀,但是Boundray對象是不能銷毀的,所以我們需要修改DestroyByContent腳本的OnTriggerEnter函數:
private void OnTriggerEnter(Collider other) { if(other.tag=="Enemy" || other.tag == "Boundray") { return; } if(explosion) { Instantiate(explosion, transform.position, transform.rotation); } if(other.tag=="Player" && explosion_player) { Instantiate(explosion_player, transform.position, transform.rotation);//產生一個特效 } Destroy(other.gameObject); Destroy(gameObject); }
對於爆炸效果的銷毀,我們采取延時一段時間后銷毀對象。首先我們新建一個腳本,命名為DestroyByTime,編輯內容如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DestroyByTime : MonoBehaviour { public float lifeTime; // Use this for initialization void Start () { Destroy(gameObject,lifeTime); } // Update is called once per frame void Update () { } }
在Prefabs/VFX/Explosions文件夾下找到explosion_asteroid、explosion_enemy和explosion_player三個爆炸特效預制體,把這三個預制體都綁定DestroyByTime腳本,並把lifeTime設置為2秒,這樣爆炸產生的特效對象會在2秒后銷毀。
六、創建計分板
建立一個Text用來記錄分數。在Hierarchy視圖中右鍵->UI->Text,創建一個Text,重命名為ScoreText,顏色選擇為紅色。在創建Text的時候系統會自動建立一個畫布,按照同樣的辦法我們再創建兩個Text用來提示游戲結束和重新開始,調整Text的位置,如下所示:
為了實現計分功能,我們需要修改GameController腳本:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; public class GameController : MonoBehaviour { public GameObject[] hazards; public Vector3 spawnValue; public long hazardCnt; public float spawnWait; public float startWait; public float waveWait; public Text scoreText; public Text gameOverText; public Text restartText; private int score; private bool gameOver; private bool restart; // Use this for initialization void Start () { StartCoroutine(SpawnWaves()); gameOverText.text = ""; restartText.text = ""; UpdateScore(); score = 0; gameOver = false; restart = false; } // Update is called once per frame void Update () { if(restart) { if(Input.GetKeyDown(KeyCode.R)) { SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); } } } IEnumerator SpawnWaves() { yield return new WaitForSeconds(startWait); while(true) { for (long i=0;i<hazardCnt;++i) { GameObject hazard = hazards[Random.Range(0,hazards.Length)]; Vector3 spawnPosition = new Vector3( Random.Range(-spawnValue.x,spawnValue.x), spawnValue.y, spawnValue.z); Quaternion spawnRotation = Quaternion.identity; Instantiate(hazard,spawnPosition,spawnRotation); yield return new WaitForSeconds(spawnWait); } yield return new WaitForSeconds(waveWait); if(gameOver) { restart = true; restartText.text = "Press 'R' for Restart."; break; } } } private void UpdateScore() { scoreText.text = "Score:" + score.ToString(); } public void AddScore(int newScore) { score += newScore; UpdateScore(); } public void GameOver() { gameOver = true; gameOverText.text = "Game Over!"; } }
修改DestroyByContent腳本內容如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DestroyByContact : MonoBehaviour { public GameObject explosion; public GameObject explosion_player; public int scoreValue; private GameController gameController; // Use this for initialization void Start () { GameObject gameControllerObj = GameObject.FindGameObjectWithTag("GameController"); if(gameControllerObj) { gameController = gameControllerObj.GetComponent<GameController>(); } if(!gameController) { Debug.Log("Can not Find 'GameController' script."); } } // Update is called once per frame void Update () { } private void OnTriggerEnter(Collider other) { if(other.tag=="Enemy" || other.tag == "Boundray") { return; } if(explosion) { Instantiate(explosion, transform.position, transform.rotation); } if(other.tag=="Player" && explosion_player) { Instantiate(explosion_player, transform.position, transform.rotation);//產生一個特效 gameController.GameOver(); } gameController.AddScore(scoreValue); Destroy(other.gameObject); Destroy(gameObject); } }
把預制體Asteroid_01、Asteroid_02和Asteroid_03的Score Value屬性值設置為10,把預制體EnemyShip的Score Value屬性值設置為20,點擊Main Camera找到GameController腳本組件,分別將Hierarchy視圖中的ScoreText、GameOverText和RestartText拖拽到Score Text、Game Over Text和Restart Text屬性上,並把Main Camera的Tag屬性設置為GameController:
至此游戲制作基本完成。