? 說(shuō)起炸彈超人,相信很多朋友都玩過(guò)類(lèi)似的游戲,其中最為人熟知的莫過(guò)于《泡泡堂》。該類(lèi)型游戲需要玩家在地圖中一邊跑動(dòng)一邊放置炸彈,同時(shí)還要躲避敵方炸彈保護(hù)自己。最初的炸彈超人游戲都是2D的,今天這篇文章將教大家在Unity中實(shí)現(xiàn)一款3D的炸彈超人游戲。

溫馨提示,本教程需要大家了解Unity的基本操作與腳本概念。




準(zhǔn)備工作
將項(xiàng)目初始資源導(dǎo)入U(xiǎn)nity項(xiàng)目,資源目錄如下:
?

?



其中分別包含要用于游戲的動(dòng)畫(huà)、材質(zhì)、模型、背景音樂(lè)、物理材質(zhì)、預(yù)制件、場(chǎng)景、腳本、音效及圖片資源。

放置炸彈
打開(kāi)項(xiàng)目中的Game場(chǎng)景并運(yùn)行。
?

?


可以通過(guò)WASD鍵或方向鍵來(lái)操作所有角色進(jìn)行移動(dòng)。下面來(lái)讓角色可以放置炸彈。角色1(紅色)通過(guò)按下空格鍵來(lái)放置炸彈,角色2(藍(lán)色)則通過(guò)按下回車(chē)鍵進(jìn)行同樣的操作。

打開(kāi)Player腳本,該腳本負(fù)責(zé)角色所有的移動(dòng)及動(dòng)畫(huà)邏輯。找到DropBomb函數(shù),添加代碼如下:

/// <summary>
/// Drops a bomb beneath the player
/// </summary>
private void DropBomb() {
    if (bombPrefab) { //Check if bomb prefab is assigned first
        // Create new bomb and snap it to a tile
        Instantiate(bombPrefab,
            new Vector3(Mathf.RoundToInt(myTransform.position.x), bombPrefab.transform.position.y, Mathf.RoundToInt(myTransform.position.z)),
            bombPrefab.transform.rotation);
    }
}

?其中RoundToInt函數(shù)用于對(duì)炸彈的坐標(biāo)參數(shù)四舍五入,以避免炸彈放置位置偏離出地塊中心。
?

?



運(yùn)行場(chǎng)景,效果如下:

?



創(chuàng)建爆炸效果
在Scripts文件夾下新建C#腳本命名為Bomb:
?

?



找到Prefabs文件夾下的Bomb預(yù)制件,將Bomb腳本綁定到該游戲?qū)ο笊?。然后打開(kāi)Bomb腳本,添加代碼如下:

?

