使用Unity建立塔防遊戲(Part2)

How to Create a Tower Defense Game in Unity – Part 2

原文地址:https://www.raywenderlich.com/107529/unity-tower-defense-tutorial-part-2算法

  歡迎你們來查看,使用Unity建立塔防遊戲(第二篇)。在第一篇的結尾,咱們已經能夠召喚和升級小怪獸,召喚一個敵人朝着餅乾前進的敵人。數組

  可是這個敵人沒有方向感,讓人感受怪怪的。接下來,咱們要作的是召喚一波一波的敵人,而後令小怪獸可以消滅它們,都是爲了保護你那塊美味的餅乾。ide

準備工做

  用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 這個方法是將場景中敵人對象的角度進行旋轉,讓敵人看起來有方向感。咱們一步一步地來看:spa

  1. 計算出下一個路標與當前路標之間的向量之差,敵人會沿着這個向量前往下一個路標。
  2. 計算敵人要旋轉的角度,即敵人當前的方向與newDirection之間的夾角的度數。調用Mathf.Atan2 來計算,參數爲newDirection的X座標和Y座標,但返回的結果是以弧度爲單位的。所以,咱們須要將結果乘以 180 / Math.PI ,將弧度轉化爲角度。 
  3. 最後,咱們獲取敵人對象的子對象——Sprite,令它圍繞Z軸旋轉的度數爲rotationAngle。這裏咱們調用Quaternion.AngleAxis來完成旋轉的工做,它的第一個參數就是咱們以前計算出的角度。注意,爲何是將子對象旋轉,而不是敵人對象?這是爲了保證敵人的血條始終保持水平,接下來咱們要爲敵人添加血條了。

    

  將Update() 中的註釋 // TODO: Rotate into move direction 替換成調用咱們剛寫好的函數—— RotateIntoMoveDirection3d

                RotateIntoMoveDirection();

  保存好腳本,返回Unity,運行遊戲,看敵人如今有方向感了。這樣纔算是朝着餅乾前進。code

  

  才一個小兵?這怎行,要來就來一大羣。在通常的塔防遊戲中,都是每一波敵人都是一大羣。orm

告知玩家——敵人來了

  在一大羣敵人出現以前,咱們應該先告知玩家——敵人來了。同時,咱們須要顯示這是第幾波敵人,在界面的右上角顯示。

  在腳本中,有很多須要用到波數的地方,咱們先在GameManager的腳本組件GameManagerBehavior中添加有關波數的代碼。

  用VS打開GameManagerBehavior.cs,而後添加下面兩個變量:

    public Text waveLable;
    public GameObject[] nextWaveLabels;

  顯示在屏幕右上角的波數會存儲在waveLabel 這個變量中。 nextWaveLabels 這個數組保存了兩個遊戲對象。在一波新的敵人到來以前,它們會構成一個文字合併的動畫,以下圖所示:

  

  保存好腳本,返回Unity。選中Hierarchy視圖中的GameManager,在Inspector面板中,點擊Wave Label右側的小圓圈,而後從彈出的Text對話框中的Scene標籤頁下選擇 WaveLabel

  將NextWave LabelsSize 設置爲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);
        }

  讓咱們一步一步來理解這段代碼:

  1. 得到當前波數,並判斷是否未到最後一波。
  2. 若是是這樣的話,先計算距離上一個敵人的建立過去了多少時間,而且判斷是否到了建立下一個敵人的時間。這取決於兩個條件:1、若是已建立的敵人數量爲0,而且timeInterval大於timeBetweenWaves;2、判斷timeInterval是否大於spawnInterval。不管如何,前提是這波敵人還沒有被建立完畢。
  3. 假如符合2中的條件,就以enemyPrefab爲拷貝,實例化一個敵人對象,賦予敵人對象有關路標的信息,而且將已建立的敵人數量加1。
  4. 若全部敵人都已被建立,但場景中找不到標籤爲Enemy的遊戲對象,說明這波敵人都已玩家消滅。咱們就要準備建立下一波敵人,而且給予玩家金幣數量增長百分之十。
  5. 玩家消滅了最後一波敵人,播放遊戲勝利的動畫。

