與遊戲世界交互——作業與練習

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

遊戲內容要求:
遊戲有 n 個 round,每個 round 都包括10 次 trial;
每個 trial 的飛碟的色彩、大小、發射位置、速度、角度、同時出現的個數都可能不同。它們由該 round 的 ruler 控制;
每個 trial 的飛碟有隨機性,總體難度隨 round 上升;
鼠標點中得分,得分規則按色彩、大小、速度不同計算,規則可自由設定。
遊戲的要求:
使用帶緩存的工廠模式管理不同飛碟的生產與回收,該工廠必須是場景單實例的!具體實現見參考資源 Singleton 模板類
近可能使用前面 MVC 結構實現人機交互與遊戲模型分離

UML類圖:

首先製作一個飛碟的預設,採用圓柱體爲原型,將高度減小:

然後爲了遊戲的美觀,在這裏添加一個天空盒,在Asset Store上下載一個,並加入到main camera中,效果如下:

主要模塊

Singleton

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;
        }
    }
}

場景單例要求場景中至少有一個T類型的Mono子類,FindObjectOfType會 導致漫長的檢索,除非單實例模式。

DiskData

    public float size;  
    public float speed; 
    public Vector3 position;  
    public Vector3 direction;  
    public Color color; 

存儲了飛碟的屬性,包括大小,速度,位置,飛行方向和顏色。

DiskFactory

  public GameObject disk_prefab;                 //disk prefab
    private List<DiskData> used = new List<DiskData>();   //list of using disks
    private List<DiskData> free = new List<DiskData>();   //list of free disks

    public GameObject GetDisk(int round) //get disks
    {
     disk_prefab = null;
        if (free.Count > 0){
            disk_prefab = free[0].gameObject;
            free.Remove(free[0]);

        }else{
            disk_prefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/UFO"), Vector3.zero, Quaternion.identity);
            disk_prefab.AddComponent<DiskData>();
        }

        
        Debug.Log("round" + round);
        // change speed and size according to round
        if (round == 1)
        {
            disk_prefab.GetComponent<DiskData>().size = 3f;
            disk_prefab.GetComponent<DiskData>().speed = 6f;
            disk_prefab.GetComponent<DiskData>().color = Color.blue;
        }
           
        if (round == 2)
        {
            disk_prefab.GetComponent<DiskData>().size = 1.2f;
            disk_prefab.GetComponent<DiskData>().speed = 12f;
            disk_prefab.GetComponent<DiskData>().color = Color.green;
        }
            
        if (round == 3)
        {
            disk_prefab.GetComponent<DiskData>().size = 0.8f;
            disk_prefab.GetComponent<DiskData>().speed = 18f;
            disk_prefab.GetComponent<DiskData>().color = Color.red;

        }


        //direction
        float ran_x = UnityEngine.Random.Range(-1f, 1f) < 0 ? -1 : 1;
        disk_prefab.GetComponent<DiskData>().direction = new Vector3(ran_x, Random.Range(-2f, 2f), 0);
        disk_prefab.GetComponent<DiskData>().position = new Vector3(ran_x, Random.Range(-2f, 2f), UnityEngine.Random.Range(-1f, 1f));
        disk_prefab.SetActive(false);
        disk_prefab.name = disk_prefab.GetInstanceID().ToString();

        used.Add(disk_prefab.GetComponent<DiskData>());
        return disk_prefab;
    }

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

飛碟工廠用於對飛碟進行管理,他的作用有:

  • 通過場景單實例,構建了方便可取獲取DISK的類;
  • 包裝了複雜的Disk生產與回收邏輯,易於使用;
  • 它包含Disk產生規則(控制每個round的難度),可以積 極應對未來遊戲規則的變化,減少維護成本

在飛碟工廠中,used隊列存放正在使用的飛碟,free隊列存放空閒的飛碟,當需要飛碟時,飛碟工廠就會檢索free隊列是否有飛碟,當free隊列爲空時才新建飛碟。根據題目要求,不同的round對應着不同的難度,所以在工廠里根據當前round的數目對飛碟的屬性進行調整,例如第一個round產生藍色飛碟,第二個round產生綠色飛碟,第三個round產生紅色飛碟,它們的速度也依次提升。

Actions

   public Vector3 direction;
    public float speed;

    public void diskMove(Vector3 direction, float speed)
    {
        this.direction = direction;
        this.speed = speed;

    }
    void Update()
    {
        this.gameObject.transform.position += speed * direction * Time.deltaTime;
        if (Input.GetButtonDown("Fire1"))
        {  

            Debug.Log(Input.mousePosition);
            Vector3 mp = Input.mousePosition; //get Screen Position
            Camera camera;
            camera = Camera.main;
            Ray ray = camera.ScreenPointToRay(Input.mousePosition);
            //Return the ray's hits
            RaycastHit[] hits = Physics.RaycastAll(ray);
            foreach (RaycastHit hit in hits)
            {
                print(hit.transform.gameObject.name);
                if (hit.collider.gameObject.tag.Contains("Finish"))
                { //plane tag
                    Debug.Log("hit " + hit.collider.gameObject.name);
                }
                Singleton<DiskFactory>.Instance.FreeDisk(hit.transform.gameObject);
                SSDirector.GetInstance().CurrentSceneController.getRoundController().addScore();
            }
        }
    }

獲取鼠標的點擊事件,檢測鼠標點擊發出的射線,並判斷是否擊中了飛碟,若擊中則對相應事件進行處理,通知場景加分。

