文章出處

 How to Create a Tower Defense Game in Unity - Part1

原文作者:Barbara Reichart   

文章原譯:http://www.cnblogs.com/le0zh/p/create-tower-defense-game-unity-part-1.html

參考了這篇文章,我打算做一些改進,以及翻譯這篇文章的第2部分。如有不恰當的地方,歡迎各位指正。

 塔防游戲極為流行,沒有什么能比看著自己的防御塔消滅邪惡的入侵者更爽的事了。

你將會學習到

  • 創建一波一波的敵人
  • 令敵人沿著路標移動
  • 創建和升級防御塔,消滅你的敵人們。

最后,你會有一個此類型的游戲框架,你可以在此基礎上自行擴展!

小貼士:你必須具備Unity的基礎知識,例如如何添加游戲資源和組件,理解預設體(prefabs)以及一些C#的編程基礎。想要學習這些東西的朋友可以參考Chris LaPolloUnity教程。本文作者所使用的UnityOS X版本的,但本教程也適用于Windows版本的Unity

最終效果

在本教程中,你將創建一個塔防游戲,敵人們(小蟲子)會朝你的餅干移動,它們還會遇到你的幫手們——一些小怪獸。為了消滅這些入侵者,你可以在一些戰略點上,消耗一些金幣來放置和升級你的小怪獸。

消滅那些小蟲子,別讓它們碰你的餅干!敵人會隨著波數的增加而變得更強大。游戲將在玩家活到最后(勝利),或者有5個敵人抵達餅干后結束(失敗)。

下面是一張完成的游戲截圖:

快召喚你的小怪獸們!保護你的餅干啊!

準備開始

如果你還沒有安裝Unity5,請前往Unity的商店下載。

同時,請下載starter項目,解壓并使用Unity打開 TowerDefense-Part1-Starter 這個工程。

starter項目中包括了美術和聲音資源,同時還有預設的動畫以及一些幫助用的腳本,這些腳本跟塔防游戲沒有直接的關系,所以不會再本教程中詳細介紹。但是如果你想要更多的學習關于unity 2d動畫的創建,請參考Unity 2D 教程

項目中同時包含了一些 prefab供你稍后擴展用來創建游戲角色。最后,工程中包含背景和UI設置好的場景。

Scenes文件夾中找到并打開GameScene, 設置Game視圖的顯示比例為4:3來保證labels能夠正確的在背景中對齊,你在Game視圖中看到的應該如下所示:

鳴謝:

  • 工程中用到的美術資源是由Vicki Wenderlich提供的免費資源包。在這里——gameartguppy,你可以找到更多她的作品,都是棒棒噠!
  • 工程中的音樂資源是由 BenSound 提供的,他擁有不少美妙的配樂!
  • 感謝給力的 Michael Jasper 提供了關于 camera shake 的資料。

Starter工程、資源都已經到位了!

征服世界,額,這個應該是指你的塔防游戲的第一步,已經準備好了。

放置X標記點

只能在標記有 X 的地方召喚小怪獸。

先從Project Brower中依次選擇Image、Objects,將Openspot.png拖動到場景中。目前,隨便放哪都行。

在Hierarchy視圖中選中Openspot,在Inspector面板中點擊 Add Componet 并且依次選擇 Physics 2D\Box Collider 2D。Unity將會在場景中以綠色的線來顯示Box Collider 2D。這個碰撞器用來檢測在某個點的鼠標點擊。

如上圖所示,Unity能夠為碰撞器量體裁衣。這是不是很酷?

跟剛才一樣,把一個Audio\Audio Source組件添加到 Openspot上。在Inspector面板中設置Audio Source的屬性,將AudioClip設置為 Audio 文件夾中的 tower_place ,不要勾選 Play On Awake 。

你還需要創建11個這樣的召喚點,每個都得重復上面的步驟,不過不用擔心,Unity有個妙招:Prefab !

將 Openspot 從 Hierarchy視圖 拖動到位于 Project視圖的 Prefabs 文件夾中。在 Hierarchy視圖 中它的名字變成了藍色,用來表示它是和一個prefab相關聯的。如下圖所示:

現在,你擁有了一個prefab,你就可以創建任意多的拷貝。簡單的將OpenspotPrefabs文件夾中拖拽到場景中,再重復11次,一共在場景中創建12個召喚點。

現在,我們需要在Inspector面板中設置這12個召喚點的坐標,數據如下:

  • (-5.2, 3.5, 0)
  • (-2.2, 3.5, 0)
  • (0.8, 3.5, 0)
  • (3.8, 3.5, 0)
  • (-3.8, 0.4, 0)
  • (-0.8, 0.4, 0)
  • (2.2, 0.4, 0)
  • (5.2, 0.4, 0)
  • (-5.2, -3.0, 0)
  • (-2.2, -3.0, 0)
  • (0.8, -3.0, 0)
  • (3.8, -3.0, 0)

做好后,你的場景是這個樣子的:

召喚小怪獸(放置防御塔)

為了簡化放置怪獸的工作,工程的Prefab文件夾下包含了一個名為 Monster 的 prefab。

這就是工程中用到的名為Monster的 prefab 。

現在,它包含了一個空的游戲對象,由三種不同的精靈組成,和它們各自的射擊動畫。

每一個小精靈代表了小怪獸不同的能力級別。這個prefab中也包含了一個音頻組件(Audio Source component),當小怪獸發射激光的時候,就會播放此音效。

現在該創建一個腳本來控制在召喚點(Openspot)上召喚小怪獸。

在Project 視圖中選中 Prefab文件夾下的 Openspot。在Inspector 面板中,單擊 Add Component ,選擇 New Script ,將它命名為 PlaceMonster。腳本語言選擇 C Sharp ,在點擊 Create and Add。剛才我們為 Openspot prefab 添加了腳本組件后,場景中所有的 Openspot 都會擁有各自的腳本組件。真棒!

Inspector面板中雙擊剛才創建的腳本,用VS打開。注意,請雙擊下圖紅圈部分打開腳本。

往里面添加這兩個變量:

    public GameObject monsterPrefab;
    private GameObject monster;

你將使用monsterPrefab中的對象實例化一個拷貝來創建的一個小怪獸,然后保存在monster變量中,方便之后的操作。

一個蘿卜一個坑

現在讓我們添加一個方法,令一個召喚點只能召喚一只小怪獸。

    private bool canPlaceMonster()
    {
        return monster == null; 
    }

這個方法先判斷 monster這個變量的值是否為null。如果是的,這個召喚點是沒有小怪獸的,我們可以在此召喚一只。

再添加如下代碼,來執行當玩家點擊召喚點后,召喚一只小怪獸:

// 1
void OnMouseUp() 
{
        // 2
        if (canPlaceMonster()) 
        {
            // 3
            monster = (GameObject)
                Instantiate(monsterPrefab, transform.position, Quaternion.identity);
            // 4
            AudioSource audioSource = gameObject.GetComponent<AudioSource>();
            audioSource.PlayOneShot(audioSource.clip); 

            // todo: Deduct gold
        }
}    

上面的代碼在玩家點擊召喚點的時候,就在上面召喚一只小怪獸,那么這是怎么執行的呢?

  1. 當玩家在點擊了一個游戲對象的碰撞器時,Unity就會自動調用 OnMouseUp 方法。
  2. 當 OnMouseUp 方法被調用的時候,先判斷這個點能否召喚小怪獸,在 canPlaceMonster() 返回true的情況下就可以。
  3. 調用 Instantiate方法來創建一只小怪獸對象,這個方法會根據指定的prefab、指定的位置(position)和旋轉角度(rotation)創建一個對象。在本例中,我們拷貝了 monsterPrefab ,并指定位置為當前游戲對象(召喚點)的位置,不指定旋轉角度,然后將方法的返回值強轉為GameObject類型,并且存儲在monster這個變量中。
  4. 最后,調用 PlayOneShot 方法播放附加在召喚點上聲音特效。

現在, PlaceMonster腳本可以處理召喚小怪獸了,但我們還需要為此腳本中指定prefab。

 

使用正確的 Prefab

先保存腳本,在回到Unity中。

下面給變量monsterPrefab賦值,首先在Project面板,Prefabs文件夾中選中OpenSpot。

然后,在Inspector面板中,點擊PlaceMonster (Script)組件的Monster Prefab屬性右邊的小圓圈按鈕,然后在彈出來的對話框中選擇Monster

現在開始游戲,在X標記上點擊來召喚一些小怪獸。

成功啦!現在我們可以召喚小怪獸了。但它們看起來像一團漿糊,因為小怪獸所有的形態都疊在一起了。這個問題在下一步中解決。

升級你的小怪獸

在下面的圖片中,我們看到小怪獸在不同的等級有不同的外觀。

 

這些小家伙是不是萌萌噠!但如果有誰想偷吃餅干的話,它們就會痛下殺手。

我們需要編寫一個腳本來管理小怪獸升級的功能。我們需要跟蹤管理怪物在各個級別的能力大小,當然還有怪物所處的當前等級。

現在讓我們來添加這個腳本。

Project面板中選中Prefabs下的Monster這個Prefab,為它添加一個C#腳本組件,命名為MonsterData。在VS中打開它,然后將下面的代碼添加到MonsterData類的上方。

[System.Serializable]  
public class MonsterLevel
{
    public int cost; //召喚怪物所消耗的金幣
    public GameObject visualization;    //怪物在某個特定等級的視覺效果
}

這里定義了一個MonsterLevel類型,包含了費用(金幣,這個后面再說)以及對于某個特定等級的視覺效果。

我們添加了[System.Serializable]這個特性來使這個類的對象可以在Inspector面板中編輯。這可以使我們方便快速的改變MonsterLevel中的值,甚至在游戲運行過程中。這在調節游戲平衡性的時候特別的有用。

 

設定怪物的等級

我們將預先定義的MonsterLevel存儲在List<T>中。

為什么不簡單的使用數組MonsterLevel[]呢?首先我們會經常用到某個特定MonsterLevel對象的下標,當然如果使用數組編寫一點代碼來做這件事也不是特別困難。我們可以直接使用List對象的IndexOf()方法,沒有必要重新發明輪子了這次 :

重新發明輪子可不是一個好主意。

MonsterData.cs 文件的頂部添加下面的引用:

using System.Collections.Generic;

這將允許我們使用泛型的數據結構,所以可以在代碼中使用List<T>。

小貼士:泛型是C#中的一個很有用的功能,這允許我們定義類型安全的數據結構,并且不閑魚實際數據類型。涉及到泛型的常用的容器類有ListSet。如果你學習更多關于泛型的知識,請參閱Introduction to C# Generics

接下來添加下面的變量到MonsterData類,用來存儲MonsterLevel的列表。

public List<MonsterLevel> levels;

使用泛型,可以保證levels List 只能存放MonsterLevel 類型的對象。

保存好腳本文件,返回Unity中設置小怪獸的等級數據。

Proejct面板中,選中Prefabs文件夾下的Monster,然后在Inspector面板中,我們可以在MonsterData(腳本組件),可以看到Levels屬性,設置size為3。

接下來,設置每個等級的花費如下:

  • Element 0: 200
  • Element 1: 110
  • Element 2: 120

接下來給visualization這個變量賦值。

 在Project視圖中展開Prefabs/Monster來查看其子節點。拖拽子節點Monster0visualization屬性的Element 0

重復上面的動作Monster1Element 1Monster2Element 2,參考下面動圖中的演示:

當我們選中Prefabs/MonsterInspector面板中的顯示如下:(現在,我們配置好了小怪獸的等級數據)

定義當前的等級

切換回MonsterData.cs中,向MonsterData類中添加另外一個變量:

private MonsterLevel currentLevel;

在私有變量currentLevel中,我們存放怪物當前的等級信息。

現在我們需要讓其他腳本能夠調用這個變量,寫一個屬性對外使用。

     // 1
     public MonsterLevel CurrentLevel 
    {
        //  2
        get 
        {
            return currentLevel;
        }
        //  3
        set 
        {
            currentLevel = value;
            int currentLevelIndex = levels.IndexOf(currentLevel);

            //根據currentLevelIndex的值,來決定小怪獸的形態
            GameObject levelVisualization = levels[currentLevelIndex].visualization;
            for (int i = 0; i < levels.Count; i++)
            {
                if (levelVisualization != null)
                {
                    if (i == currentLevelIndex)
                    {
                        levels[i].visualization.SetActive(true);
                    }
                    else 
                    {
                        levels[i].visualization.SetActive(false);
                    }
                }
            }
        }
    }

看起來有很多C#代碼,我們慢慢來看:

  1. 為私有變量currentLevel定義一個屬性。當屬性被定義之后,你就能像其他變量一樣去調用,既可以CurrentLevel這樣在類的內部調用,也可以使用monster.CurrentLevel這樣在類的外部調用。然后還能自定義屬性的getter和setter方法,通過只提供一個getter,只有一個setter或者都有,來控制一個屬性是只讀的、只寫的或者讀寫的。
  2. 在getter方法中,直接返回私有變量currentLevel的值
  3. 在setter方法中,給currentLevel賦值。先拿到當前等級的下標,然后遍歷levels數組,依據currentLevelIndex的值來決定小怪獸的形態,好處就在于不管什么時候誰設置了currentLevel,精靈會自動更新。屬性確實很好用!

添加下面的OnEnable的一個實現:

void OnEnable()
{
    CurrentLevel = levels[0];
}

這里設置了CurrentLevel的默認值,確保它只顯示正確的那個精靈圖片。

注意:  

OnEnable中而不是OnStart中初始化屬性的值,原因是:當prefabs被實例化時,腳本中幾個自帶方法調用次序的問題,OnEnable會在Start之前被調用。

OnEnable會在Unity創建小怪獸的prefab時立即被調用,而Start會等到小怪獸對象作為場景的一部分的時候才會被調用。我們能夠確定的是,先有小怪獸的prefab,才會有小怪獸的對象。
小怪獸被召喚出來之前,我們需要確定它的有關數據:如等級、召喚它耗費的金幣等等,于是在召喚一只小怪獸之前就要先把這些數值設置好,所以選擇在OnEnable() 方法中初始化。

  注意OnEnable中的大小寫,如果大小寫不對,方法不會被調用!

     返回到Unity中,運行項目并且召喚一些小怪獸,現在它們外觀顯示正確的,也就是最低等級的形態,如下圖:

升級小怪獸

返回到代碼編輯器,增加下面的方法到MonsterData中:

    public MonsterLevel getNextLevel() 
    {
        int currentLevelIndex = levels.IndexOf(currentLevel);
        int maxLevelIndex = levels.Count - 1;
        if (currentLevelIndex < maxLevelIndex)
        {
            return levels[currentLevelIndex + 1];
        }
        else 
        {
            return null;
        }
    }

getNextLevel方法中,我們首先拿到當前等級的下標以及最高等級的下標,以此判斷如果當前不是最高等級時,返回下個等級,否則返回null。

我們可以使用該方法判斷是否可以升級到下一個等級。

添加下面的方法增加小怪獸的等級:

    public void increaseLevel() 
    {
        int currentLevelIndex = levels.IndexOf(currentLevel);
        if (currentLevelIndex < levels.Count - 1)
        {
            CurrentLevel = levels[currentLevelIndex + 1];
        }
    }

這里我們獲取當前等級的下標,然后確保它不會大于最高等級的下標,如果不大于,則將CurrentLevel設置為下一個等級。

測試是否可以升級

保存剛才的腳本文件,返回到PlaceMonster.cs文件,增加下面的方法:

    private bool canUpdateMonster() 
    {
        if (monster != null)
        {
            MonsterData monsterData = monster.GetComponent<MonsterData>();
            MonsterLevel nextLevel = monsterData.getNextLevel();
            if (null != nextLevel) //更高的等級存在
            {
                return true;
            }
        }
        return false;
    }

首先,檢查monster變量是否為null,如果為null則肯定不能升級了,如果不為null則獲取其MonsterData組件,并檢查更高的等級是否存在,如果getNextLevel方法返回的值不是null則說明更高的等級存在(返回true),否則不存在(返回false)。

為了能夠升級,在PlaceMonster中的OnMouseUp中添加else if分支:

    void OnMouseUp() 
    {
        if (canPlaceMonster()) 
        {
            // ... 這里根原來的代碼一樣,先省略
        }
        else if (canUpdateMonster()) 
        {
            monster.GetComponent<MonsterData>().increaseLevel();
            AudioSource audioSource = gameObject.GetComponent<AudioSource>();
            audioSource.PlayOneShot(audioSource.clip);
        }
    }

首先我們通過canUpdateMonster檢查能否升級,如果可以升級,則通過調用MonsterData組件的increaseLevel方法升級怪物,最后播放一次聲音特效。

保存好腳本,回到Unity中。運行游戲,現在我們可以召喚小怪獸并升級它們了。

支付金幣 – Game Manager 

現在,我們可以召喚和升級任意數目的小怪獸,但這樣用來,就木有挑戰了。

我們接下來就處理金幣的問題,為例維護金幣的信息,你不得不要在不同的游戲對象之間共享數據信息。
下面的圖片顯示了所有需要金幣信息的游戲對象:

上圖中被標記的游戲對象都需要知道玩家擁有的金幣數量。

我們將使用一個其他對象都能訪問的共享對象來存儲這類數據。

在Hierarchy面板中,右鍵選擇Create Empty,并命名為GameManager

添加一個C#腳本組件GameManagerBehavior到剛剛創建的GameManager上,然后打開并編輯這個腳本。

因為我們需要使用一個label顯示玩家所擁有的金幣數,所以在文件的頂部增加下面的引用:

using UnityEngine.UI;

這將允許我們使用UI相關的類型,比如Text用做顯示用的label,接下來在類中添加下面的變量:

public Text goldLabel;

這個變量存儲了對Text組件的引用,將用于顯示玩家所擁有的所有金幣數。

現在GameManager已經可以操作label了,但是我們如何保證變量中存儲的金幣數和label顯示的數量同步呢,我們創建一個屬性:

private int gold;

public int Gold
{
    get { return gold; }
    set
    {
        gold = value;
        goldLabel.GetComponent<Text>().text = "GOLD: " + gold;
    }
}

是不是看起來很熟悉,這個跟我們在Monster中定義的CurrentLevel比較相像,首先我們創建一個私有的變量gold用來存儲當前所有的金幣數,然后定義一個名為Gold的屬性,并提供getter和setter方法。

在getter方法中簡單的直接返回gold,setter方法則比較有趣了,除了設置gold的值,還設置了goldLabel的顯示。
這樣就保持了金幣數和顯示的同步。

Start()中增加下面的初始化語句,默認給玩家1000金幣(當然你可以給更少的)

Gold = 1000;

給腳本中的Label對象賦值

保存腳本文件并返回到Unity中。

在 Hierarchy面板中,選中GameManager 對象。在Inspector面板,點擊GoldLabel右邊的圓圈按鈕,在Select Text對話框中,選中Scene標簽頁,并選中GoldLabel。

運行游戲,可以看到金幣的顯示如下:

檢查玩家的錢包

打開PlaceMonster.cs文件,增加下面這行代碼:

private GameManagerBehavior gameManager;

我們將通過變量gameManager來訪問場景中GameManager的GameManagerBehavior組件,并在Start方法中初始化:

void Start ()
{
    gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
}

我們使用GameObject.Find方法,找到名為GameManager的游戲對象,然后定位到其GameManagerBehavior組件并存到一個私有變量中,供稍后使用。

注意:你可以在Unity中為gameManager這個變量賦值,或者是添加一個靜態方法,它返回一個GameMmanagerBehavior 類型的單例實例。在上面的代碼塊中,我們使用了Find 這個黑馬方法,雖然它的執行過程耗時,但它卻很方便,不需要頻繁地調用。

收錢

我們還沒有減少金幣數,所以在OnMouseUp() 方法里,將原來的TODO注釋改為下面的代碼:

//todo: Deduct gold
gameManager.Gold -= monster.GetComponent<MonsterData>().CurrentLevel.cost;

注意是兩個地方,在放置和升級的邏輯里各有一處,在if 和else if分支里面都各添加一處。

保存文件并返回unity,升級一些小怪獸并注意觀察金幣數目的變化。現在我們能夠減少金幣數了,但是。。。玩家可以一直召喚小怪獸(只要有空位),金幣數甚至可以變成負數。

這顯然是不允許的!只有當玩家有足夠金幣時,才能召喚和升級小怪獸。

花在小怪獸上的金幣

切換到PlaceMonster.cs腳本,更新canPlaceMonstercanUpdateMonster方法如下,就是加上檢查剩余金幣是否足夠的條件。

private bool canPlaceMonster()
{
    int cost = monsterPrefab.GetComponent<MonsterData>().levels[0].cost;
    return monster == null && gameManager.Gold >= cost; //確保金幣足夠
}

private bool canUpdateMonster()
{
    if (monster != null)
    {
        MonsterData monsterData = monster.GetComponent<MonsterData>();
        MonsterLevel nextLevel = monsterData.getNextLevel();
        if (nextLevel != null)
        {
            return gameManager.Gold >= nextLevel.cost; //確保金幣足夠
        }
    }
    return false;
}

保存,并運行游戲,試試還能不能無限添加怪物。

現在我們召喚和升級小怪獸的數目取決于金幣的數量。

塔防游戲的要素:敵人、波數和路標

是時候給敵人“鋪路”了。敵人首先在第一個路標的地方出現,然后向下一個路標移動并重復這個動作,直到他們抵達你的餅干。
我們將通過下面的手段使敵人行軍起來:

    1. 定義敵人移動的路線
    2. 使敵人沿著路線移動
    3. 旋轉敵人,使他們看起來是向前方行進

通過路標建立路線

  在Hierarcy視圖中右擊空白處,選擇Create Empty創建一個新的空游戲對象,命名為Road,并確保其坐標為 (0, 0 , 0) 。接下來,右擊Road并創建一個空游戲對象,命名為Waypoint0,設置其坐標為 (-12, 2 ,0) ,這是敵人開始進攻的起點。

    

按照相同的方法再創建5個路標:

  • Waypoint1: (7, 2, 0)
  • Waypoint2: (7, -1, 0)
  • Waypoint3: (-7, 3, 0)
  • Waypoint4: (-7.3, -4.5, 0)
  • Waypoint5: (7, -4.5, 0)

下面的截圖標示出了路標的位置以及最終的路線:

召喚敵人

  現在我們該創建一些敵人沿著剛才設定的路線前進。在Prefabs的文件夾中包含了一個Enemy的prefab。 它的位置坐標是(-20,0,0) ,所以新創建的敵人對象一開始在游戲界面的外面。

  跟Monster的prefab一樣,Enemy的prefab同樣包含了一個AudioSource,一個精靈圖片(一會兒可以旋轉其方向,并且不必旋轉敵人頭上的血條)。

    

令敵人沿著路線移動

  為Prefabs文件夾下的 Enemy prefab 添加一個名為MoveEnemy的C#腳本組件,使用VS打開,并添加下面的變量定義:

    [HideInInspector]
    public GameObject[] waypoints; //所有的路標
    private int currentWaypoint = 0; //敵人當前所在的路標
    private float lastWaypointSwitchTime; //敵人經過上一個路標的時刻
    public float speed = 1.0f;  //敵人的移動速度

  waypoints以數組的形式存儲了所有的路標,它上面的HideInInspector特性確保了我們不會在inspector面板中不小心修改了它的值,但是我們仍然可以在其他腳本中訪問。

  currentWaypoint記錄了敵人當前所在的路標,lastWaypointSwitchTime記錄了當敵人經過上一個路標的時刻,最后使用speed存儲敵人的移動速度。

` 在Start() 中添加這行代碼:

        lastWaypointSwitchTime = Time.time;

  這里將lastWaypointSwitchTime初始化為當前時間。

  為了令敵人沿著路線移動,將下列代碼添加到Update() 中:

        // 1  從路標數組中,取出當前路段的開始路標和結束路標
        Vector3 startPosition = waypoints[currentWaypoint].transform.position;
        Vector3 endPosition = waypoints[currentWaypoint + 1].transform.position;
    
        // 2 計算出通過整個路段的距離
        float pathLength = Vector3.Distance(startPosition, endPosition);
        float totalTimeForPath = pathLength / speed; //計算出通過整個路段所需要的時間
        float currentTimeOnPath = Time.time - lastWaypointSwitchTime;
        // 計算出當前時刻應該在的位置
        gameObject.transform.position = Vector3.Lerp(startPosition, endPosition, currentTimeOnPath / totalTimeForPath);
    
        // 3 檢查敵人是否已經抵達結束路標
        if (gameObject.transform.position.Equals(endPosition))
        {
            //敵人尚未抵達最終的路標
            if (currentWaypoint < waypoints.Length - 2)
            {
                currentWaypoint++;
                lastWaypointSwitchTime = Time.time;
            }
            else  //敵人抵達了最終的路標
            {
                Destroy(gameObject);

                AudioSource audioSource = gameObject.GetComponent<AudioSource>();
                AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);

                //TODO: deduct health
            }
        }

讓我們一步一步來看:

  1. 從路標數組中,取出當前路段的開始路標和結束路標。
  2. 計算出通過整個路段所需要的時間totalTimeForPath(使用 距離除以速度 的公式),計算出敵人在當前路段行進的時間currentTimeOnPath,使用Vector3.Lerp插值,通過currentTimeOnPath / totalTimeForPath 計算出當前時刻應該在的位置。
  3. 檢查敵人是否已經抵達結束路標,如果是,則有兩種可能的場景:
    A. 敵人尚未抵達最終的路標,所以增加currentWayPoint并更新lastWaypointSwitchTime,稍后我們要增加旋轉敵人的代碼使他們朝向前進的方向。
    B. 敵人抵達了最終的路標,就銷毀敵人對象,并觸發聲音特效,稍后我們要增加減少玩家生命值的代碼。

保存文件,并返回到Unity。

給敵人指明方向

  現在,敵人還不知道路標的次序。

  為Hierarchy視圖中的Road對象添加一個命名為SpawnEnemy的C#腳本組件,用VS打開,添加下面的變量:

    public GameObject[] waypoints;  //存儲路標

  游戲中路標的引用按照合理的順序存儲在waypoints這個數組中。

  保存好腳本,返回Unity中。選中Hierarchy視圖中的Road對象,在Inspector面板中,將Waypoints數組的Size設置為6。拖拽Road的孩子節點到想用的Element位置,Waypoint0對應Element0以此類推。如下圖所示:

  

  現在我們已經有了路線的路標數組,注意到敵人不會退縮,為了你那塊甜餅,它們不畏死亡。

  

  你們這些不怕死的家伙,盡管來吧,看我的小怪獸們如何消滅你們。

檢查一切順利

  打開SpawnEnemy腳本,添加下面的變量:

    public GameObject testEnemyPrefab; //保存對Enemyprefab的引用

  ·使用下面的代碼,當腳本開始時添加一個敵人:

    // Use this for initialization
    void Start () {
        //實例化一個敵人對象,并將路標告訴敵人
        Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;
    }

  上面的代碼使用testEnemyPrefab實例化一個敵人對象,并將路標賦值給它。

  保存文件返回Unity,在Hierarchy視圖中選中Road對象并將Enemy prefab賦值給testEnemyPrefab。
  運行游戲,可以看到敵人已經能夠沿著路線移動了:

      

  nice,但是有沒有注意到敵人移動的時候沒有朝向移動的方向。。沒有關系,這個問題將在part2部分修復。

小結:

  為了完成自制的塔防游戲,我們已經很好的完成了不少的工作。

  玩家可以召喚小怪獸,但數量有限;敵人會朝著你的餅干前進;玩家擁有金幣,可以用來升級小怪獸。

  在第二部分,我們的主要工作:創建數波大量的敵人,并且消滅它們。

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(1) 人氣()