[ Unity 2D ] 教學 #10 – 小總結●小飛兵 射擊遊戲實作(一)

標籤: , , ,

已經到第10篇教學文了,這次我們將實作出一款可以遊玩的射擊遊戲,將會用到之前所有教學文所教的東西。

※注意本篇內容較多,請小心食用

這篇教學文也可以算做是一個複習,接下來我們會把之前寫過的程式碼再寫過一遍,並用這些程式碼做出一個能夠遊玩的小遊戲,當然特效與介面處理還不是這一篇的重點。

首先,先準備好以下素材

  • Unit(C# Script) – 角色的資料
  • BulletMove(C# Script) – 子彈的資料與移動
  • Move(C# Script) – 玩家移動
  • Shooting(C# Script) – 發射子彈
  • AI(C# Script) – 敵人移動與攻擊
  • 小飛兵圖片素材
  • 子彈A圖片素材
  • 子彈B圖片素材
  • 敵方角色圖片素材

在設計遊戲時,能先構想好愈多細節愈好,當然要構想到完美是不可能的,一定會在撰寫時修修改改,我現在要做的小飛兵遊戲大概有以下幾項必要的設定

  • 有一隻叫小飛兵的主要人物
  • 可以用方向鍵操控小飛兵
  • 按下z鍵可以發射子彈
  • 遊戲中會一直出現敵人
  • 敵人碰到小飛兵的子彈會扣血
  • 敵人會自己移動並發射子彈,小飛兵碰到敵人的子彈會扣血

在撰寫程式與撰寫遊戲時,把所有項目先後順序條列出來,可以幫助寫程式時遇到的一些邏輯障礙,接下來只要依照項目一個一個將功能做出來,很快就能完成了。

先將剛剛所提到的素材都放進來,就可以開始動作了
※由於大多是前幾篇教學文中的內容,就不多做細節上的說明了

—有一隻叫小飛兵的主要角色—

將小飛兵的圖片檔從Project中拉近場景內,並將遊戲物件命名為小飛兵,調整Main Camera中的Size屬性讓畫面處於適當的位置,並將小飛兵拉近Project視窗中作為prefab,以待日後有需要時可以使用。

—可以用方向鍵操控小飛兵—

開啟Move(C# Script)在Update中輸入移動的程式碼

if (Input.GetKey(KeyCode.LeftArrow))
{
    transform.Translate(Vector2.left * Time.deltaTime); //向左移動
}
if (Input.GetKey(KeyCode.RightArrow))
{
    transform.Translate(Vector2.right * Time.deltaTime); //向右移動
}
if (Input.GetKey(KeyCode.UpArrow))
{
    transform.Translate(Vector2.up * Time.deltaTime); //向上移動
}
if (Input.GetKey(KeyCode.DownArrow))
{
    transform.Translate(Vector2.down * Time.deltaTime); //向下移動
}

此時我發現我需要一個方便控制移動速度的參數,Speed,因此打開Unit(C# Script),宣告全域變數

public string UnitName; //單位名稱
public int HP = 100;    //生命值
public float Speed = 3; //移動速度
public int Atk = 10;    //攻擊力

回到Move,在Update內的移動之前加上

float speed = GetComponent<Unit>().Speed;

並將四種移動都承上speed的值。

此時Move跟Unit內的程式碼應該會長得像這樣

using UnityEngine;
using System.Collections;

public class Move : MonoBehaviour {

    void Start () {
  
    }
  
    void Update () {
        float speed = GetComponent<Unit>().Speed; //獲取Unit組件中的Speed值
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            transform.Translate(Vector2.left * Time.deltaTime * speed); //向左移動
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            transform.Translate(Vector2.right * Time.deltaTime * speed); //向右移動
        }
        if (Input.GetKey(KeyCode.UpArrow))
        {
            transform.Translate(Vector2.up * Time.deltaTime * speed);  //向上移動
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            transform.Translate(Vector2.down * Time.deltaTime * speed); //向下移動
        }
    }
}
using UnityEngine;
using System.Collections;

public class Unit : MonoBehaviour {
    public string UnitName;
    public int HP = 100;
    public float Speed = 3;
    public int Atk = 10;

    void Start () {
  
    }
  
    void Update () {
    
    }
}

替場景上的小飛兵加上Move與Unit組件,按下屬性視窗右上方的Apple更新prefab,之後按下撥放鍵測試。

—按下z鍵可以發射子彈—

將子彈A的圖片拉進場景中建立物件,再拉進project視窗中建立prefab後將場景的子彈A給刪除

開啟Shooting,宣告一個來儲存子彈物件的變數Bullet(子彈)

public GameObject Bullet; //子彈物件

在Update中撰寫發射(創造)子彈的程式碼

if (Input.GetKeyDown(KeyCode.Z))
{
    Instantiate(Bullet, transform.position, new Quaternion(0, 0, 0, 0));
    //克隆一個Bullet在小飛兵的位置
}

將小飛兵套上Shooting,測試能不能創造出子彈

 

測試成功,接下來撰寫子彈自動移動程式碼

開啟BulletMove,在Update中寫下向上移動的程式

transform.Translate(Vector2.up * Time.deltaTime);//向上移動

而我希望之後的所有不同種類的子彈都可套用這個程式碼,因此我給他添加上速度的參數

public float BulletSpeed = 5;

並修改剛剛在Update中的程式碼

transform.Translate(Vector2.up * BulletSpeed * Time.deltaTime);//向上移動

將子彈A套上Bullet實際測試一次

看起來是成功了,不過子彈的物件不會消失,若發射太多子彈會讓記憶體消耗很大,因此在Update中加入判斷位置的程式碼,如果飛出太遠就將子彈移除。

if (transform.position.y > 20 || transform.position.y < -20)
{
    //如果物件的Y值大於20或小於20 就將物件移除
    Destroy(gameObject);
}

再測試一次,就會發現子彈過一段時間後就自己消失了

這樣發射子彈的部分就完成了,目前的Shooting與BulletMove程式碼會是以下這樣

using UnityEngine;
using System.Collections;

public class Shooting : MonoBehaviour {
    public GameObject Bullet; //子彈物件
    void Start () {
  
    }
  
    void Update () {
        if (Input.GetKeyDown(KeyCode.Z))
        {
            Instantiate(Bullet, transform.position, new Quaternion(0, 0, 0, 0));
            //克隆一個Bullet在小飛兵的位置
        }
    }
}
using UnityEngine;
using System.Collections;

public class BulletMove : MonoBehaviour {
    public float BulletSpeed =5;
    void Start () {
  
    }

    void Update() {
        transform.Translate(Vector2.up * BulletSpeed * Time.deltaTime);//向上移動
        if (transform.position.y > 20 || transform.position.y < -20)
        {
            //如果物件的Y值大於20或小於20 就將物件移除
            Destroy(gameObject);

        }
    }
}
–遊戲中會一直出現敵人–

雖然說是一直出現,不過我們先做只有一個敵人就好

將素材庫中的黑男拉近場景中,並調整到適當位置,順便把小飛兵也調整到適當位置

然後就……..完成了。

–敵人碰到小飛兵的子彈會扣血–

要造成碰到子彈後扣血,可以使用碰撞事件,在碰撞到子彈時觸發碰撞事件來造成傷害。

為了觸發碰撞事件,我們在小飛兵、敵人、子彈上全部套上碰撞體

依照物件的形狀,我在小飛兵跟敵人身上套用Box Collider 2D ,在子彈套用Circle Collider 2D,並將大小調整至適當大小。

只有這樣還不能觸發碰撞事件,必須在小飛兵跟敵人身上套用Rigidbody2D(2D剛體),這樣才能觸發碰撞事件,不過我們並不需要重力與物理運算,因此勾選Is Kinematic(運動學)來消除物理運算。

但是,失去物理運算的碰撞體是無法造成碰撞事件的,只能造成觸發(Trigger)事件,因此我們必須將子彈的Is Trigger勾選,讓他成為觸發區。

要發生扣血,首先你必須有血(?),因此我們也替敵人加上Unit,要知道雖然小飛兵跟敵人是玩家跟電腦,但本質上一樣都是會攻擊會掛掉的角色,Unit是可以重複使用的,添加一個碰撞事件並在碰撞到物體時降低HP值,並在HP<0的時候摧毀物件,代表他死亡了。

void OnTriggerEnter2D()
{
    HP -= 10; //生命值-10
    if (HP < 0)
    {
        Destroy(gameObject); //摧毀物件
    }
}

實際測試一遍

非常成功!敵人掛了,但怎麼小飛兵也掛了…

這是因為子彈從小飛兵身上創造出來時,就已經與小飛兵發生觸發事件了,子彈不只打中敵人,也打中了小飛兵,因此必須增加一個判斷擊中的條件。

在這邊,我用「隊伍」當作擊中的條件,如果Unit的「隊伍」跟子彈的「隊伍」不一樣的話才會造成傷害,否則無視子彈。

在Unit中增加一個Team變數,預設為0代表玩家,也在BulletMove中增加一個一樣的Team變數。

將敵人的Unit的Team數值設定為1。

修改Shooting在創造子彈的時候賦予子彈Team值,並在Unit的觸發事件內容增加參數,用來判斷子彈的Team為何。

using UnityEngine;
using System.Collections;

public class Shooting : MonoBehaviour {
    public GameObject Bullet; //子彈物件
    void Start () {
  
    }
  
    void Update () {
        if (Input.GetKeyDown(KeyCode.Z))
        {
            GameObject bullet = (GameObject)Instantiate(Bullet, 
                transform.position, new Quaternion(0, 0, 0, 0));
            //克隆一個Bullet在小飛兵的位置,轉型成GameObject型態將值給予bullet
            bullet.GetComponent<BulletMove>().Team = GetComponent<Unit>().Team;
            //設定bullet的BulletMove中的Team值 為 此物件的Unit的Team
        }
    }
}
using UnityEngine;
using System.Collections;

public class Unit : MonoBehaviour {
    public string UnitName;
    public int HP = 100;
    public float Speed = 3;
    public int Atk = 10;
    public int Team = 0;
    void Start () {
  
    }
  
    void Update () {
  
   }
    void OnTriggerEnter2D(Collider2D col)
    {
        if (col.GetComponent<BulletMove>().Team != Team)//如果Team值不同則受到傷害
        {
            HP -= 10; //生命值-10
            if (HP < 0)
            {
                Destroy(gameObject); //摧毀物件
            }
        }
    }
}

測試一遍,成功讓敵人陣亡而小飛兵沒事了,但是子彈還是持續飛行著,因此我們必須在Unit中的OnTriggerEnter2D中添加一行刪除子彈的程式碼,這行可以寫在HP -= 10;底下,扣完血後就將子彈移除,如果你想讓子彈可以穿越過去的話就不需要這行了。

if (col.GetComponent<BulletMove>().Team != Team)//如果Team值不同則受到傷害
{
    HP -= 10; //生命值-10
    Destroy(col.gameObject);
    if (HP < 0)
    {
        Destroy(gameObject); //摧毀物件
    }
}

實際測試,子彈在射中敵人時就消失了,表示成功。

–敵人會自己移動並發射子彈,小飛兵碰到敵人的子彈會扣血–

為了讓遊戲看起來更生動,我們來讓敵人會自動左右移動,並創造子彈B來攻擊玩家。

開啟AI撰寫移動的程式碼,我們的判斷式如下

  • 隨機產生一個x值
  • 讓敵人移動到x位置
  • 等待3秒後再隨機產生一個x值
  • 不斷重複

首先宣告一個儲存x值的變數,命名為posX,由於不需要被其他元件抓取,就不需要宣告成public了

float posX;

在Update中,開始撰寫移動的程式,我們的邏輯是如果posX值跟物件的x座標相差超過0.1,就開始移動,若posX小於物件的x座標值表示posX位於物件的左方,否則在右方,便往相應的方向移動。

float speed = GetComponent<Unit>().Speed;//取得速度
//如果posX與物件的x座標相減後取絕對值 大於0.1 則開始做移動判斷
if (Mathf.Abs(posX - transform.position.x) > 0.1f)
{
    if (posX < transform.position.x) //左邊
    {
        transform.Translate(Vector2.left * speed * Time.deltaTime);
    }
    else //右邊
    {
        transform.Translate(Vector2.right * speed * Time.deltaTime);
    }
}

不過因為待會要寫很多東西在Update內,為了避免東西太繁雜,我們撰寫一個函數把剛剛所寫的移動給塞進去,變成:

void Update () {
    AIMove();
}
void AIMove()
{
    float speed = GetComponent<Unit>().Speed;//取得速度
    //如果posX與物件的x座標相減後取絕對值 大於0.1 則開始做移動判斷
    if (Mathf.Abs(posX - transform.position.x) > 0.1f)
    {
        if (posX < transform.position.x) //左邊
        {
            transform.Translate(Vector2.left * speed * Time.deltaTime);
        }
        else //右邊
        {
            transform.Translate(Vector2.right * speed * Time.deltaTime);
        }
    }
}

這樣就比較方便我們整理了。

再來,宣告兩個變數LocateTime(定位時間),LocateNeedTime(定位需要時間)

float LocateTime = 0;
float LocateNeedTime = 3;

LocateTime是用來當作計時器使用,當LocateTime = 0 的時候就是要設定posX座標的時間,設定後將LocateTime = LocateNeedTime,開始計時,又到0的時候再一次設定posX。

這次我們用一個叫AITime的函數把東西包起來,並在Update內呼叫AITime。

開始撰寫

if (LocateTime <= 0)  //如果LocateTime <= 0
{
    LocateTime = LocateNeedTime;  //設定LocateTime為LocateNeedTime
    posX = Random.Range(-3, 3); //讓posX為亂數-3~3
}
else //否則LocateTime減去Time.deltaTime達成計時效果
{
    LocateTime -= Time.deltaTime; 
}

測試一次,就會看到敵人左右趴趴走了

 

終於要到最後一步了,讓敵人發射子彈,我們的邏輯是:每隔0.5秒就發射一發子彈。

與剛剛的移動一樣,需要一個每0.5秒的計時功能,因此宣告兩個變數

float ShootingTime = 0.5f;
float ShootingNeedTime = 0.5f;

以及存放子彈B的變數,在此之前要先將子彈B拉成prefab

public GameObject BulletB;

開始撰寫每隔0.5秒發射一發子彈的程式碼,在這邊要注意的是,由於要飛的方向是往下而不是往上,必須把子彈旋轉180度。

為什麼不再寫一個子彈的腳本讓他往下飛呢?因為除了方向不同,所有子彈的功能都是一致的,沒有必要多寫好幾個腳本增加管理上的複雜度。

void AIShooting()
{
    if (ShootingTime <= 0)  //ShootingTime <= 0
    {
        ShootingTime = ShootingNeedTime;  //設定ShootingTime為ShootingNeedTime

        GameObject bullet = (GameObject)Instantiate(BulletB,
            transform.position, new Quaternion(0, 0, 0, 0));
        //克隆一個Bullet在小飛兵的位置,轉型成GameObject型態將值給予bullet
        bullet.GetComponent<BulletMove>().Team = GetComponent<Unit>().Team;
        //設定bullet的BulletMove中的Team值 為 此物件的Unit的Team

        bullet.transform.Rotate(0, 0, 180);//旋轉180度
       
    }
    else //否則ShootingTime減去Time.deltaTime達成計時效果
    {
        ShootingTime -= Time.deltaTime;
    }
}

仔細看會發現,程式碼其實都跟上面其他東西一樣,只是多了一行旋轉而已,這一串我也只是複製貼上兩上改幾個字而已。

將敵人套用AI腳本,執行。

 

就會看到敵人邊趴趴走邊噴子彈了。

最後的AI程式碼如下

using UnityEngine;
using System.Collections;

public class AI : MonoBehaviour
{
    float posX = 0;
    float LocateTime = 0;
    float LocateNeedTime = 3;
    float ShootingTime = 0.5f;
    float ShootingNeedTime = 0.5f;
    public GameObject BulletB;
    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        AITime();
        AIMove();
        AIShooting();
    }
    void AIShooting()
    {
        if (ShootingTime <= 0)  //ShootingTime <= 0
        {
            ShootingTime = ShootingNeedTime;  //設定ShootingTime為ShootingNeedTime

            GameObject bullet = (GameObject)Instantiate(BulletB,
                transform.position, new Quaternion(0, 0, 0, 0));
            //克隆一個Bullet在小飛兵的位置,轉型成GameObject型態將值給予bullet
            bullet.GetComponent<BulletMove>().Team = GetComponent<Unit>().Team;
            //設定bullet的BulletMove中的Team值 為 此物件的Unit的Team

            bullet.transform.Rotate(0, 0, 180);//旋轉180度
           
        }
        else //否則ShootingTime減去Time.deltaTime達成計時效果
        {
            ShootingTime -= Time.deltaTime;
        }
    }
    void AITime()
    {
        if (LocateTime <= 0)  //如果LocateTime <= 0
        {
            LocateTime = LocateNeedTime;  //設定LocateTime為LocateNeedTime
            posX = Random.Range(-3, 3); //讓posX為亂數-3~3
        }
        else //否則LocateTime減去Time.deltaTime達成計時效果
        {
            LocateTime -= Time.deltaTime;
        }
    }
    void AIMove()
    {
        float speed = GetComponent<Unit>().Speed;//取得速度
        //如果posX與物件的x座標相減後取絕對值 大於0.1 則開始做移動判斷
        if (Mathf.Abs(posX - transform.position.x) > 0.1f)
        {
            if (posX < transform.position.x) //左邊
            {
                transform.Translate(Vector2.left * speed * Time.deltaTime);
            }
            else //右邊
            {
                transform.Translate(Vector2.right * speed * Time.deltaTime);
            }
        }
    }
}

恭喜大家完成了這款小遊戲!

也恭喜我終於打完這篇又臭又長的教學文了,這幾天一直在想這篇文的時機適不適合,是不是該先多打幾篇教學文再來總結,也怕有些要用到的東西還沒講到,看來這次小總結是很順利地做完了。

好好玩玩你製作的小遊戲吧!

在接下來的教學文中,將會提到分類用的Tag(標籤)、分層用的Layer(階層)、音效、特效、2D動畫等等。

 

其實Unit中還有一個Atk屬性沒用到,大家可以自行試試讓子彈帶有傷害值,碰到子彈受傷時扣血量不是10而是子彈的傷害值。


在這邊附上小飛兵專案檔的資源包,解壓縮後點擊兩下就能把所有資源匯入進Unity

 

LittleFly

小飛兵遊戲連結(待補)

 

 

[ Unity 2D ] 教學 #11 – Tag(標籤)

 



相關文章

遊戲程式入門 #02 變數宣告與資料型態 何謂變數?變數是在程式語言中的基本概念之一,從字面上解釋就是「可變動的數」,用來作為存放資料的一個載體,存放資料的類型則是依據「資料型態」來定義。   宣告變數的程式碼如下: ...
教學 #01 – 物件建立與Hello World! 上一篇文章中提到安裝與版面配置,這一篇文章中我們來撰寫第一行程式碼 目前我們的畫面應該是長這樣的 Hierarchy中有一個Main Camera 在上一篇文章中我們有提...
教學 #14 – Scene(場景) Scene是個很重要的環節,在中文稱為「場景」 以比較直覺的方式來說明的話,Scene可以當作遊戲中的關卡,每一個關卡都是獨立的Scene 在Unity的工具列中,File下會看到...
都是初始化,Awake、Start的差異 當我們要在 Unity 中處理初始化的部分時,大多都是寫在「Start」的函式中,但若寫在「Awake」也能有差不多的效果。 那 Start 跟 Awake 差在哪邊呢? 首先要先看一下Unit...