設置建立單個敵人的時間間隔

  保存好腳本,返回Unity,選中Hierarchy視圖中的Road對象,在Inspector面板中,將數組WavesSize設置爲4。

  接下來,依次爲數組的4個元素賦值。將名爲Enemy的prefab賦值給Enemy Prefab,分別設置Spawn IntervalMax 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方法是這段代碼的主體。

  1. 當玩家掉血的時候,咱們使用CameraShake這個組件來製造一個很棒的晃動效果。(這個晃動效果是爲了警告玩家,小蟲子正在吃掉你的餅乾。)這個腳本也被包含在咱們的工程內,但本文不做介紹。
  2. 更新私有字段health的值,以及屏幕左上角的血量顯示。
  3. 當玩家血量被扣光的時候,且遊戲未結束,先設置 gameOver 的值爲true,再觸發遊戲失敗的動畫。
  4. 將一隻綠色的小怪物從餅乾上移除。就能夠作得簡單點的話,咱們能夠只是隱藏它們,當咱們須要爲玩家加血的時候,就能夠將從新它們顯示出來。

  在Start()中初始化Health

        Health = 5;

  在遊戲開始的時候,玩家的血量爲5。

  有了這個屬性,當小蟲子抵達餅乾的時候,咱們就能夠更新玩家的血量了。保存好腳本,在VS中打開MoveEnemy.cs這個腳本。

更新玩家的血量

  將MoveEnemy.csUpdate()方法內部的註釋:// 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個子對象拖拽賦值給GameManagerHealth Indicator數組。咱們用5只正在開心地啃着餅乾的青色小蟲子來表示玩家的血量。玩家受到一次傷害,就減小一隻青色的小蟲子。

  運行遊戲,讓那些小蟲子衝向餅乾,什麼都別作,直到遊戲結束。

  

小怪獸的戰鬥:消滅那些小蟲子

  該召喚小怪獸?仍是讓小蟲子前進?如今咱們的小怪獸仍是紙老虎,咱們要作的是讓小怪獸們可以消滅那些小蟲子。

  咱們先要把如下幾件事情作好:

  • 給小蟲子一個血條,讓玩家能看出敵人的強弱。
  • 讓小怪獸可以發現它攻擊範圍內的敵人們
  • 決定朝那個敵人開火
  • 無盡的子彈

顯示敵人的血條

  咱們用兩張圖片來顯示血條,一張是暗的,用於顯示血條的背景,另外一張是綠色較小的細長圖片,咱們經過縮放它的長度來與敵人當前血量匹配。

  將Project視圖中的Prefabs\Enemy拖到場景中。

  將Images\Objects\HealthBarBackground拖拽到Hierarchy視圖中的Emeny對象上,令HealthBarBackground做爲Enemy的子對象。

  在Inspector面板中,將HealthBarBackgroundPosition設置爲 (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重複以上步驟。

  如今全部的設置都已完成,你的小怪獸們能夠偵測到射程內的敵人。  

  還有一件事情要作:在腳本中告知小怪獸敵人是否被消滅,當它們的射程內沒有敵人的時候,不必一直開火。

  爲EnemyEnemy2這兩個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);
        }
  1. 計算出子彈的當前位置,這裏咱們仍是使用 Vector3.Lerp 這個方法。
  2. 當子彈擊中目標的時候,咱們會先驗證目標是否還存在。
  3. 獲取了目標的 HealthBar 組件,按子彈形成的傷害來削減目標的生命值。
  4. 當一個敵人的生命值減到零的時候,須要銷燬這個敵人對象,而後播放一個音效,最後給予玩家金幣獎勵。

  保存好腳本,返回Unity中。