[C#]?純文本查看?復(fù)制代碼
using UnityEngine;
using System.Collections;
using System.Runtime.CompilerServices;
 
public class Bomb : MonoBehaviour {
    public AudioClip explosionSound;
    public GameObject explosionPrefab; 
    public LayerMask levelMask; // This LayerMask makes sure the rays cast to check for free spaces only hits the blocks in the level
    private bool exploded = false;
 
    // Use this for initialization
    void Start() {
        Invoke("Explode", 3f); //Call Explode in 3 seconds
    }
 
    void Explode() {
        //Explosion sound
        AudioSource.PlayClipAtPoint(explosionSound,transform.position);
 
        //Create a first explosion at the bomb position
        Instantiate(explosionPrefab, transform.position, Quaternion.identity);
 
        //For every direction, start a chain of explosions
        StartCoroutine(CreateExplosions(Vector3.forward));
        StartCoroutine(CreateExplosions(Vector3.right));
        StartCoroutine(CreateExplosions(Vector3.back));
        StartCoroutine(CreateExplosions(Vector3.left));
 
        GetComponent<MeshRenderer>().enabled = false; //Disable mesh
        exploded = true; 
        transform.FindChild("Collider").gameObject.SetActive(false); //Disable the collider
        Destroy(gameObject,.3f); //Destroy the actual bomb in 0.3 seconds, after all coroutines have finished
    }
 
    public void OnTriggerEnter(Collider other) {
        if (!exploded && other.CompareTag("Explosion")) { //If not exploded yet and this bomb is hit by an explosion...
            CancelInvoke("Explode"); //Cancel the already called Explode, else the bomb might explode twice 
            Explode(); //Finally, explode!
        }
    }
 
    private IEnumerator CreateExplosions(Vector3 direction) {
        for (int i = 1; i < 3; i++) { //The 3 here dictates how far the raycasts will check, in this case 3 tiles far
            RaycastHit hit; //Holds all information about what the raycast hits
 
            Physics.Raycast(transform.position + new Vector3(0,.5f,0), direction, out hit, i, levelMask); //Raycast in the specified direction at i distance, because of the layer mask it'll only hit blocks, not players or bombs
 
            if (!hit.collider) { // Free space, make a new explosion
                Instantiate(explosionPrefab, transform.position + (i * direction), explosionPrefab.transform.rotation);
            }
            else { //Hit a block, stop spawning in this direction
                break;
            }
 
            yield return new WaitForSeconds(.05f); //Wait 50 milliseconds before checking the next location
        }
 
    }
}

?在檢視面板中,將Bomb預(yù)制件賦值給腳本的Explosion Prefab屬性,該屬性用于定義需要生成爆炸效果的對(duì)象。Bomb腳本使用了協(xié)程來(lái)實(shí)現(xiàn)爆炸的效果,StartCoroutine函數(shù)將朝著4個(gè)方向調(diào)用CreateExplosions函數(shù),該函數(shù)用于生成爆炸效果,在For循環(huán)內(nèi)遍歷炸彈能夠炸到的所有單元,然后為能夠被炸彈影響的各個(gè)單元生成爆炸特效,炸彈對(duì)墻壁是沒(méi)有傷害的。最后,在進(jìn)入下一次循環(huán)前等待0.05秒。

代碼作用類(lèi)似下圖:
?

?


紅線(xiàn)就是Raycast,它會(huì)檢測(cè)炸彈周?chē)膯卧欠駷榭眨绻?,則朝著該方向生成爆炸效果。如果碰撞到墻,則不生成爆炸并停止檢測(cè)該方向。所以前面需要讓炸彈在地塊中心生成,負(fù)責(zé)就會(huì)出現(xiàn)不太理想的效果:
?
?


Bomb代碼中定義的LayerMask用于剔除射線(xiàn)對(duì)地塊的檢測(cè),這里還需要在檢視面板中編輯層,并新增用戶(hù)層命名為“Blocks”,然后將層級(jí)視圖中Blocks游戲?qū)ο蟮腖ayer設(shè)置為“Blocks”。
?
?


更改Blocks對(duì)象的層級(jí)時(shí)會(huì)跳出提示框,詢(xún)問(wèn)是否更改子節(jié)點(diǎn),選擇是即可:
?
?


然后選中Bomb對(duì)象,在檢視面板中將Bomb腳本的Level Mask設(shè)為“Blocks”:
?
?


連鎖反應(yīng)
如果炸彈炸到了另一個(gè)炸彈,那么被炸到的炸彈也會(huì)爆炸。Bomb腳本中的OnTriggerEnter
函數(shù)是MonoBehaviour預(yù)定義的函數(shù),會(huì)在觸發(fā)器與Rigidbody碰撞之前調(diào)用。這里OnTriggerEnter會(huì)檢測(cè)被碰撞的炸彈是否是被炸彈特效所碰撞,如果是,則該炸彈也要爆炸。

現(xiàn)在運(yùn)行場(chǎng)景,效果如下:
?
?


