編寫一個簡單的鼠標打飛碟(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中,也可以用它來完成爆炸效果。