FirstController

  public int total_num;  //disk amount
    public float time; // time
    public int round;  //current round
    public int disk_num_per_round; //disk amount per round;
    public Queue<GameObject> diskQueue = new Queue<GameObject>();
    public RoundController roundController;
    // Start is called before the first frame update
    void Awake()
    {
        disk_num_per_round = Random.Range(10, 20);
        SSDirector.GetInstance().CurrentSceneController = this;
        this.gameObject.AddComponent<DiskFactory>();
        this.gameObject.AddComponent<UserGUI>();
        SSDirector.GetInstance().CurrentSceneController.LoadResources();
    }

    void init() //disk queue initial
    {
        for (int i = 0; i < disk_num_per_round; i++) //15 disks per round
            diskQueue.Enqueue(Singleton<DiskFactory>.Instance.GetDisk(round));
    }


    void Update()
    {
        time += Time.deltaTime;
        if (time >= 2.0f - 0.3 * round)
        {
            if (total_num >= disk_num_per_round * 3) //game over
                Reset();

            else if ((total_num % disk_num_per_round) == 0) //a round is end
            {
                round ++; //add round
                //disk_num_per_round = Random.Range(10, 20); //add disk num
                roundController.addRound();
                init();
            }

            if (total_num < disk_num_per_round * 3)
            {
                time = 0; //reset timer
                ThrowDisk();
                total_num++;
            }
        }
    }



    public void ThrowDisk()

    {
        if (diskQueue.Count > 0)

        {
            GameObject disk = diskQueue.Dequeue();
            disk.GetComponent<Renderer>().material.color = disk.GetComponent<DiskData>().color;
            disk.SetActive(true);
            disk.AddComponent<Actions>();
            disk.GetComponent<Actions>().diskMove(disk.GetComponent<DiskData>().direction, disk.GetComponent<DiskData>().speed);
            disk.transform.position = disk.GetComponent<DiskData>().position;
            disk.transform.localScale = disk.GetComponent<DiskData>().size * disk.transform.localScale;
       
        }

    }
    public RoundController getRoundController() { return roundController; }
    void Reset() { this.gameObject.GetComponent<UserGUI>().restart = true; }
    void ISceneController.LoadResources()
    {
        roundController = new RoundController();
        total_num = 0;
        time = 0;
        round = 0;
        diskQueue.Clear();
    }

在Awake函數中對各個變量進行初始化,這裏設置一個飛碟隊列,存放每一個round飛出的飛碟。
在Update函數中處理飛碟狀態,在這裏設置了一個計時器,用來對飛碟飛出的間隔進行計算,時間間隔會隨round數逐漸減小,然後需要對飛碟的狀態進行判斷。這裏存在着三種狀態:遊戲結束、進入下一輪、遊戲進行。需要根據當前的飛碟數來做出判斷。總共有3個round,當遊戲中總的飛碟數加起來超過每一輪的飛碟數*3時判斷遊戲結束;當總飛碟數模每輪飛碟數爲0時進入下一輪,否則進行遊戲,重置計時器,扔出飛碟。

RoundController

  public int round;
    public int score;
    private static RoundController roundController;
    public RoundController() //constructor
    {
        score = 0;
        score = 0;
    }
    public static RoundController getInstance()
    {
        if (roundController == null)
        {
            roundController = new RoundController();
        }
        return roundController;
    }
    public void addRound(){ round++; }
    public void addScore() {  score += round; }
    public int getRound() {  return round; }
    public int getScore() { return score; }

同時負責回合和記分員的工作,round爲1時擊中每個飛碟得1分,爲2時2分,爲3時3分。

UserGUI

  public bool restart;
    GUIStyle style;
    GUIStyle buttonStyle;

    void Start()

    {
        restart = false;
        style = new GUIStyle();
        style.fontSize = 20;
        style.normal.textColor = Color.white;
        buttonStyle = new GUIStyle("button");
        buttonStyle.fontSize = 20;
        buttonStyle.normal.textColor = Color.white;
    }

    private void OnGUI()

    {
        if (restart == true)
        {
            string t = "GAME OVER!";
            GUI.Label(new Rect(Screen.width / 2 - 20, 200, 50, 50), t, style);
            if (GUI.Button(new Rect(Screen.width / 2 - 20, Screen.height / 2 - 200, 100, 50), "Restart", buttonStyle))
            {
                SSDirector.GetInstance().CurrentSceneController.LoadResources();
                restart = false;
            }
        }
        int round = SSDirector.GetInstance().CurrentSceneController.getRoundController().getRound();
        int score = SSDirector.GetInstance().CurrentSceneController.getRoundController().getScore();
        string text = "Round: " + round.ToString();
        GUI.Label(new Rect(10, 10, Screen.width, 50), text, style);
        string text2 = "Your Score:  " + score.ToString();
        GUI.Label(new Rect(Screen.width - 200, 10, 50, 50), text2, style);
    }

用戶界面中設置了當前round和當前分數的提示,當遊戲結束時顯示restart按鈕。

SSDirector以及ISceneController和之前的牧師與魔鬼基本一樣,在這裏就不放出來了。

遊戲截圖

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

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


public class Rotate : MonoBehaviour

{
    public float v;
    public float angle1, angle2;
    void Update()
    {
        Vector3 axis = new Vector3(0, angle1, angle2);
        this.transform.RotateAround(new Vector3.zero, axis, v * Time.deltaTime);
        this.transform.Rotate(Vector3.up * 100 * Time.deltaTime);
    }


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