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