判定游戲結(jié)果
打開(kāi)Player腳本,添加下面的代碼:
//Manager
  public GlobalStateManager GlobalManager;
 
  //Player parameters
  [Range(1, 2)] //Enables a nifty slider in the editor
  public int playerNumber = 1; //Indicates what player this is: P1 or P2
  public float moveSpeed = 5f;
  public bool canDropBombs = true; //Can the player drop bombs?
  public bool canMove = true; //Can the player move?
  public bool dead = false; //Is this player dead?

?其中GlobalManager是GlobalStateManager腳本的引用,該腳本用于通知玩家獲勝或死亡的消息。dead則用于標(biāo)志玩家是否死亡。

更改OnTriggerEnter函數(shù)代碼如下:

?

[C#]?純文本查看?復(fù)制代碼
public void OnTriggerEnter(Collider other) {
    if (!dead && other.CompareTag("Explosion")) { //Not dead & hit by explosion
        Debug.Log("P" + playerNumber + " hit by explosion!");
 
        dead = true;
        GlobalManager.PlayerDied(playerNumber); //Notify global state manager that this player died
        Destroy(gameObject);
    }
}

?該函數(shù)作用為設(shè)置dead變量來(lái)通知玩家死亡,并告知全局狀態(tài)管理器玩家的死亡信息,然后銷(xiāo)毀玩家對(duì)象。

在檢視面板中選中兩個(gè)玩家對(duì)象,將Global State Manager游戲?qū)ο筚x值給Player腳本的Global Manger字段。
?

?


再次運(yùn)行場(chǎng)景,效果如下:
?
?


打開(kāi)GlobalStateManager腳本,添加以下代碼:

?
[C#]?純文本查看?復(fù)制代碼
public List<GameObject> Players = new List<GameObject>();
 
 private int deadPlayers = 0;
 private int deadPlayerNumber = -1;
 
 public void PlayerDied(int playerNumber) {
     deadPlayers++;
 
     if (deadPlayers == 1) {
         deadPlayerNumber = playerNumber;
         Invoke("CheckPlayersDeath", .3f);
     }
 }

?其中deadPlayers表示死亡的玩家數(shù)量,deadPlayerNumber則用于記錄死亡玩家的編號(hào)。PlayerDied函數(shù)用于添加死亡玩家,并設(shè)置deadPlayerNumber屬性,在0.3秒后檢測(cè)另一位玩家是否也死亡。

然后在腳本中添加CheckPlayersDeath函數(shù),代碼如下:

?

[C#]?純文本查看?復(fù)制代碼
void CheckPlayersDeath() {
     if (deadPlayers == 1) { //Single dead player, he's the winner
 
         if (deadPlayerNumber == 1) { //P1 dead, P2 is the winner
             Debug.Log("Player 2 is the winner!");
         }
         else { //P2 dead, P1 is the winner
             Debug.Log("Player 1 is the winner!");
         }
     }
     else {  //Multiple dead players, it's a draw
         Debug.Log("The game ended in a draw!");
     }
 }

?以上代碼用于判斷哪位玩家獲得勝利,如果兩位玩家均死亡,則打成平局。

運(yùn)行場(chǎng)景,效果如下:
?

?


總結(jié)
到此本篇教程就結(jié)束了,大家還可以在此基礎(chǔ)上對(duì)該項(xiàng)目進(jìn)行擴(kuò)展,例如添加“推箱子”功能,將位于自己腳邊的炸彈推給敵方,或是限制能夠放置的炸彈數(shù)量,添加快速重新開(kāi)始游戲的界面,設(shè)置可以被炸彈炸毀的障礙物,設(shè)置一些道具用于獲得炸彈或者增加生命值,還可以增加多人對(duì)戰(zhàn)模式與朋友一起變身炸彈超人等等。大家都來(lái)發(fā)揮自己的創(chuàng)意吧!
視頻教程請(qǐng)點(diǎn)擊?http://www.charliecredit.com/course/324/

銳亞教育