Unity3D開發一個2D橫版射擊游戲


教程基於http://pixelnest.io/tutorials/2d-game-unity/ , 這個例子感覺還是比較經典的, 網上轉載的也比較多. 剛好最近也在學習U3D, 做的過程中自己又修改了一些地方, 寫篇文和大家一起分享下, 同時也加深記憶. 有什么紕漏的地方還請大家多包涵.

1.創建第一個場景

新建工程,在Project面板創建文件夾, 是為了更好的規划管理資源文件.

接着在Hierarchy面板上創建多個空對象(這樣的結構也是清晰了整個游戲的層次, 對象之間的關系一目了然), 這些空對象的Position保持(0,0,0)即可. 保存場景到Scenes文件夾中, 名稱為Stage1.

2.添加顯示背景

將背景圖片放入Textures文件夾, 確認這張圖片的紋理類型Texture Type為Sprite.

 

在場景中添加一個Sprite游戲對象,命名為Background1,設置Sprite屬性為剛才導入的背景圖片, 將它移動到0 - Background中, 設置Position為(0,0,0).

接着添加背景元素. 導入平台島嶼圖片到Textures文件夾, 選中Platforms圖片, 設置它的Sprite Mode為Multiple, 然后點擊Sprite Editor, 如下圖:

在彈出的Sprite Editor窗口中, 進行繪制每個平台島嶼的包圍矩形, 以便將紋理分隔成更小的部分. 然后分別命名為platform1和platform2.

創建一個新的Sprite對象, 設置它的Sprite為platform1. 然后同樣再創建一個Sprite對象, 設置Sprite為platform2. 將它們放置到1 - Middleground對象里, 並且確認他們的Z坐標為0. 設置完成后, 將這兩個對象從Hierarchy面板拖動到Project面板下的Prefabs文件夾, 保存為預制對象. 接着, 為了避免顯示順序問題, 修改下游戲對象的Z坐標, 如下所示:

Layer Z Position
0 - Background 10
1 - Middleground 5
2 - Foreground 0
 

此時, 點擊Scene面板上的2D到3D視圖切換, 可以清除的看到層次:

3.創建玩家和敵人

導入主角圖片到Textures文件夾, 創建一個Sprite對象, 命名為Player, 設置其Sprite屬性為剛才導入的主角圖片. 將它放入2 - Foreground中, 設置Scale為(0.2,0.2,1). 接着, 為主角添加碰撞機, 點擊Add Component按鈕, 選擇Box Collider 2D, 設置Size為(10,10), 雖然大於實際區域, 但是已經比圖片小多了.

但是我更願意去使用Polygon Collider 2D來達到更精致的效果, 這里只是個例子, 大家可以自由選擇.

接着, 再為主角對象添加Rigidbody 2D剛體組件, 現在運行可以看到如下結果:

可以看到主角往下落了, 這是因為剛體帶有重力, 但在這個游戲中我們用不到重力, 將Gravity Scale設置為0即可. 另外, 不想因為物理而引起的主角旋轉, 則將Fixed Angles勾選上.

開始准備讓主角移動. 在Scripts文件夾中, 創建一個C#腳本, 名稱為PlayerScript, 實現讓方向鍵移動主角, 代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 玩家控制器和行為
/// </summary>
public class PlayerScript : MonoBehaviour
{
    #region 1 - 變量

    /// <summary>
    /// 飛船移動速度
    /// </summary>
    private Vector2 speed = new Vector2(50, 50);

    // 存儲運動
    private Vector2 movement;

    #endregion

    // Update is called once per frame
    void Update()
    {
        #region 運動控制

        // 2 - 獲取軸信息
        float inputX = Input.GetAxis("Horizontal");
        float inputY = Input.GetAxis("Vertical");

        // 3 - 保存運動軌跡
        movement = new Vector2(speed.x * inputX, speed.y * inputY);

        #endregion
    }

    void FixedUpdate()
    {
        // 4 - 讓游戲物體移動
        rigidbody2D.velocity = movement;
    }
}

這里以改變剛體的速率來達到主角移動的效果, 而不是通過直接改變transform.Translate, 因為那樣的話, 可能會不產生碰撞. 另外, 這里有人可能會疑問為什么實現移動的代碼要寫在FixedUpdate而不是Update中, 請看Update和FixedUpdate的區別: 傳送門.

現在將此腳本附加到主角對象上, 點擊運行, 方向鍵來控制移動. 

接下來, 添加第一個敵人. 導入章魚敵人圖片到Textures文件夾, 創建一個Sprite對象, 命名為Poulpi, 設置Sprite為剛才導入的章魚圖片, 設置Scale為(0.4,0.4,1), 添加碰撞機(Polygon Collider 2D或Box Collider 2D都可以), 如果是Box Collider 2D, 設置Size為(4,4), 添加Rigidbody 2D組件, 設置Gravity Scale為0, 並且勾選Fixed Angles屬性. 設置完成后, 將對象保存為預制. 在這里只讓章魚簡單的往前行走, 創建一個腳本, 命名為MoveScript, 代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
///  當前游戲對象簡單的移動行為
/// </summary>
public class MoveScript : MonoBehaviour
{
    #region 1 - 變量

    /// <summary>
    /// 物體移動速度
    /// </summary>
    public Vector2 speed = new Vector2(10, 10);

    /// <summary>
    /// 移動方向
    /// </summary>
    public Vector2 direction = new Vector2(-1, 0);

    private Vector2 movement;

    #endregion

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        // 2 - 保存運動軌跡
        movement = new Vector2(speed.x * direction.x, speed.y * direction.y);
    }

    void FixedUpdate()
    {
        // 3 - 讓游戲物體移動
        rigidbody2D.velocity = movement;
    }
}

將此腳本附加到章魚對象上, 現在運行嗎可以看到章魚往前移動, 如圖:

此時如果主角和章魚發生碰撞, 會互相阻塞對方的移動.

4.射擊

導入子彈圖片到"Textures"文件夾,創建一個Sprite游戲對象,命名為"PlayerShot",設置其"Sprite"屬性為剛才導入的圖片,設置"Scale"屬性為(0.75, 0.75, 1),添加"Rigidbody 2D"組件,其"Gravity Scale"屬性為0,並且勾選"Fixed Angles"屬性框,添加"Box Collider 2D"組件,其Size為(1, 1),並且勾選"IsTrigger"屬性。勾選"IsTrigger"屬性表示該碰撞體用於觸發事件,並將被物理引擎所忽略。意味着,子彈將穿過觸碰到的對象,而不會阻礙對象的移動,觸碰的時候將會引發"OnTriggerEnter2D"事件。創建一個腳本,命名為"ShotScript",代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 子彈行為
/// </summary>
public class ShotScript : MonoBehaviour
{
    #region 1 - 變量

    /// <summary>
    /// 造成傷害
    /// </summary>
    public int damage = 1;

    /// <summary>
    /// 子彈歸屬 , true-敵人的子彈, false-玩家的子彈
    /// </summary>
    public bool isEnemyShot = false;

    #endregion

    // Use this for initialization
    void Start()
    {
        // 2 - 為避免任何泄漏,只給予有限的生存時間.[20秒]
        Destroy(gameObject, 20);
    }
}

將此腳本附加到子彈對象上,然后將"MoveScript"腳本也附加到子彈對象上以便可以移動。保存此對象為預制。接着,讓碰撞產生傷害效果。創建一個腳本,命名為"HealthScript",代碼如下:

 

using UnityEngine;
using System.Collections;

/// <summary>
/// 處理生命值和傷害
/// </summary>
public class HealthScript : MonoBehaviour
{
    #region 1 - 變量

    /// <summary>
    /// 總生命值
    /// </summary>
    public int hp = 1;

    /// <summary>
    /// 敵人標識
    /// </summary>
    public bool isEnemy = true;

    #endregion

    /// <summary>
    /// 對敵人造成傷害並檢查對象是否應該被銷毀
    /// </summary>
    /// <param name="damageCount"></param>
    public void Damage(int damageCount)
    {
        hp -= damageCount;

        if (hp <= 0)
        {
            // 死亡! 銷毀對象!
            Destroy(gameObject);
        }
    }

    void OnTriggerEnter2D(Collider2D otherCollider)
    {
        ShotScript shot = otherCollider.gameObject.GetComponent<ShotScript>();
        if (shot != null)
        {
            // 判斷子彈歸屬,避免誤傷
            if (shot.isEnemyShot != isEnemy)
            {
                Damage(shot.damage);

                // 銷毀子彈
                // 記住,總是針對游戲的對象,否則你只是刪除腳本
                Destroy(shot.gameObject); 
            }
        }
    }
}

將此腳本附加到Poulpi預制體上。現在運行,讓子彈和章魚碰撞,可以看到如下結果:

如果章魚的生命值大於子彈的傷害值,那么章魚就不會被消滅,可以試着通過改變章魚對象的"HealthScript"的hp值。

接着,我們來准備射擊。創建一個腳本,命名為"WeaponScript",代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 發射子彈
/// </summary>
public class WeaponScript : MonoBehaviour
{
    #region 1 - 變量

    /// <summary>
    /// 子彈預設
    /// </summary>
    public Transform shotPrefab;

    /// <summary>
    /// 兩發子彈之間的發射間隔時間
    /// </summary>
    public float shootingRate = 0.25f;

    /// <summary>
    /// 當前冷卻時間
    /// </summary>
    private float shootCooldown;

    #endregion

    // Use this for initialization
    void Start()
    {
        // 初始化冷卻時間為0
        shootCooldown = 0f;
    }

    // Update is called once per frame
    void Update()
    {
        // 冷卻期間實時減少時間
        if (shootCooldown > 0)
        {
            shootCooldown -= Time.deltaTime;
        }
    }

    /// <summary>
    /// 射擊
    /// </summary>
    /// <param name="isEnemy">是否是敵人的子彈</param>
    public void Attack(bool isEnemy)
    {
        if (CanAttack)
        {
            if (isEnemy)
            {
                SoundEffectsHelper.Instance.MakeEnemyShotSound();
            }
            else
            {
                SoundEffectsHelper.Instance.MakePlayerShotSound();
            }

            shootCooldown = shootingRate;

            // 創建一個子彈
            var shotTransform = Instantiate(shotPrefab) as Transform;

            // 指定子彈位置
            shotTransform.position = transform.position;

            // 設置子彈歸屬
            ShotScript shot = shotTransform.gameObject.GetComponent<ShotScript>();
            if (shot != null)
            {
                shot.isEnemyShot = isEnemy;
            }

            // 設置子彈運動方向
            MoveScript move = shotTransform.gameObject.GetComponent<MoveScript>();
            if (move != null)
            {
                // towards in 2D space is the right of the sprite
                move.direction = this.transform.right; 
            }
        }
    }

    /// <summary>
    /// 武器是否准備好再次發射
    /// </summary>
    public bool CanAttack
    {
        get
        {
            return shootCooldown <= 0f;
        }
    }
}

將這個腳本附加到主角對象上,設置其"Shot Prefab"屬性為"PlayerShot"預制體。打開"PlayerScript"腳本,在Update()方法里面,加入以下片段:

// Update is called once per frame
    void Update()
    {

        #region 射擊控制

        // 5 - 射擊
        bool shoot = Input.GetButtonDown("Fire1");
        shoot |= Input.GetButtonDown("Fire2");
        // 小心:對於Mac用戶,按Ctrl +箭頭是一個壞主意

        if (shoot)
        {
            WeaponScript weapon = GetComponent<WeaponScript>();
            if (weapon != null)
            {
                weapon.Attack(false);
            }
        }

        #endregion
    }

當收到射擊的按鈕狀態,調用Attack(false)方法。現在運行,可以看到如下結果:

接下來,准備創建敵人的子彈。導入敵人子彈圖片到"Textures"文件夾,選中"PlayerShot"預制體,按下Ctrl+D進行復制,命名為"EnemyShot1",然后改變其Sprite為剛才導入的圖片,設置其Scale為(0.35, 0.35, 1)。接着,讓章魚可以射擊。將"WeaponScript"腳本附加到章魚對象上,拖動"EnemyShot1"預制體到其"Shot Prefab"屬性,創建一個腳本,命名為"EnemyScript",用來簡單地每一幀進行自動射擊,代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 敵人通用行為
/// </summary>
public class EnemyScript : MonoBehaviour
{
    #region 變量

    private WeaponScript weapon;

    #endregion

    void Awake()
    {
        // 只檢索一次武器
        weapon = GetComponent<WeaponScript>();
    }

    // Update is called once per frame
    void Update()
    {
            // 自動開火
            if (weapon != null && weapon.CanAttack)
            {
                weapon.Attack(true);
            }
    }
}

將此腳本附加到章魚對象上,現在運行,可以看到如下結果:

可以看到章魚向右射擊了子彈,這是因為代碼就是讓它那么做的。實際上,應該做到可以朝向任何方向射擊子彈。修改"WeaponScript"中的Attack方法,代碼為如下:

    /// <summary>
    /// 射擊
    /// </summary>
    /// <param name="isEnemy">是否是敵人的子彈</param>
    public void Attack(bool isEnemy)
    {
        if (CanAttack)
        {
            if (isEnemy)
            {
                SoundEffectsHelper.Instance.MakeEnemyShotSound();
            }
            else
            {
                SoundEffectsHelper.Instance.MakePlayerShotSound();
            }

            shootCooldown = shootingRate;

            // 創建一個子彈
            var shotTransform = Instantiate(shotPrefab) as Transform;

            // 指定子彈位置
            shotTransform.position = transform.position;

            // 設置子彈歸屬
            ShotScript shot = shotTransform.gameObject.GetComponent<ShotScript>();
            if (shot != null)
            {
                shot.isEnemyShot = isEnemy;
            }

            // 設置子彈方向
            MoveScript move = shotTransform.gameObject.GetComponent<MoveScript>();
            if (move != null)
            {
                // towards in 2D space is the right of the sprite
                //move.direction = move.direction; 

                // 如果是敵人的子彈, 則改變方向和移動速度
                if (shot.isEnemyShot) { move.direction.x = -1f; move.speed = new Vector2(15, 15); } else { move.direction.x = 1f; move.speed = new Vector2(10, 10); }
            }
        }
    }

可以適當調整子彈的移動速度,它應該快於章魚的移動速度。現在運行,如下圖所示:

目前,當主角和章魚碰撞時,僅僅只是阻礙對方的移動而已,在這里改成互相受到傷害。打開"PlayerScript"文件,添加如下代碼:

    void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.ToString().IndexOf("Poulpi") >= 0)
        {
            bool damagePlayer = false;

            // 與敵人發生碰撞
            EnemyScript enemy = collision.gameObject.GetComponent<EnemyScript>();
            if (enemy != null)
            {
                // 殺死敵人
                HealthScript enemyHealth = enemy.GetComponent<HealthScript>();
                if (enemyHealth != null)
                {
                    enemyHealth.Damage(enemyHealth.hp);
                }

                damagePlayer = true;
            }

            // 玩家也受到傷害
            if (damagePlayer)
            {
                HealthScript playerHealth = this.GetComponent<HealthScript>();
                if (playerHealth != null)
                {
                    playerHealth.Damage(1);
                }
            }
        }
    }

5.視差卷軸效果
為了達到這種視差卷軸的效果,可以讓背景層以不同的速度進行移動,越遠的層,移動地越慢。如果操作得當,這可以造成深度的錯覺,這將很酷,又是可以容易做到的效果。在這里存在兩個滾動:

  • 主角隨着攝像機向前推進
  • 背景元素除了攝像機的移動外,又以不同的速度移動

一個循環的背景將在水平滾動的時候,一遍又一遍的重復進行顯示。現有的層如下:

Layer Loop Position   
0 - Background Yes (0, 0, 10)
1 - Middleground No (0, 0, 5)
2 - Foreground No (0, 0, 0)

 

接下來,實現無限背景。當左側的背景對象遠離了攝像機的左邊緣,那么就將它移到右側去,一直這樣無限循環,如下圖所示:

要做到檢查的對象渲染器是否在攝像機的可見范圍內,需要一個類擴展。創建一個C#文件,命名為"RendererExtensions.cs",代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 渲染擴展
/// </summary>
public static class RendererExtensions
{
    /// <summary>
    /// 檢查對象渲染器是否在攝像機的可見范圍內
    /// </summary>
    /// <param name="renderer">渲染對象</param>
    /// <param name="camera">攝像機</param>
    /// <returns></returns>
    public static bool IsVisibleFrom(this Renderer renderer, Camera camera)
    {
        Plane[] planes = GeometryUtility.CalculateFrustumPlanes(camera);
        return GeometryUtility.TestPlanesAABB(planes, renderer.bounds);
    }
}

接下來,可以開始實現不帶背景循環的滾動。創建一個腳本,命名為"ScrollingScript",代碼如下:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// 背景視差滾動腳本
/// </summary>
public class ScrollingScript : MonoBehaviour
{
    #region 變量

    /// <summary>
    /// 滾動速度
    /// </summary>
    public Vector2 speed = new Vector2(2, 2);

    /// <summary>
    /// 移動方向
    /// </summary>
    public Vector2 direction = new Vector2(-1, 0);

    /// <summary>
    /// 相機是否運動
    /// </summary>
    public bool isLinkedToCamera = false;

    /// <summary>
    /// 背景是否循環
    /// </summary>
    public bool isLooping = false;

    /// <summary>
    /// 渲染對象名單
    /// </summary>
    private List<Transform> backgroundPart;

    #endregion

    // Use this for initialization
    void Start()
    {
        // 只循環背景
        if (isLooping)
        {
            // 獲取該層渲染器的所有子集對象
            backgroundPart = new List<Transform>();

            for (int i = 0; i < transform.childCount; i++)
            {
                Transform child = transform.GetChild(i);

                // 只添加可見子集
                if (child.renderer != null)
                {
                    backgroundPart.Add(child);
                }
            }

            // 根據位置排序
            // Note: 根據從左往右的順序獲取子集對象
            // 我們需要增加一些條件來處理所有可能的滾動方向。
            backgroundPart = backgroundPart.OrderBy(t => t.position.x).ToList();
        }
    }

    // Update is called once per frame
    void Update()
    {
        // 創建運動狀態
        Vector3 movement = new Vector3(speed.x * direction.x, speed.y * direction.y, 0);

        movement *= Time.deltaTime;
        transform.Translate(movement);

        // 移動相機
        if (isLinkedToCamera)
        {
            Camera.main.transform.Translate(movement);
        }

        // 循環
        if (isLooping)
        {
            // 獲取第一個對象
            // 該列表的順序是從左往右(基於x坐標)
            Transform firstChild = backgroundPart.FirstOrDefault();

            if (firstChild != null)
            {
                // 檢查子集對象(部分)是否在攝像機前已准備好.
                // We test the position first because the IsVisibleFrom
                // method is a bit heavier to execute.
                if (firstChild.position.x < Camera.main.transform.position.x)
                {
                    // 如果子集對象已經在攝像機的左側,我們測試它是否完全在外面,以及是否需要被回收.
                    if (firstChild.renderer.IsVisibleFrom(Camera.main) == false)
                    {
                        // 獲取最后一個子集對象的位置
                        Transform lastChild = backgroundPart.LastOrDefault();
                        Vector3 lastPosition = lastChild.transform.position;
                        Vector3 lastSize = (lastChild.renderer.bounds.max - lastChild.renderer.bounds.min);

                        // 將被回收的子集對象作為最后一個子集對象
                        // Note: 當前只橫向滾動.
                        firstChild.position = new Vector3(lastPosition.x + lastSize.x, firstChild.position.y, firstChild.position.z);

                        // 將被回收的子集對象設置到backgroundPart的最后位置.
                        backgroundPart.Remove(firstChild);
                        backgroundPart.Add(firstChild);
                    }
                }
            }
        }
    }
}

在Start方法里,使用了LINQ將它們按X軸進行排序。

將這個腳本附加到以下對象上,並且設置好其屬性值,如下:
Layer Speed Direction Linked to Camera
0 - Background (1, 1) (-1, 0, 0) No
1 - Middleground (2.5, 2.5) (-1, 0, 0) No
Player (1, 1) (1, 0, 0) Yes
現在添加更多的元素到場景上:
  • 添加兩個天空背景到0 - Background
  • 添加一些平台到2 - Middleground
  • 添加更多的敵人到3 - Foreground,放置在攝像機的右邊

 將"0 - Background"對象里的"ScrollingScript"組件的"Is Looping"屬性勾選,現在運行,就可以看到視差卷軸的效果,如下圖所示:

 

接下來,修改敵人腳本,讓敵人靜止不動,且無敵,直到攝像機看到它們。另外,當它們移出屏幕時,則立刻移除它們。修改"EnemyScript"腳本,代碼為如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 敵人通用行為
/// </summary>
public class EnemyScript : MonoBehaviour
{
    #region 變量

    /// <summary>
    /// 是否登場
    /// </summary>
    private bool hasSpawn;
    private MoveScript moveScript;
    private WeaponScript weapon;

    #endregion

    void Awake()
    {
        // 只檢索一次武器
        weapon = GetComponent<WeaponScript>();
        // 當未登場的時候檢索腳本以禁用
        moveScript = GetComponent<MoveScript>();
    }

    // Use this for initialization
    void Start()
    {
        hasSpawn = false;

        // 禁止一切
        // -- 碰撞機
        collider2D.enabled = false;
        // -- 移動
        moveScript.enabled = false;
        // -- 射擊
        weapon.enabled = false;
    }

    // Update is called once per frame
    void Update()
    {
        // 檢查敵人是否登場
        if (hasSpawn == false)
        {
            if (renderer.IsVisibleFrom(Camera.main))
            {
                Spawn();
            }
        }
        else
        {
            // 自動開火
            if (weapon != null && weapon.enabled && weapon.CanAttack)
            {
                weapon.Attack(true);
            }

            // 超出攝像機視野,則銷毀對象
            if (renderer.IsVisibleFrom(Camera.main) == false)
            {
                Destroy(gameObject);
            }
        }
    }

    /// <summary>
    /// 激活自身
    /// </summary>
    private void Spawn()
    {
        hasSpawn = true;

        // 啟用一切
        // -- 碰撞機
        collider2D.enabled = true;
        // -- 移動
        moveScript.enabled = true;
        // -- 射擊
        weapon.enabled = true;
    }
}

在游戲過程中,可以發現主角並不是限制在攝像機區域內的,可以隨意離開攝像機,現在來修復這個問題。打開"PlayerScript"腳本,在Update方法里面添加如下代碼:

        #region 確保沒有超出攝像機邊界

        var dist = (transform.position - Camera.main.transform.position).z;
        var leftBorder = Camera.main.ViewportToWorldPoint(new Vector3(0, 0, dist)).x;
        var rightBorder = Camera.main.ViewportToWorldPoint(new Vector3(1, 0, dist)).x;
        var topBorder = Camera.main.ViewportToWorldPoint(new Vector3(0, 0, dist)).y;
        var bottomBorder = Camera.main.ViewportToWorldPoint(new Vector3(0, 1, dist)).y;

        transform.position = new Vector3(
          Mathf.Clamp(transform.position.x, leftBorder, rightBorder),
          Mathf.Clamp(transform.position.y, topBorder, bottomBorder),
          transform.position.z
        );

        #endregion

6.粒子效果

制作一個爆炸的粒子,用於敵人或者主角被摧毀時進行顯示。創建一個"Particle System",導入煙圖片到"Textures"文件夾,改變其"Texture Type"為"Texture",並且勾選"Alpha Is Transparent"屬性,附加這個紋理到粒子上,將其拖動到粒子對象上,更改其Shader為"Particles"→"Alpha Blended",接着更改一些屬性,如下所示:

Category Parameter name Value
General Duration 1
General Max Particles 15
General Start Lifetime 1
General Start Color Gray
General Start Speed 3
General Start Size 2
Emission Bursts 0 : 15
Shape Shape Sphere
Color Over Lifetime Color 見下圖
Size Over Lifetime Size 見下圖

其中Color Over Lifetime要設置成在結束時,有個淡出的效果,如下圖所示:

 

Size Over Lifetime選擇一個遞減曲線,如下圖所示:

當調整完成后,取消勾選"Looping",現在粒子效果為如下:

保存成預制,命名為"SmokeEffect",放在"Prefabs/Particles"文件夾下。現在創建另一個粒子,火焰效果,使用默認材質即可。其他設置如下:

Category Parameter name Value
General Looping false
General Duration 1
General Max Particles 10
General Start Lifetime 1
General Start Speed 0.5
General Start Size 2
Emission Bursts 0 : 10
Shape Shape Box
Color Over Lifetime Color 見下圖

 其中Color Over Lifetime要設置成有一個黃色到橙色的漸變,最后淡出,如下圖所示:

 

粒子效果為:

保存成預制,命名為"FireEffect"。創建一個腳本,命名為"SpecialEffectsHelper",代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 從代碼中創建粒子特效
/// </summary>
public class SpecialEffectsHelper : MonoBehaviour
{
    /// <summary>
    /// Singleton
    /// </summary>
    public static SpecialEffectsHelper Instance;

    public ParticleSystem smokeEffect;
    public ParticleSystem fireEffect;

    void Awake()
    {
        // Register the singleton
        if (Instance != null)
        {
            Debug.LogError("Multiple instances of SpecialEffectsHelper!");
        }

        Instance = this;
    }

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }

    /// <summary>
    /// 在給定位置創建爆炸特效
    /// </summary>
    /// <param name="position"></param>
    public void Explosion(Vector3 position)
    {
        // 煙霧特效
        instantiate(smokeEffect, position);
        // 火焰特效
        instantiate(fireEffect, position);
    }

    /// <summary>
    /// 從預制體中實例化粒子特效
    /// </summary>
    /// <param name="prefab"></param>
    /// <returns></returns>
    private ParticleSystem instantiate(ParticleSystem prefab, Vector3 position)
    {
        ParticleSystem newParticleSystem = Instantiate(prefab, position, Quaternion.identity) as ParticleSystem;

        // 確保它會被銷毀
        Destroy(newParticleSystem.gameObject, newParticleSystem.startLifetime);

        return newParticleSystem;
    }
}

這里創建了一個單例,可以讓任何地方都可以產生煙和火焰的粒子。將這個腳本附加到"Scripts"對象,設置其屬性"Smoke Effect"和"Fire Effect"為對應的預制體。現在是時候調用這個腳本了,打開"HealthScript"腳本文件,在OnTriggerEnter方法里面,更新成如下代碼:

   /// <summary>
    /// 對敵人造成傷害並檢查對象是否應該被銷毀
    /// </summary>
    /// <param name="damageCount"></param>
    public void Damage(int damageCount)
    {
        hp -= damageCount;

        if (hp <= 0)
        {
            // 爆炸特效
            SpecialEffectsHelper.Instance.Explosion(transform.position);
            // 播放音效
            SoundEffectsHelper.Instance.MakeExplosionSound();
            // 死亡! 銷毀對象!
            Destroy(gameObject);
        }
    }

現在運行,射擊敵人,可以看到如下效果:

7.游戲音效

現在來添加一些聲音。將聲音資源放入"Sounds"文件夾,取消勾選每一個聲音的"3D sound"屬性,因為這是2D游戲。准備播放背景音樂,創建一個游戲對象,命名為"Music",其Position為(0, 0, 0),將背景音樂拖到這個對象上,然后勾選"Mute"屬性。因為音效總是在一定的時機進行播放,所以創建一個腳本文件,命名為"SoundEffectsHelper",代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 創建音效實例
/// </summary>
public class SoundEffectsHelper : MonoBehaviour
{
    /// <summary>
    /// 靜態實例
    /// </summary>
    public static SoundEffectsHelper Instance;

    public AudioClip explosionSound;
    public AudioClip playerShotSound;
    public AudioClip enemyShotSound;

    void Awake()
    {
        // 注冊靜態實例
        if (Instance != null)
        {
            Debug.LogError("Multiple instances of SoundEffectsHelper!");
        }
        Instance = this;
    }

    public void MakeExplosionSound()
    {
        MakeSound(explosionSound);
    }

    public void MakePlayerShotSound()
    {
        MakeSound(playerShotSound);
    }

    public void MakeEnemyShotSound()
    {
        MakeSound(enemyShotSound);
    }

    /// <summary>
    /// 播放給定的音效
    /// </summary>
    /// <param name="originalClip"></param>
    private void MakeSound(AudioClip originalClip)
    {
        // 做一個非空判斷, 防止異常導致剩余操作被中止
        if (Instance.ToString() != "null")
        {
            // 因為它不是3D音頻剪輯,位置並不重要。
            AudioSource.PlayClipAtPoint(originalClip, transform.position);
        }
    }
}

這里我做了一個非空判斷, 因為按照原例中的程序, 當章魚死亡在攝像機邊界時, 會導致Script對象被銷毀! 從而引發程序異常. 我沒有找到被銷毀的准確原因, 所以暫時折中一下, 加了個判斷, 以確保程序能照常運行. 如果哪位大神有知道原因的話也歡迎告訴我來更新此文.

將此腳本附加到"Scripts"對象上,然后設置其屬性值,如下圖所示:

接着,在"HealthScript"腳本文件里,播放粒子效果后面,添加代碼:

SoundEffectsHelper.Instance.MakeExplosionSound();

在"WeaponScript"腳本文件里,Attack方法中,添加代碼:

        if (isEnemy)
            {
                SoundEffectsHelper.Instance.MakeEnemyShotSound();
            }
            else
            {
                SoundEffectsHelper.Instance.MakePlayerShotSound();
            }

現在運行,就可以聽到聲音了。

8.菜單

創建簡單的菜單,以便游戲可以重新開始。導入背景圖片和LOGO圖片到"Textures"文件夾的子文件夾"Menu"。創建一個新的場景,命名為"Menu"。添加背景Sprite對象,其Position為(0, 0, 1),Size為(2, 2, 1)。添加LOGO的Sprite對象,其Position為(0, 2, 0),Size為(0.75, 0.75, 1)。添加一個空對象,命名為"Scripts",用來加載腳本。現在為這個啟動畫面,添加一個開始按鈕。創建一個腳本文件,命名為"MenuScript",代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// Title screen script
/// </summary>
public class MenuScript : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }

    void OnGUI()
    {
        const int buttonWidth = 84;
        const int buttonHeight = 60;

        // 在開始游戲界面繪制一個按鈕
        if (
            // Center in X, 2/3 of the height in Y
          GUI.Button(new Rect(Screen.width / 2 - (buttonWidth / 2), (2 * Screen.height / 3) - (buttonHeight / 2), buttonWidth, buttonHeight), "開始游戲")
        )
        {
            // On Click, load the first level.
            // "Stage1" is the name of the first scene we created.
            Application.LoadLevel("Stage1");
        }
    }
}

將此腳本附加到"Scripts"對象上。現在運行,可以看到如下效果:

但是,點擊按鈕會崩潰,因為沒有將Stage1場景添加進來。打開"File"→"Build Settings",將場景"Menu"和"Stage1"拖動到上面的"Scenes In Build"里面。再次運行,就可以看到按鈕正常切換場景了。當主角被摧毀時,需要可以重新開始游戲。創建一個腳本文件,命名為"GameOverScript",代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 開始或退出游戲
/// </summary>
public class GameOverScript : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }

    void OnGUI()
    {
        const int buttonWidth = 120;
        const int buttonHeight = 60;

        // 在x軸中心, y軸1/3處創建"重來"按鈕
        if (GUI.Button(new Rect(Screen.width / 2 - (buttonWidth / 2), (1 * Screen.height / 3) - (buttonHeight / 2), buttonWidth, buttonHeight), "再來一次"))
        {
            // 重新加載游戲場景
            Application.LoadLevel("Stage1");
        }

        // x軸中心, y軸2/3處創建"返回菜單"按鈕
        if (GUI.Button(new Rect(Screen.width / 2 - (buttonWidth / 2), (2 * Screen.height / 3) - (buttonHeight / 2), buttonWidth, buttonHeight), "返回菜單"))
        {
            // 加載菜單場景
            Application.LoadLevel("Menu");
        }
    }
}

在主角死亡的時候,調用這個腳本。打開"PlayerScript"文件,添加如下代碼:

    void OnDestroy()
    {
        // Game Over.
        // Add the script to the parent because the current game
        // object is likely going to be destroyed immediately.
        transform.parent.gameObject.AddComponent<GameOverScript>();
    }

現在運行,當死亡時,就會出現按鈕,如下圖所示:

9.代碼創建平台島嶼和敵人

現在, 一個簡單的橫版射擊小游戲已經有雛形了, 然后手動創建有限的島嶼和敵人畢竟有耗盡的時候. 這個時候我們可以在代碼中動態的去創建敵人和島嶼, 這樣只要玩家還存活, 就會一直有敵人出現, 有點無盡版的意思.

創建一個腳本文件,命名為"MakePlatformScript",代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 制造平台
/// </summary>
public class MakePlatformScript : MonoBehaviour
{
    /// <summary>
    /// 平台預設體1
    /// </summary>
    public Transform platform1Prefab;
    /// <summary>
    /// 平台預設體2
    /// </summary>
    public Transform platform2Prefab;

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (transform.childCount < 4)
        {
            if (Random.Range(0, 2) > 0)
            {
                CreatePlatform1();
            }
            else
            {
                CreatePlatform2();
            }
        }
    }

    void CreatePlatform1()
    {
        var platformTransform = Instantiate(platform1Prefab) as Transform;

        platformTransform.position = new Vector3(Camera.main.transform.position.x + Random.Range(14, 23), Random.Range(-3, 3), 5);
        platformTransform.transform.parent = transform;
    }

    void CreatePlatform2()
    {
        var platformTransform = Instantiate(platform2Prefab) as Transform;

        platformTransform.position = new Vector3(Camera.main.transform.position.x + Random.Range(14, 23), Random.Range(-3, 3), 5);
        platformTransform.transform.parent = transform;
    }
}

將MakePlatformScript附加到1 - Middleground, 設置它的預制體為對應的平台島嶼預制體.

接着繼續創建一個腳本文件,命名為"MakeEnemyScript",代碼如下:

using UnityEngine;
using System.Collections;

/// <summary>
/// 制造敵人
/// </summary>
public class MakeEnemyScript : MonoBehaviour
{
    /// <summary>
    /// 敵人預設體
    /// </summary>
    public Transform enemyPrefab;

    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (transform.childCount < 2)
        {
            CreateEnemy();
        }
    }

    /// <summary>
    /// 創建敵人
    /// </summary>
    void CreateEnemy()
    {
        var enemyTransform = Instantiate(enemyPrefab) as Transform;
        enemyTransform.position = new Vector3(Camera.main.transform.position.x + 15, Random.Range(-4, 4), 0);
        enemyTransform.transform.parent = transform;
        MoveScript move = enemyTransform.gameObject.GetComponent<MoveScript>();
        if (move != null)
        {
            move.direction.x = -1f;
            move.speed = new Vector2(3, 3);
        }
    }
}

將MakeEnemyScript附加到2 - Foreground, 同樣, 設置對應的預制體為章魚.

現在再次運行, 就會看到系統自動創建的章魚敵人和漂浮島嶼了!

 

到這里, 整個游戲就完成了, 推薦大家可以去頁首找原文鏈接去看看, 老外的原文解說的更詳細, 我也是在原文的基礎上自己再次整理.

最后附上程序源碼, 文中有紕漏或看不太明白的地方大家可以對照着源碼一起看, 歡迎留言指教.

源代碼地址: http://pan.baidu.com/s/1b51XmQ


免責聲明!

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



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