塔防游戲非常地受歡迎,木有什么能比看着自己的防御毀滅邪惡的入侵者更爽的事了。
在這個包含兩部分的教程中,你將使用Unity創建一個塔防游戲。
你將會學到如何:
- 創建一波一波的敵人
- 使敵人隨着路標移動
- 創建和升級防御塔,並將敵人銷毀
最后,你會有一個此類型游戲的框架,之后可在此基礎之上進行擴展。
最終效果
在本篇教程中,你將創建一個塔防游戲,敵人(小蟲子)會朝着你的餅干移動,你可以在一些戰略點上,使用金幣放置和升級你收下的小怪獸來進行防御。
玩家必須在小蟲子抵達餅干之前消滅它們,敵人會隨着波數的增加而變得更加強大。 游戲將在玩家在所有波數之后存活下來(游戲勝利),或者有5個敵人抵達餅干之后結束(游戲失敗)。
下面是一張完成的游戲截圖:
准備開始
如果你還沒有Unity5,請從Unity的官網下載。
同時,下載這個starter項目,解壓縮並且使用unity打開TowerDefense-Part1-Starter
這個工程。
starter項目中包括了美術和聲音資源,同時還有預設的動畫以及一些幫助用的腳本,這些腳本跟塔防游戲沒有直接的關系,所以不會再本教程中詳細介紹。但是如果你想要更多的學習關於unity 2d動畫的創建,請參考Unity 2D 教程。
項目中同時包含了一些 prefab
供你稍后擴展用來創建游戲角色。最后,工程中包含背景和UI設置好的場景。
在Scenes
文件夾中找到並打開GameScene
, 設置Game視圖的顯示比例為4:3來保證labels能夠正確的在背景中對齊,你在Game視圖中看到的應該如下所示:
注:一開始我也沒弄清楚什么意思,后來理解了是在
這里所示的窗口
Starter project – check!
Assets – check!
走向征服世界的第一步(你的塔防游戲...)已經准備好了!
可放置點的 X 標記
小怪獸只能放在標記有X的地方。
從Project Brower
中拖動Images\Objects\Openspot
到場景中,目前來說放到哪個位置沒有關系。
在Hierarchy
中選中Openspot
,在Inspector
面板點擊 Add Component
並且選擇 Physics 2D\Box Collider 2D
。 Unity將會在場景中以綠色的線顯示盒子碰撞器。你將會使用這個碰撞器來檢測在某個點的鼠標點擊。
使用相同的步驟,添加一個Audio\Audio Source
組件到 Openspot
上。並且設置Audio Source’s AudioClip
為 tower_place
(可以在Audio
文件夾中找到),記住不能勾選Play On Awake
。
你需要再創建11個放置點,每個都得重復上面的步驟,不過不用擔心,Unity有一個很好的解決辦法: Prefab!
將OpenSpot
從Hierarchy
拖動到Project Browser
里面的Prefabs
文件夾,它的名字在Hierarchy
將會變成藍色,用來標示它是和一個prefab相關聯的。像下面這樣:
現在,你擁有了一個prefab,你就可以創建任意多的拷貝。簡單的將Openspot
從Prefabs
文件夾中拖拽到場景中,再重復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同時包含了一個Audio Source
的組件,當怪獸發射激光的時候會觸發播放聲音。
現在你將創建一個腳本來在Openspot
上放置一個小怪獸。
在Project Browser
中 選擇Openspot
,在Inspector
面板中,點擊 Add Component
然后選擇New Script
並重命名為PlaceMonster
,選擇C#作為腳本語言並以此點擊Create
和Add
。因為你是向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的時候放置一個怪獸,那么如何工作的呢?
- 當玩家點擊一個游戲對象的碰撞器的時候,Unity會自動的調用
OnMouseUp
方法
2 . 當被調用之后,這個方法會檢查是否可以放置怪獸,如果可則放置一個新的怪獸 - 使用
Instantiate
方法創建一個怪獸對象,這個方法會根據指定的prefab和指定的位置和旋轉角度創建一個對象。在本例中,我們拷貝了monsterPrefab
,並指定了當前游戲對象的位置,以及沒有旋轉,最后將結果轉換為GameObject
類型存儲在monster
變量中。 - 最后,調用
PlayOneShot
方法播放附加在AudioSource
組件上的聲音特效。
現在我們的PlaceMonster
腳本已經可以放置新的怪獸了,但還需要指定prefab這個步驟。
使用正確的Prefab
在代碼編輯器中保存,並返回Unity 。
下面給monsterPrefab
賦值,首先在Prefabs文件夾中選中OpenSpot
,在Inspector
面板中,點擊PlaceMonster (Script)
組件的Monster Prefab
屬性右邊的小圓圈按鈕,然后在彈出來的對話框中選擇Monster
。
現在開始游戲,在 X 標記上點擊來創建一些怪物~
升級我們的怪物
在下面的圖片中,我們看到怪物在不同的等級有不同的外觀
我們需要一個腳本來作為怪物升級系統實現的基礎,來跟蹤管理怪物在各個級別的能力大小,當然還有怪物所處的當前等級。
現在來添加這個腳本 。
在 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()
方法,沒有必要重新發明輪子了這次 :]
在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
接下來,設置每個等級的花費如下:
- Element 0: 200
- Element 1: 110
- Element 2: 120
接下來,給visualization
賦值。
在project browers中展開Prefabs/Monster
來查看其子節點。拖拽子節點Monster0
到visualization
屬性的Element 0
。
重復上面的動作Monster1
對Element 1
,Monster2
對Element 2
,參考下面動圖中的演示:
現在,當你選中了Prefabs/Monster
,它應該看下像下面這樣子:
定義當前的等級
切換回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#代碼,我們慢慢來看:
- 為私有變量
currentLevel
定義一個屬性。當屬性被定義之后,你就能像其他變量一樣去調用,既可以CurrentLevel
這樣在類的內部調用,也可以使用monster.CurrentLevel
這樣在類的外部調用。然后還能自定義屬性的getter和setter方法,通過只提供一個getter,只有一個setter或者都有,來控制一個屬性是只讀的、只寫的或者讀寫的。 - 在getter方法中,直接返回私有變量
currentLevel
的值 - 在setter方法中,給
currentLevel
賦值。先拿到當前等級的下標,然后遍歷levels
數組依據currentLevelIndex
的值設置外觀的活動或者非活動狀態,好處就在於不管什么時候誰設置了currentLevel
,精靈會自動更新。
添加下面的OnEnable
的一個實現:
void OnEnable()
{
CurrentLevel = levels[0];
}
這里設置了CurrentLevel
的默認值,確保它只顯示正確的那個精靈。
注意
在OnEnable
中而不是OnStart
中初始化屬性的值,是因為當prefabs被實例化時方法的調用次序問題。
OnEnable
會在創建prefab時立即被調用,而OnStart
會等到對象作為場景的一部反的時候才會被調用。
這里我們需要在放置一個怪獸之前就要初始化一下,所以在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 (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,運行游戲,試試放置和升級怪物~
支付金幣 - 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()
中增加下面的初始化語句,默認給玩家100金幣(當然你可以給更少的)
Gold = 100;
給腳本中的Label對象賦值
保持腳本文件並返回到Unity中。
在Hierarcy中,選中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
組件並存到一個私有變量中,供稍后使用。
收錢!
我們還沒有減少金幣數,所以在OnMouseUp
方法里面,將原來的TODO注釋修改為下面的代碼:
//todo: Deduct gold
gameManager.Gold -= monster.GetComponent<MonsterData>().CurrentLevel.cost;
注意是兩個地方,在放置和升級的邏輯里各有一處。
保存文件並返回unity,升級一些怪物並注意觀察金幣數目的變化。現在我們能夠減少金幣數了,但是。。。玩家可以一直放置怪物(只要有空位),金幣數甚至可以變成負數。
這顯然是不能被允許的,只有當玩家有足夠的金幣時,才能放置或者升級我們的怪物。
切換到PlaceMonster.cs腳本,更新canPlaceMonster
和canUpdateMonster
方法如下,就是加上檢查剩余金幣是否足夠的條件。
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;
}
保存,並運行游戲,試試還能不能無限添加怪物。
敵人、波數和路標
是時候給敵人“鋪路”了。敵人首先在第一個路標的地方出現,然后向下一個路標移動並重復這個動作,知道他們抵達你的餅干。
我們將通過下面的手段使敵人行軍起來:
- 定義敵人移動的路線
- 使敵人沿着路線移動
- 旋轉敵人,使他們看起來是向前方行進
通過路標建立路線
在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新建一個名為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
}
}
讓我們一步一步來看:
- 從路標數組中,取出當前路段的開始路標和結束路標。
- 計算出通過整個路段所需要的時間(使用 距離除以速度 的公式),使用
Vector3.Lerp
插值計算出當前時刻應該在的位置。 - 檢查敵人是否已經抵達結束路標,如果是,則有兩種可能的場景:
A. 敵人尚未抵達最終的路標,所以增加currentWayPoint
並更新lastWaypointSwitchTime
,稍后我們要增加旋轉敵人的代碼使他們朝向前進的方向。
B. 敵人抵達了最終的路標,就銷毀敵人對象,並觸發聲音特效,稍后我們要增加減少玩家生命值的代碼。
保存文件,並返回到Unity。
給敵人指明方向
現在,敵人還不知道路標的次序。
在Hierarchy中選中Road,然后添加一個新的C#腳本命名為SpawnEnemy,並在代碼編輯器中打開,增加下面的變量:
public GameObject[] waypoints;
我們將使用waypoints
來存放路標的引用。
保存文件返回Unity, 在Hierarchy中選中Road,將WayPoints
數組的大小改為6,拖拽Road的孩子節點到想用的Element位置,Waypoint0對應Element0以此類推。
現在我們已經有了路線的路標數組,注意到敵人不會退縮。。。
檢查一切順利
打開SpawnEnemy腳本,增加下面的變量:
public GameObject testEnemyPrefab;
這里使用了testEnemyPrefab
保存對Enemy
prefab的引用。
使用下面的代碼,當腳本開始時添加一個敵人:
void Start () {
Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;
}
上面的代碼使用testEnemyPrefab實例化一個敵人對象,並將路標賦值給它。
保存文件返回Unity,在Hierarchy中選中Road並將Enemy prefab賦值給testEnemyPrefab。
運行游戲,可以看到敵人已經能夠沿着路線移動了:
nice,但是有沒有注意到敵人移動的時候沒有朝向移動的方向。。沒有關系,這個問題將在part2部分修復。
原文地址:http://www.raywenderlich.com/107525/create-tower-defense-game-unity-part-1