今天我們做一個簡單的飛碟遊戲
有了上一章點擊地面出現攻擊目標而引出的單例模式,這一次的遊戲可以很好設計出來。
這個遊戲中的主要角色有
飛碟:最基本的要素,就是一個ganmeobject
遊戲場景:作爲遊戲美化
導演:控制全局,場景轉換(本遊戲中還沒有體現它的作用,因爲只有一個場景。。。)
場記:加載資源,給飛碟工廠、記分員、場次控制員下達命令,協調工作
飛碟工廠:用於飛碟的製造和回收,以及發射
飛碟加工廠:要發射的飛碟要根據場記的要求調整顏色、大小
記分員:統計打下來的飛碟,根據難度,給玩家計算相應分數
場次控制員:遊戲分3個Round,玩家選擇以後場次控制員要做相關調控
他們之間的關係可以用下圖表示(不表示導演)
實現單例模式我是通過GetInstance來實現的,代碼幾乎都是
if (instance == null)
instance = new 類名;
return instance;
這樣編程的好處是分工合理,每個人各司其職,比如我們發現飛碟製造有問題,可以直接找飛碟工廠,計分有問題,可以直接找記分員,這樣的思路相比於把所有代碼都混在一個類裏面,要好理解、更清晰得多
下面講一下遊戲規則
遊戲難度分爲三種難度,玩家選擇後遊戲開始,飛碟會隨機從玩家的左邊或右邊飛出,顏色隨機,大小、速度則嚴格按照選擇的遊戲難度進行復雜的概率分佈得到的(好吧就是隨機數把比例改變一下)每一局一共有20個飛碟,難度不同,飛碟飛出的時間間隔不同,難度三會同時多個飛碟一起飛出。玩家需要通過鼠標點擊飛碟把它打下來(一開始想做一個瞄準鏡,還搞了一把槍,但是槍在鼠標移出遊戲界面後就會變亂,選的準星圖片背景不能變透明很難看就放棄了,果斷用光標吧。。。Low就Low了)玩家的槍一共有三發子彈,按空格就是換子彈
遊戲規則弄清楚了,那麼我說一下每個類
ISceneController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController {
void LoadResources();
void Pause();
void Resume();
}
SSDirector
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object {
private static SSDirector _instance;
public ISceneController currentSceneController { get; set; }
public static SSDirector getInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
public int getFPS()
{
return Application.targetFrameRate;
}
public void setFPS(int fps)
{
Application.targetFrameRate = fps;
}
}
RoundController
這個類很好理解,由於遊戲只需要一個Round,所以有一個靜態成員變量Round用於記錄當前是哪個Round。用了OnGUI來給玩家提供難度以及重新開始的選項
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RoundController : MonoBehaviour {
private static RoundController instance;
private static int Round;
public static RoundController GetInstance()
{
if (instance == null)
instance = new RoundController();
return instance;
}
private void Start()
{
Init();
}
public void Init()
{
Round = 0;
}
public static int GetRound()
{
return Round;
}
void OnGUI()
{
GUIStyle style = new GUIStyle();
style.normal.background = null;
style.normal.textColor = Color.red;
style.fontSize = 30;
if (GUI.Button(new Rect(0, 410, 60, 30), "Round 1"))
{
Round = 1;
} else if (GUI.Button(new Rect(0, 440, 60, 30), "Round 2"))
{
Round = 2;
} else if (GUI.Button(new Rect(0, 470, 60, 30), "Round 3"))
{
Round = 3;
} else if (GUI.Button(new Rect(0, 500, 60, 30), "Reset"))
{
Round = 4;
}
if (Round < 4 && Round != 0)
{
GUI.Label(new Rect(0, 0, 60, 30), "Round " + Round.ToString(), style);
} else
{
GUI.Label(new Rect(0, 0, 60, 30), "Select a round to start", style);
}
}
}
ScoreRecorder
有一個靜態成員變量Score用於記錄當前分數,與RoundController一樣,都只需要一個實例,所以用了單例模式GetInstance。Record函數用於統計分數,根據飛碟的大小來統計
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour {
private static ScoreRecorder instance;
private static int Score;
public static ScoreRecorder GetInstance()
{
if (instance == null)
instance = new ScoreRecorder();
return instance;
}
public static int GetScore()
{
return Score;
}
private void Start()
{
Score = 0;
}
public void Reset()
{
Score = 0;
}
public void Record(GameObject disk)
{
if (disk.GetComponent<Transform>().localScale == new Vector3(1, 0.1f, 1))
Score += 3;
else if (disk.GetComponent<Transform>().localScale == new Vector3(2, 0.1f, 2))
Score += 2;
else
Score += 1;
Debug.Log(Score.ToString());
}
void OnGUI()
{
GUIStyle style = new GUIStyle();
style.normal.background = null;
style.normal.textColor = Color.yellow;
style.fontSize = 30;
GUI.Label(new Rect(800, 0, 60, 30), "Score: " + Score.ToString(), style);
}
}
DiskData
是飛碟的加工廠,不同於生產廠,這裏記錄了飛碟的color、size。在我的認知中,List中存的應該是對象本身,所以我覺得DiskData不是作爲記錄飛碟數據的存在(可能是我認知有缺陷)而是實實在在的飛碟本身,所以包含一個遊戲對象disk,可以說,在我的設計中,DiskData纔是真正飛碟的製造商
關鍵方法:
DiskData:從預設中克隆一個飛碟,把它放到地圖中玩家看不到的地方
build:根據設定的顏色、大小對飛碟進行改造,並決定它是放在玩家右邊還是玩家左邊
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour {
private string color;
private int size;
public GameObject disk;
Texture img;
public DiskData()
{
disk = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/disk"), Vector3.zero, Quaternion.identity);
SetColor("red");
SetSize(1);
disk.transform.position = Vector3.zero;
disk.transform.Rotate(new Vector3(-25, 0, 0));
}
public void SetColor(string col)
{
color = col;
}
public void SetSize(int n)
{
size = n;
}
public void build(int Pos = 0)
{
if (color == "red")
{
img = (Texture)Resources.Load("red"); // 這是紋理
} else if (color == "blue")
{
img = (Texture)Resources.Load("blue");
} else if (color == "yellow")
{
img = (Texture)Resources.Load("yellow");
}
disk.GetComponent<Renderer>().material.mainTexture = img;
if (size == 1)
disk.GetComponent<Transform>().localScale = new Vector3(1, 0.1f, 1);
else if (size == 2)
disk.GetComponent<Transform>().localScale = new Vector3(2, 0.1f, 2);
else if (size == 3)
disk.GetComponent<Transform>().localScale = new Vector3(3, 0.1f, 3);
if (Pos == 0)
{
System.Random pos = new System.Random();
int where = pos.Next(0, 2);
if (where == 0)
{
disk.transform.position = new Vector3(-3.5f, 0, 5f);
}
else
{
disk.transform.position = new Vector3(3.5f, 0, 5f);
}
} else
{
if (Pos == 1) // right
disk.transform.position = new Vector3(3.5f, 0, 5f);
else
disk.transform.position = new Vector3(-3.5f, 0, 5f);
}
}
}
DiskFactory
飛碟工廠,場記發送指令,告訴DiskFactory要什麼顏色、什麼大小、什麼速度的飛碟,DiskFactory得到指令後開始生產(交給它的DiskData小弟做完)然後完成發射。同時,當玩家打掉一個飛碟時,場記通知DiskFactory回收飛碟。
關鍵變量:
used:正在被用的集合,表示正在飛的飛碟
free:庫存中的飛碟,即回收和生產後多餘的飛碟,可以拿來使用
Speed :記錄場記要求的速度,添加到Fly組件中
關鍵方法:
GetDisk:生產飛碟的函數,四個參數分別代表顏色、大小、速度、以及位置。執行該函數,工廠首先會判斷是否有庫存,有直接拿來用,沒有的話判斷當前是否已經生產了20個(工廠也要錢運作,不能浪費),已經有20個則告訴場記沒錢了不幹了,沒有則默默生產,並把生產的添加到used中,對其進行相關改造(build),然後給它岸上飛翔的翅膀(Fly組件)
FreeDisk:把飛碟的翅膀拔掉(刪除組件),並對其進行回收,放在玩家看不見的地方等候使用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour {
private static DiskFactory instance;
public static List<DiskData> used = new List<DiskData>();
public static List<DiskData> free = new List<DiskData>();
public static float Speed;
public static DiskFactory GetInstance()
{
if (instance == null)
instance = new DiskFactory();
return instance;
}
public void Init()
{
while (used.Count != 0)
{
FreeDisk(used[0].disk);
}
}
public void GetDisk(string color, int size, float speed, int pos = 0)
{
DiskData t;
if (free.Count ==0)
{
if (used.Count == 20)
{
Debug.LogError("There are too many disks!");
return;
}
t = new DiskData();
} else
{
t = free[0];
free.Remove(t);
}
Speed = speed;
t.SetColor(color);
t.SetSize(size);
t.build(pos);
used.Add(t);
if (t.disk.GetComponent<Fly>() != null)
t.disk.GetComponent<Fly>().life = 1;
else
t.disk.AddComponent<Fly>();
}
public void FreeDisk(GameObject Disk)
{
Disk.GetComponent<Fly>().life = 0;
free.Add(used[0]);
used.RemoveAt(0);
Disk.transform.position = Vector3.zero;
print("free: " + free.Count);
print("used: " + used.Count);
}
}
這裏添加了一個Fly組件,所以我還要說一下Fly組件。
由於遊戲創建和銷燬的成本很高,組件也是一樣,所以我在這個組件中加入了一個變量life,要用的時候設爲1,不用的時候設爲0,這樣子可以節省一大部分的資源
Fly
給飛碟添加飛行動作,包含飛碟的速度以及終點位置。
Mathf函數與隨機函數結合動態地給飛碟一個相對合理的終點,增加遊戲可玩性(好吧,本身這遊戲就很無聊)。飛碟的運動包括朝終點的飛行以及自身的旋轉,當飛碟到達終點以後,被工廠回收
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Fly : DiskFactory{
private Vector3 dir;
float time;
float x;
float y;
public float speed;
public int life;
// Use this for initialization
void Start () {
life = 1;
speed = Speed;
System.Random pos = new System.Random();
float z = 50;
y = pos.Next(7, 15);
if (Mathf.Min(transform.position.x, 0) == transform.position.x)
{
x = pos.Next(2, 10);
} else
{
x = pos.Next(-11, -2);
}
dir = new Vector3(x, y, z);
}
// Update is called once per frame
void Update () {
if (life == 1)
{
time += Time.deltaTime;
if (transform.position == dir)
{
FreeDisk(transform.gameObject);
}
else
{
transform.position = Vector3.MoveTowards(transform.position, dir, Time.deltaTime * 15);
transform.Rotate(Vector3.up * 500 * Time.deltaTime);
}
}
}
}
FirstController
作爲場記,有着協調各部門工作的任務,所以肯定有飛碟工廠、場景控制員、記分員這幾個變量。主要功能是加載資源、接受這三個部門發來的消息並進行處理、同時給他們下達命令。還要時時監察遊戲進行情況,包括當前子彈數、飛碟剩餘數,有一個名叫cam的遊戲對象,是用於放入攝像機從而進行撞擊檢測的,以及一個time記錄時間確定是否需要飛碟。
關鍵方法:
LoadResources:加載我做的遊戲場景(好大好大,畢竟100多M的遊戲)
Init :初始化相關數據,包括場景控制器、工廠、記分員的初始化,以及子彈數、剩餘飛盤數的初始化
GetColor、GetSize、GetSpeed:通過隨機變量,以當前難道爲依據,得到需要生產的飛碟的相關數據
Update:
1.累加當前遊戲進行時間
2.判斷是否需要飛碟,如果需要,獲取飛碟生產的相關參數,通知飛碟工廠生產飛碟
3.判斷是否按了Reset,如果按了則遊戲重置,等候玩家選擇難度重新開始
4.控制子彈的減少(鼠標點擊)以及裝彈(空格)
5.檢測是否擊中飛碟,如果擊中,通知飛碟工廠回收
值得一提的是判斷飛碟是否被點擊,如果單純用if(Physics.Raycast(ray, out hit))是不行的,因爲我在場景中加入了別的元素,當他們被點擊時,由於他們不能被回收,所以會報錯,我發現克隆後的飛碟名字叫“disk(Clone)”,所以可以這樣子
if (Physics.Raycast(ray, out hit))
{
if (!hit.collider.gameObject.tag.Contains("Finish"))
{
if (hit.collider.gameObject.name == "disk(Clone)")
{
MyFactroy.FreeDisk(hit.transform.gameObject);
Recorder.Record(hit.transform.gameObject);
}
}
}
貼代碼
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController
{
public GameObject cam;
private DiskFactory MyFactroy;
private RoundController Round;
private ScoreRecorder Recorder;
private int number;
float time;
private int now = 0;
private int bullet;
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.setFPS(60);
director.currentSceneController = this;
director.currentSceneController.LoadResources();
}
public void LoadResources()
{
GameObject Environment = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Environment"), new Vector3(3.279565f, 0.5247455f, 9.926256f), Quaternion.identity);
Environment.name = "Environment";
}
// Use this for initialization
void Start () {
Init();
}
void Init()
{
Round = RoundController.GetInstance();
MyFactroy = DiskFactory.GetInstance();
Recorder = ScoreRecorder.GetInstance();
Debug.Log("start");
time = 0;
now = 0;
number = 20;
bullet = 3;
}
public string GetColor()
{
System.Random select = new System.Random();
string color = "red";
switch (select.Next(0, 3))
{
case 0:
color = "red";
break;
case 1:
color = "blue";
break;
case 2:
color = "yellow";
break;
}
return color;
}
private void Reset()
{
Init();
MyFactroy.Init();
Recorder.Reset();
}
private void OnGUI()
{
GUIStyle style = new GUIStyle();
style.normal.background = null;
style.normal.textColor = Color.blue;
style.fontSize = 30;
GUI.Label(new Rect(400, 0, 60, 30), "剩餘飛盤數: " + number.ToString(), style);
GUI.Label(new Rect(400, 30, 60, 30), "Bullet: " + bullet.ToString(), style);
if (number == 0)
{
style.normal.textColor = Color.black;
GUI.Label(new Rect(300, 200, 100, 30), "Game over. Your Score is: " + ScoreRecorder.GetScore(), style);
}
}
public int GetSize(int round)
{
int size = 3;
System.Random select = new System.Random();
if (round == 1)
{
switch (select.Next(0, 6))
{
case 0:
case 1:
case 2:
size = 3;
break;
case 3:
case 4:
size = 2;
break;
case 5:
size = 1;
break;
}
} else if (round == 2)
{
switch (select.Next(0, 6))
{
case 0:
case 1:
size = 3;
break;
case 2:
case 3:
case 4:
size = 2;
break;
case 5:
size = 1;
break;
}
} else if (round == 3)
{
switch (select.Next(0, 6))
{
case 0:
size = 3;
break;
case 1:
case 2:
size = 2;
break;
case 3:
case 4:
case 5:
size = 1;
break;
}
}
return size;
}
float GetSpeed(int round)
{
int speed = 15;
System.Random select = new System.Random();
if (round == 1)
{
switch (select.Next(0, 6))
{
case 0:
case 1:
case 2:
speed = 15;
break;
case 3:
case 4:
speed = 20;
break;
case 5:
speed = 25;
break;
}
}
else if (round == 2)
{
switch (select.Next(0, 6))
{
case 0:
case 1:
speed = 15;
break;
case 2:
case 3:
case 4:
speed = 20;
break;
case 5:
speed = 25;
break;
}
}
else if (round == 3)
{
switch (select.Next(0, 6))
{
case 0:
speed = 15;
break;
case 1:
case 2:
speed = 20;
break;
case 3:
case 4:
case 5:
speed = 25;
break;
}
}
return speed;
}
// Update is called once per frame
void Update ()
{
float speed = 15;
if (RoundController.GetRound() == 4)
{
Reset();
}
if (number == 0)
{
return;
}
time += Time.deltaTime;
System.Random select = new System.Random();
string color = GetColor();
int size = 3;
if (now != RoundController.GetRound())
{
Reset();
now = RoundController.GetRound();
}
if (RoundController.GetRound() == 1) // 難度一
{
if (time >= 6)
{
speed = GetSpeed(RoundController.GetRound());
number--;
time = 0;
size = GetSize(RoundController.GetRound());
MyFactroy.GetDisk(color, size, speed);
}
} else if (RoundController.GetRound() == 2)
{
if (time >= 4)
{
speed = GetSpeed(RoundController.GetRound());
number--;
time = 0;
size = GetSize(RoundController.GetRound());
MyFactroy.GetDisk(color, size, speed);
}
} else if (RoundController.GetRound() == 3)
{
if (time >= 3)
{
speed = GetSpeed(RoundController.GetRound());
float speed2 = GetSpeed(RoundController.GetRound());
number -= 2;
time = 0;
int size2 = GetSize(RoundController.GetRound());
string color2 = GetColor();
size = GetSize(RoundController.GetRound());
MyFactroy.GetDisk(color, size, speed, 1);
MyFactroy.GetDisk(color2, size2, speed2, 2);
}
}
if (Input.GetAxis("Jump") > 0)
bullet = 3;
if (Input.GetButtonDown("Fire1") && bullet > 0)
{
bullet--;
Vector3 mp = Input.mousePosition;
Camera ca = cam.GetComponent<Camera>();
Ray ray = ca.ScreenPointToRay(mp);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
if (!hit.collider.gameObject.tag.Contains("Finish"))
{
if (hit.collider.gameObject.name == "disk(Clone)")
{
MyFactroy.FreeDisk(hit.transform.gameObject);
Recorder.Record(hit.transform.gameObject);
}
}
}
}
}
public void Pause()
{
throw new NotImplementedException();
}
public void Resume()
{
throw new NotImplementedException();
}
}
所有類都構造完成了,我們需要把FirstController、RoundController和ScoreRecorder掛載到一個新建的空對象中,然後把Main Camera作爲 FirstController中的對象傳進去就完成了,爲什麼要掛載RoundController和ScoreRecorder?因爲我在裏面設置了GUI,我發現如果不掛載,僅僅作爲FirstController的組成是不會觸發GUI的,暫時還沒找到解決辦法。
然後我們可以根據之前學的場景美化,種點花花草草,挖點坑,造點山,可以做出挺不錯的效果
我沒開玩笑!真的挖了個坑!
最後的效果還是蠻好看的
感謝閱讀!