編寫一個簡單的鼠標打飛碟(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);
}