來些更大的子彈

  假如等級高的小怪獸能發射較大的子彈,這是否是很酷呢?是的,咱們能作到,由於這很簡單。

  將 Hierarchy 視圖中的 Bullet1 拖拽到Project 視圖中的Prefab文件夾下,創造出一個子彈的prefab。刪除場景中的子彈對象,咱們已經再也不須要它。

  利用 Bullet1 prefab再建立兩個prefab,分別命名爲 Bullet2Bullet3 。傳統的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進行賦值,分別爲十、1五、20,或者隨你的便。

  注意:級別越高的子彈形成的傷害越大。玩家須要將金幣花在刀刃上,優先升級那些位置好的小怪獸們。

  

  子彈的大小與小怪獸的等級成正比。

提高子彈的威力

   爲不一樣等級的小怪獸分配威力不一樣的子彈,這樣小怪獸越強,就能越快地消滅敵人。

  打開腳本 MonsterData.cs ,爲 MonsterLevel 添加下面的變量:

    public GameObject bullet;
    public float fireRate;

  前者是指子彈的 prefab,後者是指小怪獸發射子彈的速率。保存好腳本,返回Unity,讓咱們完成對小怪獸的配置。

  在Project視圖中選中Monster prefab。在Inspector面板中,展開Monster Data腳本組件中的Levels數組,將全部元素的Fire Rate都設置爲1,分別設置Elements0、Elements一、Elements2的BulletBullet1Bullet2Bullet3

  配置好後的結果以下圖所示:

  

開火

  打開腳本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);
    }
  1. 獲取了子彈的初始座標和目標所在座標,將這兩個座標的Z座標設置爲 bulletPrefab的Z座標。以前咱們設置bullet prefab的Z座標的緣由是爲了表現一種層次感,子彈所處的位置要比小怪獸和敵人更低。
  2. 方法開頭從MonsterData中獲取了bulletPrefab,bulletPrefab建立出一個子彈對象。startPosition 和 targetPosition 賦值給咱們建立出來的子彈對象。
  3. 讓遊戲更生動:當小怪獸開火的時候播放一個射擊的動畫和音效。

整合全部的模塊

  如今是時候該整合一切了,讓你的小怪獸可以準確地朝着目標開火。

  往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));
        }

  讓咱們一步一步地來看這些代碼:

  1. 決定小怪獸開火的目標,這裏咱們採用了尋找最小數的算法。先將 minimalEnemyDistance設置爲float.MaxValue,這樣就不會有比它更大的數出現了。遍歷集合中的全部敵人,當循環結束的時候,咱們就能夠找出距離餅乾最近的敵人。
  2. 當前時間與小怪獸上次開火的時間間隔大於射擊速率的時候,調用Shoot方法, 再將lastShotTime設置爲當前時間。
  3. 計算出小怪獸和目標之間的當前角度,而後旋轉小怪獸,讓小怪獸可以一直面對着目標。

  保存好腳本,啓動遊戲。看你的小怪獸們正在奮力地保護你的餅乾。好樣的,如今咱們完成了整個工程。

  

從這個項目中咱們學到了什麼

  從這裏能夠下載完整的項目。

  如今咱們這個教程就要結束了,咱們完成了一個很棒的塔防遊戲。

  這個遊戲咱們還能夠作出如下擴展:

   1. 添加更多種類的敵人和小怪獸

   2. 爲敵人創建更多的通往餅乾的道路

   3. 爲小怪獸們設置更多的級別

  這些小小的擴展能夠令咱們的遊戲更好玩。假如你以此教程爲基礎創造出了屬於本身的新遊戲,請在評論中分享你的連接,讓你們都可以好好地體驗一回。

  在這裏你能夠發現更多有趣的關於塔防遊戲的想法。

   感謝你們抽出時間來完成這篇教程。但願你們可以提出更多好的想法,祝你們都可以愉快地殺敵。

相關文章
相關標籤/搜索