如何使用Unity3D+C#開發(fā)炸彈人游戲
譯文簡介
炸彈人游戲是上世紀(jì)80年代廣泛流行的一個(gè)2D游戲,本文創(chuàng)建的是一個(gè)基本型的此游戲的Unity3D版本。
通過本游戲,你可以實(shí)現(xiàn)如下功能:
- 投擲炸彈并把它放到特定位置
- 通過光線跟蹤技術(shù)激活炸彈
- 處理與玩家的爆炸碰撞
- 處理與炸彈的爆炸碰撞
- 游戲結(jié)局處理
準(zhǔn)備工作
首先,請下載一個(gè)我為本文游戲建立的初始示例項(xiàng)目,然后把它放到一個(gè)你指定的位置。
然后,使用Unity3D打開這個(gè)項(xiàng)目,注意到Assets文件夾下包含了好多的子文件夾,如圖所示。
這里具體說一下各個(gè)文件夾的主要功能:
- Animation Controllers:存儲著游戲控制器部分,包括的邏輯部分。
- Materials:包含構(gòu)建各關(guān)卡場景所需要的塊(Block)材質(zhì)。
- Models:存儲玩家、關(guān)卡及炸彈模型,及其相關(guān)材質(zhì)。
- Music:存儲游戲的音效文件。
- Physics Materials:存儲玩家的物理材質(zhì)數(shù)據(jù),它們是一些特殊類型的材質(zhì),用于實(shí)現(xiàn)特定的物理屬性。在本教程中,用于使玩家在無摩擦情況下輕松地在關(guān)卡中穿越。
- Prefabs:包含炸彈及爆炸的預(yù)制數(shù)據(jù)。
- Scenes:對應(yīng)于游戲場景數(shù)據(jù)。
- Scripts:包含游戲的啟動(dòng)腳本,其中添加的大量注釋將有利于讀者閱讀源碼。
- Sound Effects:包含炸彈及爆炸效果相關(guān)的聲效文件。
- Textures:包含兩個(gè)玩家的紋理數(shù)據(jù)。
投擲炸彈
如果你還沒有打開游戲工程,請抓緊打開,然后試著運(yùn)行一下此程序。沒有其他問題的話,你會(huì)觀察到如圖所示的情形:
你會(huì)注意到,游戲中的兩個(gè)玩家可以通過鍵盤上的WASD四個(gè)字符鍵或者四個(gè)箭頭鍵驅(qū)動(dòng),使其沿著游戲地圖運(yùn)動(dòng)。
通常,當(dāng)按下空格鍵時(shí)紅色玩家會(huì)在其腳下安置一枚炸彈,而另一個(gè)玩家也能夠做同樣的事情——只是通過按回車鍵實(shí)現(xiàn)。
然而,目前我們還沒有實(shí)現(xiàn)這一功能。為此,你需要先編寫放置炸彈的代碼?,F(xiàn)在,請你使用自己喜歡的代碼編輯器打開腳本文件Player.cs。
此腳本負(fù)責(zé)處理所有的玩家運(yùn)動(dòng)及動(dòng)畫邏輯,還包含一個(gè)方法DropBomb,當(dāng)關(guān)聯(lián)游戲?qū)ο?GameObject)bombPrefab時(shí),它用于檢測目的。
- private void DropBomb() {
- if (bombPrefab) { //Check if bomb prefab is assigned first
- }
- }
為了實(shí)現(xiàn)一個(gè)炸彈掉落在玩家下面的效果,在if語句中添加下面的代碼:
- Instantiate(bombPrefab, myTransform.position, bombPrefab.transform.rotation);
上述代碼將在玩家腳下生成炸彈(隨著玩家的運(yùn)動(dòng)路徑的變化,將生成成串的炸彈)?,F(xiàn)在,運(yùn)行一下游戲工程,你會(huì)觀察到如下圖所示效果:
目前,效果不錯(cuò)吧!
但是,還有一個(gè)小問題:炸彈投擲的方式如何?如果是無論在哪里你都能放炸彈的話,當(dāng)你需要計(jì)算爆炸應(yīng)該發(fā)生的位置時(shí)就會(huì)帶來一些問題。
接下來,本教程將向你具體介紹如何實(shí)現(xiàn)爆炸的所有細(xì)節(jié)。
炸彈定位
下一步任務(wù)是確保炸彈在丟掉時(shí)能夠附著到相應(yīng)位置,從而實(shí)現(xiàn)炸彈很好地與地板上的網(wǎng)格對齊。由于我們的設(shè)計(jì)中網(wǎng)格上的每個(gè)圖塊大小是 1 × 1,所以進(jìn)行此更改是相當(dāng)容易的。
打開文件Player.cs,編輯一下Instantiate()函數(shù),像下面這樣:
- Instantiate(bombPrefab, new Vector3(Mathf.RoundToInt(myTransform.position.x),
- bombPrefab.transform.position.y, Mathf.RoundToInt(myTransform.position.z)),
- bombPrefab.transform.rotation);
注意,這里函數(shù)Mathf.RoundToInt調(diào)用中使用了玩家位置的x和z兩個(gè)參數(shù)值,每一個(gè)浮點(diǎn)類型值被轉(zhuǎn)換為一個(gè)整型值,這就可以實(shí)現(xiàn)炸彈很好地與地板上的網(wǎng)格對齊的效果:
現(xiàn)在,你可以再次啟動(dòng)工程來運(yùn)行一下,你會(huì)觀察到當(dāng)投擲炸彈時(shí),這些炸彈恰好能夠?qū)R網(wǎng)格:
雖然把炸彈投擲到地圖上是很有趣的,但你知道真正有趣的事是如何實(shí)現(xiàn)爆炸!為此,我們再來添加一些功能。
創(chuàng)建爆炸效果
首先,我們要?jiǎng)?chuàng)建一個(gè)新的腳本文件:
(1)從Project視圖下選擇Scripts文件夾;
(2)按下Create按鈕;
(3)選擇“C# Script”;
(4)把腳本文件命名為Bomb即可。
現(xiàn)在,把Bomb.cs腳本關(guān)聯(lián)到預(yù)制Bomb上:
(1)在Prefabs文件夾中選擇GameObject Bomb;
(2)點(diǎn)擊按鈕“Add Component”;
(3)在搜索框中輸入“bomb”;
(4)選擇你剛剛創(chuàng)建的腳本Bomb.cs;
(5)打開此腳本文件,然后在其Start()方法中輸入如下代碼:
- Invoke("Explode", 3f);
此方法使用了兩個(gè)參數(shù),第一個(gè)是將要調(diào)用的方法名稱,第二個(gè)是在調(diào)用此方法時(shí)需要延遲的時(shí)間數(shù)。在本例中,想實(shí)現(xiàn)炸彈在3秒內(nèi)爆炸的效果。我們將在后面添加這個(gè)Explode方法的具體內(nèi)容。
現(xiàn)在,只是在Update()方法下面添加這個(gè)方法占位符形式(目前為空):
- void Explode() {
- }
在生成任何GameObject Explosion之前,還需要?jiǎng)?chuàng)建一個(gè)公共類型的GameObjet對象,以便進(jìn)行預(yù)制Explosion的賦值。恰好在Start()方法上面定義如下代碼:
- public GameObjectexplosionPrefab;
保存此文件,然后從Prefabs文件夾下選擇預(yù)制Bomb,然后把預(yù)制Explosion拖動(dòng)到“Explosion Prefab”選項(xiàng)后面空白處。
完成這一操作后,返回到編輯器中?,F(xiàn)在開始編寫更有意思的代碼。
在方法Explode()中,添加如下代碼行:
- Instantiate(explosionPrefab, transform.position, Quaternion.identity); //1
- GetComponent<MeshRenderer>().enabled = false; //2
- transform.FindChild("Collider").gameObject.SetActive(false); //3
- Destroy(gameObject, .3f); //4
上述代碼實(shí)現(xiàn)如下功能:
1.在炸彈位置觸發(fā)爆炸;
2.禁用網(wǎng)絡(luò)渲染器(mesh render),使炸彈不可見;
3.禁用碰撞器,從而允許玩家在爆炸中移動(dòng)與行走;
4.在0.3秒后拆除炸彈;這可以確保在刪除GameObject之前所有爆炸都會(huì)觸發(fā)。
現(xiàn)在,保存腳本Bomb.cs,返回到編輯器嘗試再玩一下游戲。放下一些炸彈并觀察一下它們爆炸時(shí)良好的效果,參考下圖。
設(shè)置爆炸音效
為了創(chuàng)建理想的爆炸效果,你需要?jiǎng)?chuàng)建一個(gè)協(xié)程。
「補(bǔ)充」協(xié)程本質(zhì)上是一個(gè)函數(shù),允許你暫停執(zhí)行并將控制返回到Unity3D。在以后的某個(gè)時(shí)間點(diǎn)處該函數(shù)將從上次離開的位置恢復(fù)執(zhí)行。
人們經(jīng)常混淆協(xié)程與多線程。其實(shí),它們是不同的:協(xié)程運(yùn)行在同一個(gè)線程中,并能夠在某中間點(diǎn)處及時(shí)恢復(fù)執(zhí)行。若要了解更多的關(guān)于協(xié)程及其定義相關(guān)信息,請查閱相關(guān)的Unity文檔(http://docs.unity3d.com/Manual/Coroutines.html)。
現(xiàn)在,返回到代碼編輯器中修改腳本Bomb.cs,在函數(shù)Explode()下面添加一個(gè)名字為CreateExplosions的IEnumberator:
- private IEnumeratorCreateExplosions(Vector3 direction) {
- return null // placeholder for now
- }
創(chuàng)建協(xié)程
現(xiàn)在,請把下面四行代碼添加到函數(shù)Explode()內(nèi)部的Instantiate調(diào)用與MeshRender禁用之間:
- StartCoroutine(CreateExplosions(Vector3.forward));
- StartCoroutine(CreateExplosions(Vector3.right));
- StartCoroutine(CreateExplosions(Vector3.back));
- StartCoroutine(CreateExplosions(Vector3.left));
這里的StartCoroutine調(diào)用將針對游戲場景中的每個(gè)方向觸發(fā)CreateExplosions。
現(xiàn)在,更有趣的時(shí)刻到了。在方法CreateExplosions()內(nèi)部加入如下代碼:
- //1
- for (inti = 1; i< 3; i++) {
- //2
- RaycastHit hit;
- //3
- Physics.Raycast(transform.position + new Vector3(0,.5f,0), direction, out hit, i, levelMask);
- //4
- if (!hit.collider) {
- Instantiate(explosionPrefab, transform.position + (i * direction),
- //5
- explosionPrefab.transform.rotation);
- //6
- } else {
- //7
- break;
- }
- //8
- yield return new WaitForSeconds(.05f);
- }
這段代碼看起來相當(dāng)復(fù)雜,但實(shí)際上相當(dāng)簡單。詳細(xì)解釋如下:
1.通過for循環(huán)來遍歷你想要爆炸覆蓋的每個(gè)單位距離。在本例情況下,爆炸將達(dá)到兩米的距離。
2.RaycastHit對象包含有關(guān)Raycast擊中的是什么對象及擊中位置的所有信息,當(dāng)然也可能沒有擊中。
3.上述代碼中非常重要的代碼行是StartCoroutine調(diào)用,這個(gè)調(diào)用中實(shí)現(xiàn)從炸彈中心朝你通過的方向發(fā)出raycast。然后,它將結(jié)果輸出到RaycastHit對象。I 參數(shù)指示射線走過的距離。最后,代碼中使用命名為levelMask的層蒙版(LayerMask)來確保射線只檢查當(dāng)前關(guān)卡中的塊而忽略檢查玩家及其他的碰撞對象。
4.如果raycast沒有撞到任何東西,那么說明這個(gè)塊(Block)是一個(gè)自由塊。
5.在raycast檢查的位置產(chǎn)生爆炸。
6.Raycast擊中塊。
7.一旦raycast擊中一個(gè)塊,它就跳出for循環(huán)。這將確保爆炸不會(huì)跨越墻。
8.在進(jìn)行下一個(gè)for循環(huán)迭代前等待0.05秒。這將使爆炸呈現(xiàn)向外擴(kuò)展的效果而更具有說服力。
下圖給出的是上面添加代碼后的動(dòng)畫效果:
注意,下圖中的紅線是raycast。它圍繞炸彈檢查一段自由空間距離;如果發(fā)現(xiàn)存在碰撞,那么將產(chǎn)生爆炸。當(dāng)它擊中塊時(shí),它并不產(chǎn)生任何東西并停止在那個(gè)方向上的檢查。
現(xiàn)在,你該明白了為什么炸彈需要附著到網(wǎng)格中各小格子中心了吧。如果炸彈能去任何地方,那么在一些邊緣情況下,raycast會(huì)擊中塊卻并不產(chǎn)生任何爆炸,因?yàn)樗鼪]有正確地進(jìn)行水平對齊。
添加遮罩層
在Bomb代碼中還存在一個(gè)錯(cuò)誤:事實(shí)上LayerMask并不存在。因此,現(xiàn)在就在explosionPrefab變量聲明的下面,再添加一行代碼,如下:
- public GameObjectexplosionPrefab;
- public LayerMasklevelMask;
LayerMask通常使用raycasts技術(shù)有選擇地篩選出特定圖層。在本例情況下,你只需要篩選出塊部分,所以,raycasts技術(shù)并沒有做什么事。
保存Bomb腳本并返回到Unity編輯器。單擊右上角的Layers按鈕并選擇Edit Layers...
現(xiàn)在,請點(diǎn)擊User Layer 8旁邊的文本框并輸入“Blocks”。這樣就定義了你可以使用的新層。
在層次視圖中,選擇GameObject Blocks,如下圖所示:
把圖層改變?yōu)槟銊倓倓?chuàng)建的圖層Blocks,如下圖所示:
當(dāng)出現(xiàn)“Change Layer”對話框時(shí)點(diǎn)擊“Yes,change children”按鈕,以便應(yīng)用于地圖上所有散布的黃色長方體塊。
最后,從Prefabs文件夾下選擇預(yù)制Bomb,并把遮罩層改成Blocks。
現(xiàn)在,再次運(yùn)行一下游戲場景并投擲幾枚炸彈。你會(huì)觀察到爆炸效果比以前好多了:良好地分布于各長方體塊之間!
恭喜你,你已經(jīng)攻克了本游戲中最困難的編碼部分。剩下的是為游戲添加一些附加效果。
鏈?zhǔn)椒磻?yīng)
當(dāng)一枚炸彈爆炸時(shí)會(huì)接觸到另一個(gè)炸彈,這枚相鄰的炸彈也應(yīng)該爆炸,這將產(chǎn)生一種更令人驚喜的效果。
值得欣喜的是,上述效果并不難實(shí)現(xiàn)。
現(xiàn)在打開腳本文件Bomb.cs,然后在方法CreateExplosions()下面添加一個(gè)新的方法OnTriggerEnter:
- public void OnTriggerEnter(Collider other) {
- }
OnTriggerEnter方法是MonoBehaviour中一個(gè)預(yù)定義的方法,在觸發(fā)器碰撞器與剛體碰撞時(shí)激活執(zhí)行。碰撞器參數(shù)是other,對應(yīng)于進(jìn)入觸發(fā)器的游戲物體(GameObject)的碰撞器。
在本例情況下,你需要檢查碰撞對象,并確定當(dāng)該對象是一個(gè)爆炸對象時(shí)使之爆炸。
首先,你需要知道是否發(fā)生炸彈爆炸。需要首先聲明exploded變量,因此在levelMask變量聲明下面添加以下聲明:
- private bool exploded = false;
然后,在方法OnTriggerEnter()內(nèi)部添加如下代碼:
- if (!exploded&&other.CompareTag("Explosion")) { // 1 & 2
- CancelInvoke("Explode"); // 2
- Explode(); // 3
- }
這段代碼做了三件事情:
1.檢查炸彈是否已經(jīng)爆炸了;
2.檢查觸發(fā)器碰撞器是否已經(jīng)有標(biāo)簽Explosion;
3.通過投擲炸彈取消已經(jīng)調(diào)用的Explode調(diào)用——如果不這樣做,炸彈可能會(huì)爆炸兩次;
4.實(shí)現(xiàn)爆炸。
現(xiàn)在你已經(jīng)定義了變量,但是還沒有作任何修改。而最合乎邏輯的地方是在Explode()函數(shù)中實(shí)現(xiàn)這一操作(應(yīng)當(dāng)在禁用組件MeshRenderer之后),代碼如下:
- ...
- GetComponent<MeshRenderer>().enabled = false;
- exploded = true;
- ...
現(xiàn)在準(zhǔn)備好了一切,請保存一下剛才的文件修改,然后再次運(yùn)行一下工程。再次投擲一枚炸彈,并連續(xù)在其周圍投擲炸彈,觀察效果:
最后剩下的事情是處理玩家對于爆炸的反應(yīng)情況,以及游戲結(jié)局處理邏輯。
游戲結(jié)局處理
打開文件Player.cs。目前還沒有定義變量來表示玩家的死活;因此,在腳本頂部添加一個(gè)布爾變量,如下所示:
- public cool dead=false;
這個(gè)變量用于跟蹤是否玩家在爆炸以后死亡。
接下來,在其他變量聲明后面添加如下變量聲明:
- public GlobalStateManagerGlobalManager;
注意,到現(xiàn)在在方法OnTriggerEnter()內(nèi)部已經(jīng)能夠檢查是否玩家被炸彈擊中,但目前實(shí)現(xiàn)的僅僅是通過控制臺窗口輸出這一消息。因此,現(xiàn)在請將如下代碼添加到Debug.Log調(diào)用后面:
- dead = true; // 1
- GlobalManager.PlayerDied(playerNumber); // 2
- Destroy(gameObject); // 3
這段代碼實(shí)現(xiàn)如下功能:
1.修改變量dead,以便跟蹤玩家死亡的消息;
2.通知全局狀態(tài)管理器玩家已經(jīng)死亡;
3. 銷毀玩家對象GameObject。
現(xiàn)在,保存一下文件并返回到Unity編輯器中。你需要把GlobalStateManager連接到兩個(gè)玩家:
(1)在層次窗口內(nèi),選擇兩個(gè)Player GameObject。
(2)把全局狀態(tài)管理器GameObject拖動(dòng)到它們的Global Manager選項(xiàng)處。
再次運(yùn)行游戲場景,確保至少有一個(gè)玩家被炸彈擊中,參考下圖。
每一個(gè)遭遇到爆炸的玩家都會(huì)立即死亡。
但是,目前為止游戲并不知道誰贏了,因?yàn)镚lobalStateManager還沒有使用它收到的信息。下面來討論這件事情。
定義贏家
打開文件GlobalStateManager.cs。為了使GlobalStateManager能夠跟蹤玩家的死亡,還需要定義兩個(gè)變量。在函數(shù)PlayerDied()上面加上下面的定義:
- private intdeadPlayers = 0;
- private intdeadPlayerNumber = -1;
首先,變量deadPlayers會(huì)存儲死亡的玩家數(shù)量。一旦第一個(gè)玩家死亡,變量deadPlayerNumber即被修改,此變量也表示了是哪一位玩家這種額外信息。
準(zhǔn)備好了上面變量后,現(xiàn)在加入實(shí)際邏輯。在函數(shù)PlayerDied()中加入如下代碼:
- deadPlayers++; // 1
- if (deadPlayers == 1) { // 2
- deadPlayerNumber = playerNumber; // 3
- Invoke("CheckPlayersDeath", .3f); // 4
- }
這段代碼的功能是:
1.添加一個(gè)死亡玩家;
2.進(jìn)一步判斷是否這是第一個(gè)死亡玩家…
3.把死亡玩家數(shù)設(shè)置為首先死亡的玩家;
4.檢查是否另一個(gè)玩家也死亡了,還是在0.3秒后僅起了一些爆炸塵埃而沒有死亡。
最后的一點(diǎn)時(shí)間延遲對于繪制檢查來說很重要。如果立即進(jìn)行檢查,你可能發(fā)現(xiàn)不了有人死亡,而0.3秒對于判斷是否每一個(gè)人都死亡了已經(jīng)足夠了。
輸贏判定
現(xiàn)在,請?jiān)贕lobalStateManager腳本中添加一個(gè)新方法CheckPlayersDeath:
- void CheckPlayersDeath() {
- // 1
- if (deadPlayers == 1) {
- // 2
- if (deadPlayerNumber == 1) {
- Debug.Log("Player 2 is the winner!");
- // 3
- } else {
- Debug.Log("Player 1 is the winner!");
- }
- // 4
- } else {
- Debug.Log("The game ended in a draw!");
- }
- }
上述條件語句的功能列舉如下:
1.只有一個(gè)玩家死亡,則判定他是輸家;
2.玩家1死亡了,那么玩家2是贏家;
3.玩家2死亡了,那么玩家1是贏家;
4.兩個(gè)玩家都死亡了,那么這是一場平局。
現(xiàn)在,再保存并運(yùn)行一下你的工程試試吧,參考下圖:
剩下的話
請下載工程代碼并進(jìn)行詳細(xì)研究吧!
你通過本文了解了如何使用Unity3D創(chuàng)建像炸彈人這樣的基本類型的游戲。
本文使用了一些粒子系統(tǒng)用于炸彈與爆炸效果。更多的有關(guān)信息,請參考Unity3D官方文檔。
最后,強(qiáng)烈建議你做如下增強(qiáng)性修改:
(1)可以使炸彈能夠被推動(dòng),這樣當(dāng)炸彈靠近你時(shí)你可以逃跑,而把炸彈推到你的對手身上;
(2)限制可以投擲的炸彈數(shù)量;
(3)加入重新啟動(dòng)游戲功能;
(4)伴隨爆炸加入可破裂的場景中的塊(Blocks);
(5)你可以增加一些有趣的裝備;
(6)多加幾條命,以及使用某種方式來進(jìn)行購買;
(7)創(chuàng)建漂亮的UI元素來顯示玩家贏了什么東西;
(8)探討某種方法來添加更多的玩家,等等。