使用unity創建塔防游戲(原譯)(part1)


塔防游戲非常地受歡迎,木有什么能比看着自己的防御毀滅邪惡的入侵者更爽的事了。
在這個包含兩部分的教程中,你將使用Unity創建一個塔防游戲。

你將會學到如何:

  • 創建一波一波的敵人
  • 使敵人隨着路標移動
  • 創建和升級防御塔,並將敵人銷毀

最后,你會有一個此類型游戲的框架,之后可在此基礎之上進行擴展。
img

最終效果

在本篇教程中,你將創建一個塔防游戲,敵人(小蟲子)會朝着你的餅干移動,你可以在一些戰略點上,使用金幣放置和升級你收下的小怪獸來進行防御。

玩家必須在小蟲子抵達餅干之前消滅它們,敵人會隨着波數的增加而變得更加強大。 游戲將在玩家在所有波數之后存活下來(游戲勝利),或者有5個敵人抵達餅干之后結束(游戲失敗)。

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

img

准備開始

如果你還沒有Unity5,請從Unity的官網下載。

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

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

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

img

注:一開始我也沒弄清楚什么意思,后來理解了是在
img
這里所示的窗口

Starter project – check!
Assets – check!
走向征服世界的第一步(你的塔防游戲...)已經准備好了!

可放置點的 X 標記

小怪獸只能放在標記有X的地方。
Project Brower中拖動Images\Objects\Openspot到場景中,目前來說放到哪個位置沒有關系。

Hierarchy中選中Openspot,在Inspector面板點擊 Add Component並且選擇 Physics 2D\Box Collider 2D。 Unity將會在場景中以綠色的線顯示盒子碰撞器。你將會使用這個碰撞器來檢測在某個點的鼠標點擊。
img

使用相同的步驟,添加一個Audio\Audio Source組件到 Openspot上。並且設置Audio Source’s AudioCliptower_place(可以在Audio文件夾中找到),記住不能勾選Play On Awake

你需要再創建11個放置點,每個都得重復上面的步驟,不過不用擔心,Unity有一個很好的解決辦法: Prefab!

OpenSpotHierarchy拖動到Project Browser里面的Prefabs文件夾,它的名字在Hierarchy將會變成藍色,用來標示它是和一個prefab相關聯的。像下面這樣:
gif

現在,你擁有了一個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)

結束后,你的場景應該像下面這樣:
img

放置小怪獸(防御塔)

為了使放置的工作簡單點兒,工程的Prefab文件下中包含了一個Monster的prefab。
img

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

每一個精靈代表了小怪獸不同的能力級別。Monster的prefab同時包含了一個Audio Source的組件,當怪獸發射激光的時候會觸發播放聲音。

現在你將創建一個腳本來在Openspot上放置一個小怪獸。

Project Browser中 選擇Openspot,在Inspector面板中,點擊 Add Component然后選擇New Script並重命名為PlaceMonster,選擇C#作為腳本語言並以此點擊CreateAdd。因為你是向prefab添加的腳本,所以場景中所有的Openspot都將會被附件該腳本。

雙擊剛才創建的腳本,在編輯器中打開。然后添加下面的這兩個變量

public GameObject monsterPrefab;
private GameObject monster;

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

一個位置一個怪獸

添加下面的方法來限制一個位置只能放置一個怪獸:

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

canPlaceMonster方法中,我們檢查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
    }
}

上面的代碼在玩家點擊或者Tap的時候放置一個怪獸,那么如何工作的呢?

  1. 當玩家點擊一個游戲對象的碰撞器的時候,Unity會自動的調用OnMouseUp方法
    2 . 當被調用之后,這個方法會檢查是否可以放置怪獸,如果可則放置一個新的怪獸
  2. 使用 Instantiate方法創建一個怪獸對象,這個方法會根據指定的prefab和指定的位置和旋轉角度創建一個對象。在本例中,我們拷貝了monsterPrefab ,並指定了當前游戲對象的位置,以及沒有旋轉,最后將結果轉換為GameObject類型存儲在monster變量中。
  3. 最后,調用PlayOneShot方法播放附加在AudioSource組件上的聲音特效。

現在我們的PlaceMonster腳本已經可以放置新的怪獸了,但還需要指定prefab這個步驟。

使用正確的Prefab

在代碼編輯器中保存,並返回Unity 。

下面給monsterPrefab 賦值,首先在Prefabs文件夾中選中OpenSpot,在Inspector面板中,點擊PlaceMonster (Script)組件的Monster Prefab屬性右邊的小圓圈按鈕,然后在彈出來的對話框中選擇Monster
gif

現在開始游戲,在 X 標記上點擊來創建一些怪物~
img

升級我們的怪物

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

我們需要一個腳本來作為怪物升級系統實現的基礎,來跟蹤管理怪物在各個級別的能力大小,當然還有怪物所處的當前等級。

現在來添加這個腳本 。

Project Browser中選中 Prefabs/Monster,添加一個新的C#腳本命名為MonsterData,在代碼編輯器中打開該腳本並添加下面的代碼在MonsterData 類的上面:

[System.Serializable]
public class MonsterLevel
{
    public int cost;
    public GameObject visualization;
}

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

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

定義怪物的等級

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

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

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

using System.Collections.Generic;

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

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

public List<MonsterLevel> levels;

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

在代碼編輯器中保存文件,並返回到Unity中配置每個階段.

Project Browser中選中Prefabs/Monster,然后在Inspector面板中我們可以在MonsterData (Script)組件看到Levels屬性,設置size為3
img

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

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

接下來,給visualization賦值。

在project browers中展開Prefabs/Monster來查看其子節點。拖拽子節點Monster0visualization屬性的Element 0
重復上面的動作Monster1Element 1Monster2Element 2,參考下面動圖中的演示:

gif

現在,當你選中了Prefabs/Monster,它應該看下像下面這樣子:
img

定義當前的等級

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

private MonsterLevel currentLevel;

在私有變量currentLevel中,我們存放怪物當前的等級信息。
現在設置currentLevel同時將它暴露給其他腳本使用,添加下面的代碼到MonsterData中:

//1
public MonsterLevel CurrentLevel
{
    //2
    get
    {
        return currentLevel;
    }

    //3
    set
    {
        currentLevel = value;

        int currentLevelIndex = levels.IndexOf(currentLevel);

        GameObject levelVisualization = levels[currentLevelIndex].visualization;
        for(var 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 會在創建prefab時立即被調用,而OnStart會等到對象作為場景的一部反的時候才會被調用。
這里我們需要在放置一個怪獸之前就要初始化一下,所以在OnEnable 中初始化。

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

保存文件返回到Unity中,運行項目並且放置一些怪獸,現在他們顯示正確的,也就是最低等級的精靈,如下圖:
img

升級小怪獸

返回到代碼編輯器,增加下面的方法到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 (nextLevel != null)
        {
            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,運行游戲,試試放置和升級怪物~
img

支付金幣 - Game Manager

目前而言,可以立即的建造和升級任意數目的怪獸,但這樣一來,就木有挑戰了。

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

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

在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()中增加下面的初始化語句,默認給玩家100金幣(當然你可以給更少的)

Gold = 100;

給腳本中的Label對象賦值

保持腳本文件並返回到Unity中。

在Hierarcy中,選中GameManager,在Inspector面板,點擊GoldLabel右邊的圓圈按鈕,在Select Text對話框中,選中Scene標簽頁,並選中GoldLabel
img

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

檢查玩家的“錢包"

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

private GameManagerBehavior gameManager;

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

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

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

收錢!

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

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

注意是兩個地方,在放置和升級的邏輯里各有一處。

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

這顯然是不能被允許的,只有當玩家有足夠的金幣時,才能放置或者升級我們的怪物。

切換到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;
}

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

敵人、波數和路標

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

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

通過路標建立路線

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

創建另外5個路標:

  • Waypoint1: (7, 2, 0)
  • Waypoint2: (7, -1, 0)
  • Waypoint3: (-7, 3, 0)
  • Waypoint4: (-7.3, -4.5, 0)
  • Waypoint5: (7, -4.5, 0)
    下面的截圖標示出了路標的位置以及最終的路線:
    img

召喚敵人

現在是時候去創建一些敵人來沿着上面的路線移動了。在Prefabs的文件夾中包含了一個Enemy的prefab。 它的位置坐標是(-20,0,0) ,所以新創建的敵人對象一開始在平面外面。

跟Monster的prefab一樣,Enemy的prefab同樣包含了一個AudioSource,一個精靈圖片(一會兒可以旋轉其方向)。
img

使敵人沿着路線移動

向Prefabs\Enemy新建一個名為MoveEnemy的C#腳本,使用代碼編輯器打開,並添加下面的變量定義:

[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)
    {
        // 3.a 
        currentWaypoint++;
        lastWaypointSwitchTime = Time.time;
        // TODO: Rotate into move direction
    }
    else
    {
        // 3.b 
        Destroy(gameObject);

        AudioSource audioSource = gameObject.GetComponent<AudioSource>();
        AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);
        // TODO: deduct health
    }
}

讓我們一步一步來看:

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

保存文件,並返回到Unity。

給敵人指明方向

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

在Hierarchy中選中Road,然后添加一個新的C#腳本命名為SpawnEnemy,並在代碼編輯器中打開,增加下面的變量:

public GameObject[] waypoints;

我們將使用waypoints來存放路標的引用。
保存文件返回Unity, 在Hierarchy中選中Road,將WayPoints數組的大小改為6,拖拽Road的孩子節點到想用的Element位置,Waypoint0對應Element0以此類推。
gif

現在我們已經有了路線的路標數組,注意到敵人不會退縮。。。
img

檢查一切順利

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

public GameObject testEnemyPrefab;

這里使用了testEnemyPrefab保存對Enemyprefab的引用。
使用下面的代碼,當腳本開始時添加一個敵人:

void Start () {
    Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;
}

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

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

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

原文地址:http://www.raywenderlich.com/107525/create-tower-defense-game-unity-part-1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM