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

教程基於http://pixelnest.io/tutorials/2d-game-unity/ , 這個例子感受仍是比較經典的, 網上轉載的也比較多. 恰好最近也在學習U3D, 作的過程當中本身又修改了一些地方, 寫篇文和你們一塊兒分享下, 同時也加深記憶. 有什麼紕漏的地方還請你們多包涵.html

1.建立第一個場景dom

新建工程,在Project面板建立文件夾, 是爲了更好的規劃管理資源文件.ide

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

2.添加顯示背景學習

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

 

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

接着添加背景元素. 導入平臺島嶼圖片到Textures文件夾, 選中Platforms圖片, 設置它的Sprite Mode爲Multiple, 而後點擊Sprite Editor, 以下圖:ui

在彈出的Sprite Editor窗口中, 進行繪製每一個平臺島嶼的包圍矩形, 以便將紋理分隔成更小的部分. 而後分別命名爲platform1和platform2.this

建立一個新的Sprite對象, 設置它的Sprite爲platform1. 而後一樣再建立一個Sprite對象, 設置Sprite爲platform2. 將它們放置到1 - Middleground對象裏, 而且確認他們的Z座標爲0. 設置完成後, 將這兩個對象從Hierarchy面板拖動到Project面板下的Prefabs文件夾, 保存爲預製對象. 接着, 爲了不顯示順序問題, 修改下游戲對象的Z座標, 以下所示:spa

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

相關文章
相關標籤/搜索