首先對該游戲框架進行分析:打飛碟這個游戲中只有“飛碟”這一類游戲對象,因此首先需要UFO.cs類用來保存飛碟的gameObject和飛碟的大小、速度、移動方向、顏色等單個飛碟獨有的屬性。然后,分析該飛碟的行為:它可以被點擊、它在視線范圍內時遵循拋物線的飛行軌跡、它飛行到視線范圍外時自動消失。對於第一個行為和第三個行為,我分別定義了Click.cs和MoveOut.cs組件執行該動作。而對於拋物線的飛行軌跡,我通過調用Unity中既成的Rigidbody模塊實現。我們還需要一個飛碟控制器對游戲中所有的飛碟進行管理。根據要求,該控制器被實現為工廠+單例模式,在代碼中我定義為UFOFatory.cs文件。此外,根據上次作業的經驗,我們還需要一個場景控制器SceneController.cs、一個導演類Director.cs和管理GUI類UserGUI.cs。以上七個文件構成了本次作業的全部代碼。
文件的結構如下。其中Resource/Prefabs文件夾中只有一個橢圓形預制體代表飛碟。
導演需要作為游戲的系統對象,並且實現為單例模式。單例的作用是保證其余的所有控制器都在同一個導演的管理之下。導演記錄了當前活躍的場景管理器(雖然本游戲中只有一種場景管理器)
public class Director : System.Object { static Director _instance; public SceneController CurrentSceneController {get; set;} public static Director GetInstance() { if (_instance == null) { _instance = new Director(); } return _instance; } }
場景管理器對該場景內的全局變量和動作做管理,它控制全局游戲的開始和結束,同時管理一輪游戲開始時下一輪游戲會自動啟動。另外當一個飛碟被點擊、飛出視線外時,會使游戲的全局變量(得分、血量、輪數)發生變化,場景管理器也要對這個功能進行維護。另外在實現時,我創建了“飛碟工廠”這個游戲對象,把UFOFatory.cs掛到該游戲對象上,保證了飛碟工廠的單實例。
public class SceneController : MonoBehaviour { public int round; public int score; public int blood; public bool isStart; public bool isFailed; public int currentDifficulty; GameObject fatory; public void Awake(){ Director director = Director.GetInstance(); director.CurrentSceneController = this; director.CurrentSceneController.Initialize(); fatory = new GameObject("UFO fatory"); fatory.AddComponent<UFOFatory>(); fatory.GetComponent<UFOFatory>().SetDepend(this); } //對場景參數進行初始化 public void Initialize(){ round = 1; score = 0; blood = 10; currentDifficulty = 0; isStart = false; isFailed = false; } //點擊飛碟時觸發加分 public void AddScore(){ score += 1; } //試去一個飛碟時觸發減分 public void SubBlood(){ if(blood > 0){ blood -= 1; } if(blood == 0){ isFailed = true; fatory.GetComponent<UFOFatory>().InitializeUFO(); isStart = false; } } //開始一次新游戲 public void StartNewGame(){ Initialize(); isStart = true; StartRound(); } //開始一輪拋擲飛碟(10個) public void StartRound(){ fatory.GetComponent<UFOFatory>().SetDifficulty(currentDifficulty); fatory.GetComponent<UFOFatory>().InitializeUFO(); fatory.GetComponent<UFOFatory>().StartRound(); } //當一輪游戲結束,通知開始下一輪游戲 public void RoundDone(){ round++; currentDifficulty++; StartRound(); } }
飛碟工廠是本次作業中最重要的部分。飛碟工廠控制飛碟的行為,在一輪拋飛碟的動作中,負責管理所有飛碟的初始化、逐個拋出和回收,以及通過難度決定飛碟的大小和拋出飛碟的間隔。在游戲過程中,飛碟對象被一直緩存在飛碟控制器中。老師建議的做法和以前師兄們留下的博客不同,我在這里使用了協程解決了“定時間隔拋出飛碟”這一動作。使用協程后代碼更加簡單容易維護。唯一要注意的是,當一輪游戲結束或者生命值耗盡時,要主動中斷協程。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; public class UFOFatory : MonoBehaviour { public int difficulty; UFO[] UFOs = new UFO[trial]; public SceneController myController; public static int trial = 10; Coroutine mCoroutine = null; // 當前的協程 public static Color[] colors = new Color[5]{Color.red, Color.black, Color.blue, Color.green, Color.yellow}; public static float[] scales = new float[5]{0.6f, 0.8f, 1.0f, 1.2f, 1.4f}; public static float[] y_position = new float[5]{1f,2f,3f,4f,5f}; public static float[] x_position = new float[2]{-13f, 13f}; public static float[] x_force = new float[8]{10f,12.5f,15f,20f,-10f,-12.5f,-15f,-20f}; //設定該飛碟工廠上游的場景控制器 public void SetDepend(SceneController firstCtrl){ this.myController = firstCtrl; } //檢查一輪游戲是否結束 public bool CheckRoundFinish(){ for(int i = 0; i < trial; i++){ if(!UFOs[i].isOver){ return false; } } return true; } //當點擊UFO時執行 public void ClickUFO(int id){ UFOs[id].SetUFOActive(false); myController.AddScore(); if(CheckRoundFinish()){ if(mCoroutine != null){ StopCoroutine(mCoroutine); mCoroutine = null; } myController.RoundDone(); } } //當UFO未被點擊並離開視線時執行 public void FailUFO(int id){ UFOs[id].SetUFOActive(false); myController.SubBlood(); if(CheckRoundFinish()){ if(mCoroutine != null){ StopCoroutine(mCoroutine); mCoroutine = null; } myController.RoundDone(); } } //開始一輪游戲 public void StartRound(){ mCoroutine = StartCoroutine(SetUFOStart(2f-difficulty*0.2f)); } //讓飛碟移動 IEnumerator SetUFOStart(float gapTime){ for(int i = 0; i < trial; i++){ int side = Random.Range(0,2); UFOs[i].SetUFOActive(true); UFOs[i].Fly(colors[Random.Range(0,5)], scales[Random.Range(0,5)]*(2f-difficulty*0.2f), new Vector3(x_position[side],y_position[Random.Range(0,5)],0), new Vector3(x_force[Random.Range(0,4)+side*4],0,0)); yield return new WaitForSeconds(gapTime); } } //設置難度 public void SetDifficulty(int difficulty){ this.difficulty = difficulty; } //在每輪游戲開始前初始化所有飛碟 public void InitializeUFO(){ if(mCoroutine != null){ StopCoroutine(mCoroutine); mCoroutine = null; } for(int i = 0; i < trial; i++){ if(UFOs[i] != null){ Destroy(UFOs[i].ufoObject); } UFOs[i] = new UFO(this, i); UFOs[i].SetUFOActive(false); } } }
在初始化UFO類時,先把Click和MoveOut兩個腳本掛上去,通過Click()和Fail()函數接收來自這兩個腳本的反饋信息。通過設置該游戲對象是否可見從而控制飛碟的生成和消失。只有當飛碟工廠命令該飛碟開始運動時才會把rigidbody組件附加到游戲對象上並且添加一個額外的力使得飛碟作法拋物線運動。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class UFO { int id; public bool isOver; public GameObject ufoObject; private UFOFatory myFatory; public UFO(UFOFatory fatory, int id){ this.myFatory = fatory; this.id = id; this.isOver = false; ufoObject = GameObject.Instantiate(Resources.Load("Prefabs/UFO", typeof(GameObject))) as GameObject; ufoObject.AddComponent<Click>(); ufoObject.GetComponent<Click>().SetDepend(this); ufoObject.AddComponent<MoveOut>(); ufoObject.GetComponent<MoveOut>().SetDepend(this); } //設置飛碟對象是否可見 public void SetUFOActive(bool boolean){ ufoObject.SetActive(boolean); } //處理飛碟點擊事件 public void Click(){ this.isOver = true; myFatory.ClickUFO(this.id); } //該飛碟已經飛出屏幕時觸發 public void Fail(){ this.isOver = true; myFatory.FailUFO(this.id); } //飛碟開始移動 public void Fly(Color c, float scale, Vector3 pos, Vector3 force){ ufoObject.SetActive(true); ufoObject.GetComponent<Renderer>().material.color = c; ufoObject.transform.localScale = ufoObject.transform.localScale * scale; ufoObject.transform.position = pos; if(!ufoObject.GetComponent<Rigidbody>()){ ufoObject.AddComponent<Rigidbody>(); } ufoObject.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse); } }
Click腳本中設置監聽鼠標事件並且制定了解決該鼠標事件的函數。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Click : MonoBehaviour { UFO ufo; //設置上層對象 public void SetDepend(UFO ufo) { this.ufo = ufo; } //設置點擊事件 void OnMouseDown() { this.ufo.Click(); } }
MoveOut腳本中每一幀檢查飛碟的位置並且判讀是否已經超出視線范圍,如果已經超過則調用飛碟類中的Fail函數解決該事件。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MoveOut : MonoBehaviour { UFO ufo; bool onScreen = true; Vector3 position; //設置上層對象 public void SetDepend(UFO ufo){ this.ufo = ufo; } //判斷飛碟是否已經飛出屏幕外 void Update(){ if(!onScreen) return; this.position = ufo.ufoObject.transform.position; if(this.position.x < -15 || this.position.x > 15 || this.position.y < -8 || this.position.y > 8){ onScreen = false; ufo.Fail(); } } }
UserGUI負責繪制標題、按鈕、顯示得分和血量等。在游戲開始后,按鈕變灰且不可以再被按下,只有當游戲結束時該按鈕才重新可用。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class UserGUI : MonoBehaviour { SceneController CurrentSceneController; GUIStyle msgStyle, titleStyle; string roundText, bloodText, scoreText; void Start(){ CurrentSceneController = Director.GetInstance().CurrentSceneController; msgStyle = new GUIStyle(); msgStyle.normal.textColor = Color.black; msgStyle.alignment = TextAnchor.MiddleCenter; msgStyle.fontSize = 30; titleStyle = new GUIStyle(); titleStyle.normal.textColor = Color.black; titleStyle.alignment = TextAnchor.MiddleCenter; titleStyle.fontSize = 60; } void OnGUI(){ roundText = $"Round: {CurrentSceneController.round}"; bloodText = $"Blood: {CurrentSceneController.blood}"; scoreText = $"Score: {CurrentSceneController.score}"; GUI.enabled = !CurrentSceneController.isStart; // 重新開始的按鈕 if(GUI.Button(new Rect(Screen.width*0.75f, Screen.height*0.85f, Screen.width*0.2f, Screen.height*0.1f), "Start")){ CurrentSceneController.StartNewGame(); } // 標題與其他提示信息 GUI.Label(new Rect(0, 0, Screen.width, Screen.height*0.2f), "Hit UFO", titleStyle); GUI.Label(new Rect(0, Screen.height*0.2f, Screen.width*0.15f, Screen.height*0.1f), roundText, msgStyle); GUI.Label(new Rect(0, Screen.height*0.3f, Screen.width*0.15f, Screen.height*0.1f), bloodText, msgStyle); GUI.Label(new Rect(0, Screen.height*0.4f, Screen.width*0.15f, Screen.height*0.1f), scoreText, msgStyle); //判斷是否結束游戲 if(CurrentSceneController.isFailed){ GUI.Label(new Rect(0, Screen.height*0.4f, Screen.width, Screen.height*0.2f), "Game Over.", titleStyle); } } }
代碼:https://gitee.com/GallonC/unityhomework/tree/master/homework4
演示:https://www.bilibili.com/video/BV1Tr4y117FW?spm_id_from=333.999.0.0
運行方式:新建一個unity3D項目,命名為 hit_UFO,將該新項目中的Assert文件替換為以上代碼提供的Assert文件。在Unity軟件中創建一個空游戲對象,將SceneController.cs掛到該空游戲對象上,將UserGUI掛到camera上。點擊運行即可。