程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 使用Unity創建塔防游戲(Part2),unitypart2

使用Unity創建塔防游戲(Part2),unitypart2

編輯:C#入門知識

使用Unity創建塔防游戲(Part2),unitypart2


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

    

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

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

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

  保存好腳本,返回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方法是這段代碼的主體。

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

  保存好腳本,返回Unity中。

來些更大的子彈

  假如等級高的小怪獸能發射較大的子彈,這是不是很酷呢?是的,我們能做到,因為這很簡單。

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

  利用 Bullet1 prefab再創建兩個prefab,分別命名為 Bullet2Bullet3 。注意:這裡為什麼不是通過復制黏貼來創建?先看下圖,右擊Bullet1之後,沒有出現Copy和Paste這樣的選項。沒關系,我們可以通過Import New Asset... 這個選項來實現。

    

  選擇Bullet1.prefab,點擊Import就可以了。

  

  Unity會自動將我們剛才導入的prefab命名為Bullet2,重復剛才的方法,再創建Bullet3。

  

  選中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的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. 為小怪獸們設置更多的級別

  這些小小的擴展可以令我們的游戲更好玩。假如你以此教程為基礎創造出了屬於自己的新游戲,請在評論中分享你的鏈接,讓大家都能夠好好地體驗一回。

  在這裡你可以發現更多有趣的關於塔防游戲的想法。

   感謝大家抽出時間來完成這篇教程。希望大家能夠提出更多好的想法,祝大家都能夠愉快地殺敵。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved