【Unity 3D】學習筆記(四)

 

編寫一個簡單的鼠標打飛碟(Hit UFO)遊戲


  • 遊戲內容要求:

    • 遊戲有 n 個 round,每個 round 都包括10 次 trial;
    • 每個 trial 的飛碟的色彩、大小、發射位置、速度、角度、同時出現的個數都可能不同。它們由該 round 的 ruler 控制;
    • 每個 trial 的飛碟有隨機性,總體難度隨 round 上升;
    • 鼠標點中得分,得分規則按色彩、大小、速度不同計算,規則可自由設定。
  • 遊戲的要求:

    • 使用帶緩存的工廠模式管理不同飛碟的生產與回收,該工廠必須是場景單實例的!具體實現見參考資源 Singleton 模板類
    • 近可能使用前面 MVC 結構實現人機交互與遊戲模型分離

 

參考上次動作分離版魔鬼與牧師的MVC結構對動作進行管理,保留SSDirector,SSAction和SSActionManager等類,重複的代碼略過不表。

 

遊戲規則

玩家點擊飛出的飛碟即可得分,而讓飛碟飛出畫面會降低血量。隨着分數積累可以到達不同關卡,級別越高的關卡難度越大。玩家的初始血量爲10,血量降爲0時遊戲結束。

 

Singleton

本次作業的要求包括飛碟工廠場景單實例,具體實現需要定義Singleton模板類。運用模板,可以爲每個MonoBehaviour子類創建一個對象的實例。代碼如下所示:

public class Singleton<T> : MonoBehaviour where T: MonoBehaviour {
    protected static T instance;

    public static T Instance {
        get {
            if(instance == null) {
                instance = (T)FindObjectOfType(typeof(T));
                if(instance == null)
                    Debug.LogError("An instance of " + typeof(T) + " is needed in the scene, but there is none.");
            }
            return instance;
        }
    }
}

由此,場景單實例的使用就很簡單了,只需要將MonoBehaviour子類對象掛載在任何一個遊戲對象上即可。之後,在任意位置使用代碼Singleton<YourMonoType>.Instance獲得該對象。

 

UserGUI

此類用來實現遊戲的界面,根據遊戲規則,擊中不同種類的飛碟會有不同的得分,所以需要顯示總分數。

而且,需要顯示出關卡等級,並設計一個簡易的血條來展示剩餘血量更能夠增加遊戲性。

其中的關鍵性代碼如下:

        if (isStart) {
            if (Input.GetButtonDown("Fire1")) act.hit(Input.mousePosition);
            GUI.Label(new Rect(10, 5, 200, 50), "SCORE", textStyle);
            GUI.Label(new Rect(10, 50, 200, 50), "LEVEL", textStyle);
            GUI.Label(new Rect(Screen.width - 380, 5, 50, 50), "BLOOD", textStyle);
            GUI.Label(new Rect(200, 5, 200, 50), act.getScore().ToString(), scoreStyle);
            GUI.Label(new Rect(200, 50, 200, 50), act.getLevel().ToString(), scoreStyle);
            for (int i = 0; i < blood; i++)
                GUI.Label(new Rect(Screen.width - 220 + 20 * i, 5, 50, 50), "#", bStyle);
            if (blood == 0) {
                GUI.Label(new Rect(Screen.width / 2 - 130, Screen.height / 2 - 120, 100, 100), "Game Over", style);
                if (GUI.Button(new Rect(Screen.width / 2 - 40, Screen.height / 2 - 30, 100, 50), "REPLAY")) {
                    blood = 10;
                    act.restart();
                    return;
                }
                act.gameOver();
            }
        }
        else {
            GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 120, 100, 100), "Hit UFO", style);
            if (GUI.Button(new Rect(Screen.width / 2 - 40, Screen.height / 2 - 30, 100, 50), "START")) {
                isStart = true;
                act.begin();
            }
        }

isStart用來判斷遊戲是否開始,用action來進行遊戲進度的調節,包括遊戲開始、重新開始、結束、設計動作等。

得到的得分欄和血條效果如下:

 

IUserAction

public interface IUserAction {
    void restart();
    void hit(Vector3 pos);
    void gameOver();
    int getScore();
    int getLevel();
    void begin();
}

IUserAction用來調整遊戲的進度,協同計分器類與玩家的操作進行交互,爲每一次成功的射擊加上相應的分數,並在血量爲空時重新開始遊戲。具體的實現在FirstController中,代碼如下:

    public void hit(Vector3 pos) {
        bool isHit = false;
        RaycastHit[] hits;
        Ray ray = Camera.main.ScreenPointToRay(pos);
        hits = Physics.RaycastAll(ray);
        for (int i = 0; i < hits.Length; i++) {
            RaycastHit temp = hits[i];
            if (temp.collider.gameObject.GetComponent<DiskData>() != null) {
                for (int j = 0; j < notHit.Count; j++)
                    if (temp.collider.gameObject.GetInstanceID() == notHit[j].gameObject.GetInstanceID())
                        isHit = true;
                if (!isHit) return;
                notHit.Remove(temp.collider.gameObject);
                record.Record(temp.collider.gameObject);
                temp.collider.gameObject.transform.GetChild(0).GetComponent<ParticleSystem>().Play();
                StartCoroutine(WaitingParticle(0.08f, temp, factory, temp.collider.gameObject));
                break;
            }
        }
    }

    public int getScore() {
        return record.score;
    }
    
    public int getLevel() {
        return level;
    }

    public void restart() {
        record.score = 0;
        level = 1;
        speed = 2f;
        isOver = false;
        isPlay = false;
    }

    public void gameOver() {
        isOver = true;
    }

    public void begin() {
        isStart = true;
    }

 

DiskFactory

飛碟工廠用來製造發送飛碟。

        switch (level) {
            case 1: num = Random.Range(0, s1); break;
            case 2: num = Random.Range(0, s2); break;
            case 3: num = Random.Range(0, s3); break;
        }

首先根據不同的級別生成隨機數。在更高的關卡,可以生成低級關卡的飛碟,所以隨機數的區間從0開始。

        if (num <= s1) type = "disk1";
        else if (num <= s2 && num > s1) type = "disk2";
        else type = "disk3";

然後根據不同的隨機數對應生成飛碟的類型。

        if (disk == null) {
            if (type == "disk1") {
                disk = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, Y, 0), Quaternion.identity);
                disk.GetComponent<DiskData>().score = 10;
            }
            else if (type == "disk2") {
                disk = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, Y, 0), Quaternion.identity);
                disk.GetComponent<DiskData>().score = 20;
            }
            else if (type == "disk3") {
                disk = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, Y, 0), Quaternion.identity);
                disk.GetComponent<DiskData>().score = 30;
            }

然後根據飛碟的類型實例化,並且對不同類型的飛碟賦予不同的分數。

    public void freeDisk(GameObject disk) {
        for (int i = 0; i < close.Count; i++)
            if (disk.GetInstanceID() == close[i].gameObject.GetInstanceID()) {
                close[i].gameObject.SetActive(false);
                open.Add(close[i]);
                close.Remove(close[i]);
                break;
            }
    }

最後需要回收飛碟,因爲飛出遊戲畫面的飛碟不再被需要。

 

FirstController

此類用來控制整個遊戲的狀態。

    private int level = 1;
    private float speed = 2f;
    private bool isPlay = false, isOver = false, isStart = false;

以上是遊戲中用到的表示狀態的變量。isPlay用來表示遊戲中的狀態,isOver用來表示遊戲結束的狀態,isStart則是遊戲開始的狀態。

    void Update () {
        if(isStart) {
            if (isOver) CancelInvoke("LoadResources");
            if (!isPlay) {
                InvokeRepeating("LoadResources", 1f, speed);
                isPlay = true;
            }
            createDisk();
            if (level == 1 && record.score >= 30) {
                level++;
                speed = speed - 0.6f;
                CancelInvoke("LoadResources");
                isPlay = false;
            }
            else if (level == 2 && record.score >= 100) {
                level++;
                speed = speed - 0.5f;
                CancelInvoke("LoadResources");
                isPlay = false;
            }
        }
    }

Update函數如上,當獲取的分數大於30時,就進入關卡2;分數大於100時就進入關卡3.

CancelInvoke定義如下:

public void CancelInvoke();

Description

Cancels all Invoke calls on this MonoBehaviour.

public void CancelInvoke(string methodName);

Description

Cancels all Invoke calls with name methodName on this behaviour.

        for (int i = 0; i < notHit.Count; i++)
            if (notHit[i].transform.position.y < -10 && notHit[i].gameObject.activeSelf == true) {
                factory.freeDisk(notHit[i]);
                notHit.Remove(notHit[i]);
                GUI.bloodReduce();
            }

當飛碟飛出畫面時,就及時銷燬並按照遊戲規則減掉血量。

    public void bloodReduce() {
        if (blood > 0) blood--;
    }

當調用bloodReduce函數,就對血量blood執行減一即可。

 

ScoreRecorder

public class ScoreRecorder : MonoBehaviour {
    public int score;

    void Start () {
        score = 0;
    }

    public void Record(GameObject disk) {
        score += disk.GetComponent<DiskData>().score;
    }

    public void Reset() {
        score = 0;
    }
}

記分器類的邏輯比較簡單。初始狀態分數變量score爲0,此後每次擊中飛碟則累加上此飛碟對應的分數,重新開始遊戲則重置score爲0。

 

遊戲實現

遊戲視頻戳這裏

 

編寫一個簡單的自定義 Component (選做


  • 用自定義組件定義幾種飛碟,做成預製

    • 參考官方腳本手冊 https://docs.unity3d.com/ScriptReference/Editor.html
    • 實現自定義組件,編輯並賦予飛碟一些屬性

創造三個關卡中對應的飛碟類型如上,做成預製。

創建DiskData類,存儲飛碟的一些基本屬性。

public class DiskData : MonoBehaviour {
    public int score;
    public Vector3 direction;
    public Vector3 scale = new Vector3(1, 1, 1);
}

將DiskData.cs掛載在飛碟的預製上,結果如下:

在此可以編輯修改飛碟的一些屬性。

        if (disk == null) {
            if (type == "disk1") {
                disk = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, Y, 0), Quaternion.identity);
                disk.GetComponent<DiskData>().score = 10;
            }
            else if (type == "disk2") {
                disk = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, Y, 0), Quaternion.identity);
                disk.GetComponent<DiskData>().score = 20;
            }
            else if (type == "disk3") {
                disk = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, Y, 0), Quaternion.identity);
                disk.GetComponent<DiskData>().score = 30;
            }
            float X = Random.Range(-1f, -1f) < 0 ? -1 : 1;
            disk.GetComponent<DiskData>().direction = new Vector3(X, Y, 0);
            disk.transform.localScale = disk.GetComponent<DiskData>().scale;
        }

在飛碟工廠裏,每當實例化一個飛碟預製,即通過GetComponent來根據飛碟的類型修改相關屬性。

GetComponent定義如下:

public T GetComponent();

Description

GetComponent is the primary way of accessing other components. From javascript the type of a script is always the name of the script as seen in the project view. You can access both builtin components or scripts with this function.

通過GetComponent,可以即時地編輯組件的屬性。

           temp.collider.gameObject.transform.GetChild(0).GetComponent<ParticleSystem>().Play();

在FirstController中,也可以用它來完成爆炸效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章