How to Create a Tower Defense Game in Unity – Part 2
原文地址:https://www.raywenderlich.com/107529/unity-tower-defense-tutorial-part-2
歡迎大家來查看,使用Unity創建塔防游戲(第二篇)。在第一篇的結尾,我們已經可以召喚和升級小怪獸,召喚一個敵人朝著餅干前進的敵人。
但是這個敵人沒有方向感,讓人感覺怪怪的。接下來,我們要做的是召喚一波一波的敵人,然后令小怪獸能夠消滅它們,都是為了保護你那塊美味的餅干。
準備工作
用Unity打開你之前完成的工程,但如果你沒看過Part1,先下載starter project ,然后打開TowerDefense-Part2-Starter這個工程。打開Scenes文件夾下的GameScene。
讓敵人有方向感
在Part1的結尾,我們可以令敵人沿著路線前進,但它們毫無方向感。
用VS打開腳本MoveEnemy.cs,添加下面的代碼來解決這個問題。
private void RotateIntoMoveDirection() { // 1 Vector3 newStartPosition = waypoints[currentWaypoint].transform.position; Vector3 newEndPosition = waypoints[currentWaypoint + 1].transform.position; Vector3 newDirection = (newEndPosition - newStartPosition); // 2 float x = newDirection.x; float y = newDirection.y; float rotationAngle = Mathf.Atan2(y, x) * 180 / Mathf.PI; // 3 GameObject sprite = (GameObject)gameObject.transform.FindChild("Sprite").gameObject; sprite.transform.rotation = Quaternion.AngleAxis(rotationAngle, Vector3.forward); }
RotateIntoMoveDirection 這個方法是將場景中敵人對象的角度進行旋轉,讓敵人看起來有方向感。我們一步一步地來看:
- 計算出下一個路標與當前路標之間的向量之差,敵人會沿著這個向量前往下一個路標。
- 計算敵人要旋轉的角度,即敵人當前的方向與newDirection之間的夾角的度數。調用Mathf.Atan2 來計算,參數為newDirection的X坐標和Y坐標,但返回的結果是以弧度為單位的。因此,我們需要將結果乘以 180 / Math.PI ,將弧度轉化為角度。
- 最后,我們獲取敵人對象的子對象——Sprite,令它圍繞Z軸旋轉的度數為rotationAngle。這里我們調用Quaternion.AngleAxis來完成旋轉的工作,它的第一個參數就是我們之前計算出的角度。注意,為什么是將子對象旋轉,而不是敵人對象?這是為了保證敵人的血條始終保持水平,接下來我們要為敵人添加血條了。
將Update() 中的注釋 // TODO: Rotate into move direction
替換成調用我們剛寫好的函數—— RotateIntoMoveDirection
RotateIntoMoveDirection();
保存好腳本,返回Unity,運行游戲,看敵人現在有方向感了。這樣才算是朝著餅干前進。
才一個小兵?這怎行,要來就來一大群。在一般的塔防游戲中,都是每一波敵人都是一大群。
告知玩家——敵人來了
在一大群敵人出現之前,我們應該先告知玩家——敵人來了。同時,我們需要顯示這是第幾波敵人,在界面的右上角顯示。
在腳本中,有不少需要用到波數的地方,我們先在GameManager的腳本組件GameManagerBehavior中添加有關波數的代碼。
用VS打開GameManagerBehavior.cs,然后添加下面兩個變量:
public Text waveLable; public GameObject[] nextWaveLabels;
顯示在屏幕右上角的波數會存儲在waveLabel 這個變量中。 nextWaveLabels 這個數組保存了兩個游戲對象。在一波新的敵人到來之前,它們會構成一個文字合并的動畫,如下圖所示:
保存好腳本,返回Unity。選中Hierarchy視圖中的GameManager,在Inspector面板中,點擊Wave Label右側的小圓圈,然后從彈出的Text對話框中的Scene標簽頁下選擇 WaveLabel 。
將NextWave Labels 的Size 設置為2。就像剛才設置WaveLabel那樣,將Element0設置為NextWaveBottomLabel ,將Element1設置為NextWaveTopLabel。
這是設置好數據的結果。
當玩家輸掉游戲的時候,它無法看到有關下一波敵人的信息。回到GameManagerBehavior.cs中,添加一個變量:
public bool gameOver = false;
gameOver這個變量表示玩家是否輸掉了游戲。
同樣的,我們也要為wave這個私有變量添加一個屬性,讓wave中的值與游戲當前波數保持一致,再向GameManagerBehavior.cs添加以下代碼:
private int wave; public int Wave { get { return wave; } set { wave = value; if (!gameOver) { for (int i = 0; i < nextWaveLabels.Length; i++) { nextWaveLabels[i].GetComponent<Animator>().SetTrigger("nextWave"); } } waveLable.text = "WAVE: " + (wave + 1); } }
在上面的代碼中,我們創建了一個私有變量,一個屬性。這個屬性的getter方法,我們已經習以為常了,但它的setter方法看起來有些棘手。
先是更新了wave的值。接下來,判斷游戲是否未結束,如果是的話,遍歷nextWaveLabels中元素,這些元素都帶有一個Animator組件。調用SetTrigger來觸發動畫。
最后,我們設置waveLabel上的數值為 wave + 1。為什么呢?因為在程序中,變量的初始值可以是0,但是人們都是從1開始數數的。
在Start()方法中設置這個屬性的值:
Wave = 0;
將Wave的初始值設置為1。
保存好腳本,返回Unity中,運行游戲。波數的確是從1開始的。
對于玩家而言,首先要解決的是第一波敵人。
逐個創建敵人
顯然,我們現在要做的是創建一支敵軍(由想吃掉你餅干的小蟲子組成),但我們暫時無法做到。
此外,當玩家剛消滅一波敵人的時候,先不要創建下一波敵人,至少現在是這樣。
于是,我們必須要知道游戲場景中是否還有敵人存在,我們為敵人對象添加Tags(標簽)來區別于其他游戲對象。此外,在腳本中,可以通過標簽名快速查找物體。
為敵人對象添加標簽
在Project視圖中,選中名為Enemy的prefab。在Inspector面板的頂部,點擊Tag右邊的下拉框,從彈出的對話框中選擇Add Tag。
新建一個標簽,命名為Enemy。
選中名為Enemy的prefab,在Inspector中將它的標簽設置為我們剛才創建的標簽——Enemy。
配置敵軍的信息
現在,我們需要定義有關敵軍的類和變量。用VS打開SpawnEnemy.cs,在SpawnEnemy的上方添加一個新的類,如下面代碼所示:
[System.Serializable] public class Wave { public GameObject enemyPrefab; public float spawnInterval = 2; public int maxEnemies = 20; }
Wave這個類表示一支敵軍,它有3個字段,enemyPrefab用于實例化敵人對象;每隔spawnInterval秒產生一個敵人,每波創建單個敵人的時間間隔可能是不同的;一波敵人的最大數量為maxEnemies。
這個類是序列化的,所以我們可以在Inspector面板中更改它的數據。
接下來為SpawnEnemy這個類添加下列變量:
public Wave[] waves; public int timeBetweenWaves = 5; private GameManagerBehavior gameManager; private float lastSpawnTime; private int enemiesSpawned = 0;
這幾個變量都是與創建敵人有關的。我們將各個級別的敵軍存儲在waves這個數組里;enemiesSpawned記錄了已產生的敵人的數量;lastSpawnTime記錄了還是上一個敵人產生的時間;
玩家需要一些時間來消滅這些敵人,于是我們將timeBetweenWaves設置為5秒,即每隔5秒產生一波敵人。
將Start()方法中的代碼替換為以下代碼:
lastSpawnTime = Time.time; gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
我們將lastSpawnTime設置為當前時間,當場景加載完成后,Start()方法就會被執行。然后,我們獲取了游戲對象GameManager的引用。
向Update()方法中添加下列代碼:
// 1 int currentWave = gameManager.Wave; if (currentWave < waves.Length) { // 2 float timeInterval = Time.time - lastSpawnTime; float spawnInterval = waves[currentWave].spawnInterval; if(((enemiesSpawned == 0 && timeInterval > timeBetweenWaves) || timeInterval > spawnInterval) && enemiesSpawned < waves[currentWave].maxEnemies) { // 3 lastSpawnTime = Time.time; GameObject newEnemy = (GameObject)Instantiate(waves[currentWave].enemyPrefab); enemiesSpawned++; } // 4 if (enemiesSpawned == waves[currentWave].maxEnemies && GameObject.FindGameObjectWithTag("Enemy") == null) { gameManager.Wave++; gameManager.Gold = Mathf.RoundToInt(gameManager.Gold * 1.1f); enemiesSpawned = 0; lastSpawnTime = Time.time; } } // 5 else { gameManager.gameOver = true; GameObject gameOverText = GameObject.FindGameObjectWithTag("GameWon"); gameOverText.GetComponent<Animator>().SetBool("gameOver", true); }
讓我們一步一步來理解這段代碼:
- 獲得當前波數,并判斷是否未到最后一波。
- 如果是這樣的話,先計算距離上一個敵人的創建過去了多少時間,并且判斷是否到了創建下一個敵人的時間。這取決于兩個條件:一、如果已創建的敵人數量為0,并且timeInterval大于timeBetweenWaves;二、判斷timeInterval是否大于spawnInterval。無論如何,前提是這波敵人尚未被創建完畢。
- 假如符合2中的條件,就以enemyPrefab為拷貝,實例化一個敵人對象,賦予敵人對象有關路標的信息,并且將已創建的敵人數量加1。
- 若所有敵人都已被創建,但場景中找不到標簽為Enemy的游戲對象,說明這波敵人都已玩家消滅。我們就要準備創建下一波敵人,并且給予玩家金幣數量增加百分之十。
- 玩家消滅了最后一波敵人,播放游戲勝利的動畫。
設置創建單個敵人的時間間隔
保存好腳本,返回Unity,選中Hierarchy視圖中的Road對象,在Inspector面板中,將數組Waves的Size設置為4。
接下來,依次為數組的4個元素賦值。將名為Enemy的prefab賦值給Enemy Prefab,分別設置Spawn Interval和Max Enemies的值如下:
- Element 0: Spawn Interval: 2.5, Max Enemies: 5
- Element 1: Spawn Interval: 2, Max Enemies: 10
- Element 2: Spawn Interval: 2, Max Enemies: 15
- Element 3: Spawn Interval: 1, Max Enemies: 5
最終設置好的結果如下如圖所示:
我們可以通過上面的設置達到平衡游戲的目的。運行游戲,哈哈!那些小蟲子正朝著你的餅干前進!
可選項:添加不同種類的敵人
塔防游戲里的敵人一般都不止一種。在我們工程的Prefab文件夾中還包含著另一種敵人的prefab,Enemy2。
選中Prefab文件夾中的Enemy2,在Inspector面板中,為它添加一個腳本組件,我們選擇已有的MoveEnemy這個腳本。將Speed的值設置為3,將它的標簽設置為Enemy。我們用這種快速前進的小蟲子,讓玩家保持警覺。
更新玩家的血量——不要讓我死的那么快
現在,即使一大群小蟲子抵達了你那美味的餅干,你的血量都絲毫未損。于是,當有小蟲子碰了你那塊餅干的時候,你就要受傷了。
打開GameManagerBehavior.cs,添加下面兩個變量。
public Text healthLabel; public GameObject[] healthIndicator;
我們用healthLabel來顯示玩家當前的血量,healthIndicator用于表示5只正在啃你餅干的小蟲子,比起一個簡單的數字或血條,用它們來表示玩家的血量會更有趣一些。
控制玩家的血量
接下來,為 GameManagerBehavior 添加一個屬性,用來管理玩家的血量。
private int health; public int Health { get { return health; } set { // 1 if (value < health) { Camera.main.GetComponent<CameraShake>().Shake(); } // 2 health = value; healthLabel.text = "HEALTH: " + health; // 3 if (health <= 0 && !gameOver) { gameOver = true; GameObject gameOverText = GameObject.FindGameObjectWithTag("GameOver"); gameOverText.GetComponent<Animator>().SetBool("gameOver", true); } // 4 for (int i = 0; i < healthIndicator.Length; i++) { if (i < Health) { healthIndicator[i].SetActive(true); } else { healthIndicator[i].SetActive(false); } } } }
以上代碼塊用于管理玩家的血量,同樣的,setter方法是這段代碼的主體。
- 當玩家掉血的時候,我們使用CameraShake這個組件來制造一個很棒的晃動效果。(這個晃動效果是為了警告玩家,小蟲子正在吃掉你的餅干。)這個腳本也被包含在我們的工程內,但本文不作介紹。
- 更新私有字段health的值,以及屏幕左上角的血量顯示。
- 當玩家血量被扣光的時候,且游戲未結束,先設置
gameOver
的值為true,再觸發游戲失敗的動畫。 - 將一只綠色的小怪物從餅干上移除。就可以做得簡單點的話,我們可以只是隱藏它們,當我們需要為玩家加血的時候,就可以將重新它們顯示出來。
在Start()中初始化Health:
Health = 5;
在游戲開始的時候,玩家的血量為5。
有了這個屬性,當小蟲子抵達餅干的時候,我們就可以更新玩家的血量了。保存好腳本,在VS中打開MoveEnemy.cs這個腳本。
更新玩家的血量
將MoveEnemy.cs中Update()方法內部的注釋:// TODO: deduct health ,替換成以下代碼:
GameManagerBehavior gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>(); gameManager.Health -= 1;
這段代碼是為了獲取GameManagerBehavior對象,然后將Health的值減1。
保存好腳本,返回Unity。
選中Hierarchy視圖中的GameManager對象,為Health Label 賦值,選擇HealthLabel。
在Hierarchy視圖中展開Cookie對象,注意不要選中它,我們只要讓它下面的5個子對象顯示出來即可。將這5個子對象拖拽賦值給GameManager的Health Indicator數組。我們用5只正在開心地啃著餅干的青色小蟲子來表示玩家的血量。玩家受到一次傷害,就減少一只青色的小蟲子。
運行游戲,讓那些小蟲子沖向餅干,什么都別做,直到游戲結束。
小怪獸的戰斗:消滅那些小蟲子
該召喚小怪獸?還是讓小蟲子前進?現在我們的小怪獸還是紙老虎,我們要做的是讓小怪獸們能夠消滅那些小蟲子。
我們先要把以下幾件事情做好:
- 給小蟲子一個血條,讓玩家能看出敵人的強弱。
- 讓小怪獸能夠發現它攻擊范圍內的敵人們
- 決定朝那個敵人開火
- 無盡的子彈
顯示敵人的血條
我們用兩張圖片來顯示血條,一張是暗的,用于顯示血條的背景,另一張是綠色較小的細長圖片,我們通過縮放它的長度來與敵人當前血量匹配。
將Project視圖中的Prefabs\Enemy拖到場景中。
將Images\Objects\HealthBarBackground拖拽到Hierarchy視圖中的Emeny對象上,令HealthBarBackground作為Enemy的子對象。
在Inspector面板中,將HealthBarBackground的Position設置為 (0, 1, -4) 。
接下來選中Project視圖中的Images\Objects\HealthBar,確保它的Pivot被設置為Left。同樣的,也將它作為Hierarchy視圖中的Emeny對象的子對象,將它的Position設置為 (-0.63, 1, -5),將它的X Scale設置為125 。
為游戲對象HealthBar添加一個C#腳本,命名為HealthBar,后面我們需要在腳本中調整血條長度。
現在我們將Hierarchy視圖中的Emeny對象的坐標調整為(20, 0, 0) 。
點擊Inspector面板頂部的Apply按鈕,保存剛才對prefab的更改。回到Project視圖,剛才我們所作的更改已經成為了Prefab的一部分。最后,刪除Hierarchy視圖中的Emeny對象。
同上,我們也為Prefab\Enemy2添加一個血條。
調整血條的長度
在VS中打開HealthBar.cs,添加下列變量:
public float maxHealth = 100; public float currentHealth = 100; private float originalScale;
maxHealth表示敵人的最大生命值,currentHealth則表示敵人的當前的生命值,originalScale記錄的是血條圖片的初始長度。
在Start()方法中,為originalScale賦值:
originalScale = gameObject.transform.localScale.x;
這里,我們獲取了HealthBar這個游戲對象的X Scale。
在Update()方法中,我們通過縮放HealthBar的圖片長度,令它與敵人的當前生命值匹配:
Vector3 tmpScale = gameObject.transform.localScale; tmpScale.x = currentHealth / maxHealth * originalScale; gameObject.transform.localScale = tmpScale;
以上代碼能夠簡寫為下面的代碼么?
gameObject.transform.localScale.x = currentHealth / maxHealth * originalScale;
不行的,單獨為localScale.x賦值的時候,編譯器報錯了。
于是,我們只能夠先用一個臨時變量tmpScale獲取localScale的值,然后為tmpScale.X賦值,最后將tmpScale賦值localScale。
保存好腳本,啟動游戲。現在我們可以看到每個敵人都有了自己的血條。
選中一個敵人對象Enemy(Clone),在Hierarchy視圖將它展開,選中它的子對象HealthBar。在Inspector面板中調整Current Health這個變量的值,我們可以看到敵人的血條的長度隨著Current Health的值變化。
追蹤射程內的敵人
現在,小怪獸們需要知道它們的攻擊目標在哪里。在我們做這件事之前,我們要先為小怪獸和敵人做一點準備工作。
選中Project面板中的Prefab\Monster,在Inspector面板中為它添加一個Circle Collider 2D組件,這是一個2D圓形碰撞體組件。
將該圓形碰撞體的半徑設置為2.5——這是小怪獸的射程。
啟用Is Trigger這個屬性,目的是令此碰撞體用于觸發事件,并且不會發生任何物理交互。如果不啟用這個屬性的話,就是會發生碰撞。
最后,在Inspector面板的頂部,將Monster的Layer屬性設置為Ignore Raycast。在彈出的對話框中選擇Yes,change children。如果你不這樣設置的話,碰撞體會響應鼠標點擊事件,這是我們不需要的。小怪獸位于召喚點Openspot的上方,這個碰撞體又是小怪獸的組件,于是鼠標點擊事件就會被碰撞體優先響應,而不是被Openspot響應。這樣的結果是什么?上一篇文章中,Openspot通過響應鼠標點擊事件,可以放置或升級小怪獸;想想看,放置小怪獸后不能對它升級,這是不是違背了之前的設定?
為了令小怪獸的碰撞體能夠檢測到在它范圍內的敵人,我們需要為敵人對象添加一個碰撞體和剛體。在兩個碰撞體發生碰撞的時候,假如其中一個有附加剛體組件,那么就會觸發碰撞事件。
在Project面板中,選中Prefab\Enemy,為它添加Rigid Body 2D組件,勾選Is Kinematic屬性。這是為了令敵人對象不受Unity中的物理引擎影響。
再添加一個Circle Collider 2D,半徑設置為1。對Prefab\Enemy2重復以上步驟。
現在所有的設置都已完成,你的小怪獸們可以偵測到射程內的敵人。
還有一件事情要做:在腳本中告知小怪獸敵人是否被消滅,當它們的射程內沒有敵人的時候,沒必要一直開火。
為Enemy和Enemy2這兩個prefab添加一個新的腳本組件,命名為EnemyDestructionDelegate。
在VS中打開這個腳本,為它添加一個委托的聲明:
public delegate void EnemyDelegate(GameObject enemy); public EnemyDelegate enemyDelegate;
這里我們創建了一個委托,它包含了一個方法的聲明,可以像變量一樣傳遞。
提示: 當我們需要讓一個游戲對象靈活地通知另一個游戲對象做出改變,請使用委托吧。關于委托的更多知識點,你可以從這里學習到—— the Unity documentation。
再添加下面的方法:
void OnDestroy() { if (enemyDelegate != null) { enemyDelegate(gameObject); } }
以上代碼的目的是為了銷毀一個游戲對象,如同Start()和Update()方法一樣,Unity會自動調用OnDestroy()這個方法。在這個方法中,我們先判斷委托變量的值是否不為null。如果是這樣的話,我們調用這個委托,將gameObject作為它的參數。所有注冊過這個委托的游戲對象都會得知敵人對象被銷毀了。
保存好腳本,返回Unity。
讓你的小怪獸們能對敵人開火
現在,小怪獸們能偵測到攻擊范圍內的敵人。為Monster prefab添加一個C#腳本組件,命名為ShootEnemies。
在VS中打開它,添加下面的代碼,目的是引用命名空間Generics。
using System.Collections.Generic;
添加一個集合變量,用于追中所有攻擊范圍內的敵人:
public List<GameObject> enemiesInRanges;
這個集合里面存儲了攻擊范圍內所有的敵人對象。
在Start()方法里對這個集合進行初始化。
enemiesInRanges = new List<GameObject>();
起先,小怪獸的射程內木有敵人,于是我們就創建了一個空的List。
接下來是向這個List中添加元素,在腳本中添加下面的代碼段:
// 1 void OnEnemyDestroy(GameObject enemy) { enemiesInRanges.Remove(enemy); } void OnTriggerEnter2D(Collider2D other) { // 2 if (other.gameObject.tag.Equals("Enemy")){ enemiesInRanges.Add(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate += OnEnemyDestroy; } } // 3 void OnTriggerExit2D(Collider2D other) { if (other.gameObject.tag.Equals("Enemy")){ enemiesInRanges.Remove(other.gameObject); EnemyDestructionDelegate del = other.gameObject.GetComponent<EnemyDestructionDelegate>(); del.enemyDelegate -= OnEnemyDestroy; } }
這段代碼分為3個小方法:
1. 在OnEnemyDestroy()方法中,我們移除了enemiesInRange中的enemy對象。當有敵人經過小怪獸的射程時,方法OnTriggerEnter2D()就會被調用。
2. 將敵人對象添加到enemiesInRange當中,并且將方法OnEnemyDestroy()添加到委托EnemyDestructionDelegate上。這是為了確保當敵人對象被銷毀的時候,方法OnEnemyDestroy()會被調用。你的小怪獸們不需要為已死的敵人浪費火力。
3. 在OnTriggerExit2D()方法中,我們將敵人對象enemy從當中enemiesInRange移除,并且移除之前添加到委托上方法。現在小怪獸們可以知道它射程內的敵人是哪些了。
保存好腳本,啟動游戲,看看我們之前做的行不行。召喚一只小怪獸,選中它,然后在Inspector面板中查看enemiesInRange這個變量的變化。
就像數綿羊那樣。圍欄(Fence )和綿羊(sheep)都由OpenClipArt提供。
為小怪獸選擇開火的目標
現在小怪獸們可以偵測到它射程之內的敵人,但問題是當有多個敵人存在它射程之內的時候,該怎么辦?
當然是對離餅干最近的敵人開火啦!
在VS中打開MoveEnemy.cs,添加一個新的方法來完成這個任務:
public float distanceToGoal() { float distance = 0; distance += Vector3.Distance( gameObject.transform.position, waypoints[currentWaypoint + 1].transform.position); for (int i = currentWaypoint + 1; i < waypoints.Length - 1; i++){ Vector3 startPosition = waypoints[i].transform.position; Vector3 endPosition = waypoints[i + 1].transform.position; distance += Vector3.Distance(startPosition, endPosition); } return distance; }
這個方法計算出了敵人尚未走完的路有多長。我們使用了Distatnce這個方法來計算兩個Vector3之間的距離。
·通過這個方法來決定小怪獸的攻擊目標。但是,現在你的小怪獸們無法攻擊敵人,什么事都做不了,這個問題在下一步中解決。
保存好腳本,返回Unity中,我們需要為小怪獸們配備射擊敵人的子彈。
為小怪獸們配備無盡的子彈
將 Images/Objects/Bullet1 拖拽到場景視圖中。將它的Z坐標設置為-2,在游戲過程中,我們需要不斷地產生新的子彈,X和Y坐標是在子彈產生時候設置的。
為Bullet1添加一個名為 BulletBehavior 的C#腳本組件,將下面的變量添加到腳本中:
public float speed = 10; public int damage; public GameObject target; public Vector3 startPosition; public Vector3 targetPosition; private float distance; private float startTime; private GameManagerBehavior gameManager;
變量 speed 指的是子彈的飛行速度,damage 指的是子彈對敵人造成的傷害。
Target、startPosition、 targetPosition 分別指的是:子彈的目標、初始坐標、目標的坐標。
distance 和 startTime 這兩個變量決定了子彈的當前坐標。當玩家消滅一個敵人的時候,我們通過操作 gameManager 這個變量來給予玩家獎勵。
在 Start() 方法中為這些變量賦值:
startTime = Time.time; distance = Vector3.Distance(startPosition, targetPosition); GameObject gm = GameObject.Find("GameManager"); gameManager = gm.GetComponent<GameManagerBehavior>();
我們將 startTime 設置為當前時間;distance變量的值為 startPosition 和 targetPosition 之間的距離;最后,我們獲取了GameManagerBehavior的實例。
在Update()方法中,添加下面的代碼來控制子彈的運動軌跡:
// 1 float timeInterval = Time.time - startTime; gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance); // 2 if (gameObject.transform.position.Equals(targetPosition)) { if (target != null){ // 3 Transform healthBarTransform = target.transform.FindChild("HealthBar"); HealthBar healthBar = healthBarTransform.gameObject.GetComponent<HealthBar>(); healthBar.currentHealth -= Mathf.Max(damage, 0); // 4 if (healthBar.currentHealth <= 0) { Destroy(target); AudioSource audioSource = target.GetComponent<AudioSource>(); AudioSource.PlayClipAtPoint(audioSource.clip, transform.position); gameManager.Gold += 50; } } Destroy(gameObject); }
- 計算出子彈的當前位置,這里我們還是使用
Vector3.Lerp
這個方法。 - 當子彈擊中目標的時候,我們會先驗證目標是否還存在。
- 獲取了目標的
HealthBar
組件,按子彈造成的傷害來削減目標的生命值。 - 當一個敵人的生命值減到零的時候,需要銷毀這個敵人對象,然后播放一個音效,最后給予玩家金幣獎勵。
保存好腳本,返回Unity中。
來些更大的子彈
假如等級高的小怪獸能發射較大的子彈,這是不是很酷呢?是的,我們能做到,因為這很簡單。
將 Hierarchy 視圖中的 Bullet1 拖拽到Project 視圖中的Prefab文件夾下,創造出一個子彈的prefab。刪除場景中的子彈對象,我們已經不再需要它。
利用 Bullet1 prefab再創建兩個prefab,分別命名為 Bullet2 和 Bullet3 。傳統的CTRL + C,CTRL + V命令在這里行不通。選中Bullet1后,按下快捷鍵CTRL + D,(duplicate 復制的意思),按下CTRL + D 兩次后,創建 Bullet2 和 Bullet3。因為Bullet2 和 Bullet3都是比較大的子彈,接下來,我們要為這兩個prefab設置新的子彈圖片。
選中Bullet2 ,在Inspector面板中,設置 Sprite Renderer 組件的Sprite為 Images/Objects/Bullet2。這樣,Bullet2的樣子會比Bullet1更大一些。
同上,將Bullet3 prefab的sprite設置為 Images/Objects/Bullet3。
之前在編寫Bullet Behavior腳本的時候,沒有進行設置 Damage 這個變量的值,接下來,分別設置這三種子彈造成的傷害值。
在Inspector面板中,對Bullet1 、Bullet2 、Bullet3 的Damage進行賦值,分別為10、15、20,或者隨你的便。
注意:級別越高的子彈造成的傷害越大。玩家需要將金幣花在刀刃上,優先升級那些位置好的小怪獸們。
子彈的大小與小怪獸的等級成正比。
提升子彈的威力
為不同等級的小怪獸分配威力不同的子彈,這樣小怪獸越強,就能越快地消滅敵人。
打開腳本 MonsterData.cs ,為 MonsterLevel 添加下面的變量:
public GameObject bullet; public float fireRate;
前者是指子彈的 prefab,后者是指小怪獸發射子彈的速率。保存好腳本,返回Unity,讓我們完成對小怪獸的配置。
在Project視圖中選中Monster prefab。在Inspector面板中,展開Monster Data腳本組件中的Levels數組,將所有元素的Fire Rate都設置為1,分別設置Elements0、Elements1、Elements2的Bullet為Bullet1、Bullet2、Bullet3。
配置好后的結果如下圖所示:
開火
打開腳本ShootEnemies.cs,添加下面的變量:
private float lastShotTime; private MonsterData monsterData;
像這兩個變量名所顯示的那樣,前者記錄了小怪獸上一次開火的時間,后者的類型為MonsterData,這里包含了該小怪獸的子彈類型,發射速率等等數據。
在Start()方法中為這兩個變量賦值:
lastShotTime = Time.time;
monsterData = gameObject.GetComponentInChildren<MonsterData>();
這里,我們設置lastShotTime為當前時間,然后獲取了該游戲對象的MonsterData 組件。
再添加下面的代碼,令小怪獸能夠對敵人開火:
void Shoot(Collider2D target) { GameObject bulletPrefab = monsterData.CurrentLevel.bullet; // 1 Vector3 startPosition = gameObject.transform.position; Vector3 targetPosition = target.transform.position; startPosition.z = bulletPrefab.transform.position.z; targetPosition.z = bulletPrefab.transform.position.z; // 2 GameObject newBullet = (GameObject)Instantiate(bulletPrefab); newBullet.transform.position = startPosition; BulletBehavior bulletComp = newBullet.GetComponent<BulletBehavior>(); bulletComp.target = target.gameObject; bulletComp.startPosition = startPosition; bulletComp.targetPosition = targetPosition; // 3 Animator animator = monsterData.CurrentLevel.visualization.GetComponent<Animator>(); animator.SetTrigger("fireShot"); AudioSource audioSource = gameObject.GetComponent<AudioSource>(); audioSource.PlayOneShot(audioSource.clip); }
- 獲取了子彈的初始坐標和目標所在坐標,將這兩個坐標的Z坐標設置為
bulletPrefab的Z坐標
。之前我們設置bullet prefab的Z坐標的原因是為了表現一種層次感,子彈所處的位置要比小怪獸和敵人更低。 方法開頭從MonsterData中獲取了bulletPrefab,再
以bulletPrefab創建出一個子彈對象。
將startPosition
和targetPosition
賦值給我們創建出來的子彈對象。- 讓游戲更生動:當小怪獸開火的時候播放一個射擊的動畫和音效。
整合所有的模塊
現在是時候該整合一切了,讓你的小怪獸能夠準確地朝著目標開火。
往ShootEnemies.cs腳本的Update()方法中添加下面的代碼:
GameObject target = null; // 1 float minimalEnemyDistance = float.MaxValue; foreach (GameObject enemy in enemiesInRange) { float distanceToGoal = enemy.GetComponent<MoveEnemy>().distanceToGoal(); if (distanceToGoal < minimalEnemyDistance) { target = enemy; minimalEnemyDistance = distanceToGoal; } } // 2 if (target != null) { if (Time.time - lastShotTime > monsterData.CurrentLevel.fireRate){ Shoot(target.GetComponent<Collider2D>()); lastShotTime = Time.time; } // 3 Vector3 direction = gameObject.transform.position - target.transform.position; gameObject.transform.rotation = Quaternion.AngleAxis( Mathf.Atan2(direction.y, direction.x) * 180 / Mathf.PI, new Vector3(0, 0, 1)); }
讓我們一步一步地來看這些代碼:
- 決定小怪獸開火的目標,這里我們采用了尋找最小數的算法。先將
minimalEnemyDistance設置為float.MaxValue,這樣就不會有比它更大的數出現了。遍歷集合中的所有敵人,當循環結束的時候,我們就可以找出距離餅干最近的敵人。
- 當前時間與小怪獸上次開火的時間間隔大于射擊速率的時候,調用
Shoot方法,
再將lastShotTime
設置為當前時間。 - 計算出小怪獸和目標之間的當前角度,然后旋轉小怪獸,讓小怪獸能夠一直面對著目標。
保存好腳本,啟動游戲。看你的小怪獸們正在奮力地保護你的餅干。好樣的,現在我們完成了整個工程。
從這個項目中我們學到了什么
從這里可以下載完整的項目。
現在我們這個教程就要結束了,我們完成了一個很棒的塔防游戲。
這個游戲我們還可以做出以下擴展:
1. 添加更多種類的敵人和小怪獸
2. 為敵人建立更多的通往餅干的道路
3. 為小怪獸們設置更多的級別
這些小小的擴展可以令我們的游戲更好玩。假如你以此教程為基礎創造出了屬于自己的新游戲,請在評論中分享你的鏈接,讓大家都能夠好好地體驗一回。
在這里你可以發現更多有趣的關于塔防游戲的想法。
感謝大家抽出時間來完成這篇教程。希望大家能夠提出更多好的想法,祝大家都能夠愉快地殺敵。
文章列表