任務(wù)概述
這次我們重新制作一個打飛碟小游戲女蜈。游戲每一輪生成10個飛碟描沟,每個飛碟隨機飛行飒泻,玩家要在這一輪結(jié)束之前盡快地射擊飛碟,擊中了就加分吏廉,分數(shù)達到一定的程度就提升難度泞遗。這個游戲很基本,也很簡單席覆,我們通過它來學習玩家輸入史辙、使用射線、使用工廠來獲取和回收對象佩伤,并且體會代碼復(fù)用的技巧聊倔。
游戲截圖
下載我的項目在本地查看!
從我的github下載項目資源生巡,將Assets文件夾覆蓋你的項目中的Assets文件夾耙蔑,然后在U3D中雙擊“hw5”,就可以運行了孤荣!
學會使用他人的資源
這個游戲有一些資源是從外部導入的甸陌,比如說RigidBodyFPSController(第一人稱控制器,可以像CS一樣控制主角)來自標準資源庫的Characters包(在這篇文章中我教大家導入了標準資源的Environments包)盐股。
槍支的預(yù)制和爆炸的預(yù)制钱豁,是從Asset Store中免費下載的資源,下載好之后會彈出選擇框疯汁,讓你從下載的資源包中選擇自己需要的資源牲尺。適當?shù)厥褂盟说馁Y源能夠讓你專注于自己的游戲內(nèi)容。
玩家輸入幌蚊、使用射線
在Update中使用Input.GetButton(string buttonName)谤碳,在某一幀如果這個按鍵出于按下狀態(tài),就返回true溢豆,否則返回false蜒简。通過這個方式來監(jiān)測用戶的輸入并做出反應(yīng)。
使用GetButton可以得到“掃射”的效果沫换,也就是說如果你按著這個鍵不放臭蚁,那么就一直返回true。Input.GetButtonDown則不一樣讯赏,只有你“按下”的那一幀會返回true垮兑,只能得到“點射”的效果。
Input還可以監(jiān)測鍵盤按鍵漱挎、鼠標移動等系枪,其他的使用方式可以查找官方文檔或搜索其他博客,這里我們專注于這個小游戲磕谅。
射線:
Ray ray = cam.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
// do something
}
通過cam.ScreenPointToRay(Input.mousePosition)
我們得到了一條射線私爷,從攝像機攝像鼠標點擊的方向雾棺。Physics.Raycast(ray, out hit)
將這條射線發(fā)射出去,如果射線擊中了物體則返回true衬浑,并將射線擊中的信息保存在參數(shù)hit
中捌浩,你可以從中獲得擊中的物體、擊中的位置等信息工秩。
out是一個關(guān)鍵字尸饺,類似于傳遞引用、只不過函數(shù)會將out傳進去的參數(shù)清空助币,再放入數(shù)據(jù)浪听。也就是說如果使用ref關(guān)鍵字,信息有進有出眉菱;使用out迹栓,信息只出不進。
Shooter
在我們的游戲中俭缓,Shooter就是用來監(jiān)測鼠標點擊并發(fā)射射線的克伊,掛載在槍支對象上,射線擊中UFO或地面就通知sceneController尔崔。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.MyGameFramework;
public class Shooter : MonoBehaviour
{
public Camera cam;
private FirstController sceneController;
LayerMask layerMask; // 指定一些layer層答毫,下面我們讓射線只能擊中這些layer中的物體
public GameObject muzzleFlash; // 槍口火焰的預(yù)制褥民,我已經(jīng)將預(yù)制拖動到了Inspector中
bool muzzleFlashEnable = false; // 是否顯示槍口火焰
float muzzleFlashTimer = 0; // 記錄槍口火焰已經(jīng)顯示了多久
const float muzzleFlashMaxTime = 0.1F; // 槍口火焰每次顯示0.1秒
void Awake()
{
muzzleFlash.SetActive(false);
layerMask = LayerMask.GetMask("Shootable", "RayFinish");
// 指定這兩個層季春,Shootable中是飛碟,RayFinish中的是地面Terrain
}
void Start()
{
cam = Camera.main;
sceneController = Director.getInstance().currentSceneController as FirstController;
}
void Update()
{
if (Input.GetButton("Fire1")) // Fire1按鍵是鼠標左鍵或左Ctrl鍵
{
Ray ray = cam.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
// layerMask參數(shù)使這個射線只能打中指定layer的物體
if (Physics.Raycast(ray, out hit, Mathf.Infinity, layerMask))
{
if (hit.transform.gameObject.layer == 8)
{ // 通過hit獲取到了擊中物體所在的層
UFOController UFOCtrl = hit.transform.GetComponent<UFOScript>().ctrl;
sceneController.UFOIsShot(UFOCtrl); // 通知sceneController
}
else if (hit.transform.gameObject.layer == 9)
{
sceneController.GroundIsShot(hit.point); // 通知sceneController
}
}
}
if (muzzleFlashEnable == false) // 顯示槍口火焰
{
muzzleFlashEnable = true;
muzzleFlash.SetActive(true);
}
if (muzzleFlashEnable) // 計時消返,槍口火焰顯示0.1秒后消失
{
muzzleFlashTimer += Time.deltaTime;
if (muzzleFlashTimer >= muzzleFlashMaxTime)
{
muzzleFlashEnable = false;
muzzleFlash.SetActive(false);
muzzleFlashTimer = 0;
}
}
}
}
關(guān)鍵的代碼我都已經(jīng)注釋說明载弄。物體被擊中以后的事情交給sceneController來安排,Shooter只專注于“射擊”的功能撵颊。
使用工廠來獲取宇攻、回收對象
GameObject.Instantiate是一個非常消耗系統(tǒng)資源的函數(shù)。如果每一次我們需要飛碟的時候倡勇,我們都使用GameObject.Instantiate逞刷,游戲的性能會很差。所以我們現(xiàn)在使用一個工廠來回收利用使用完畢的UFO妻熊。UFO銷毀的時候夸浅,我們不調(diào)用系統(tǒng)的destroy函數(shù),而是僅僅setactive(false)扔役,下次需要UFO的時候讓它出現(xiàn)在應(yīng)該的位置就可以了帆喇。這樣做減少了Instantiate和Destroy的調(diào)用。
public class UFOFactory : MonoBehaviour
{
Queue<UFOController> freeQueue; // 儲存空閑狀態(tài)的UFO
List<UFOController> usingList; // 儲存正在使用的UFO
GameObject originalUFO; // UFO的原型亿胸,以后創(chuàng)建UFO就克隆這個對象
int count = 0;
void Awake()
{
freeQueue = new Queue<UFOController>();
usingList = new List<UFOController>();
originalUFO = Instantiate(Resources.Load("ufo", typeof(GameObject))) as GameObject;
originalUFO.SetActive(false);
}
public UFOController produceUFO(UFOAttributes attr)
{
UFOController newUFO;
if (freeQueue.Count == 0) // 如果沒有UFO空閑坯钦,則克隆一個對象
{
GameObject newObj = GameObject.Instantiate(originalUFO);
newUFO = new UFOController(newObj);
newObj.transform.position += Vector3.forward * Random.value * 5;
count++;
}
else // 如果有UFO空閑预皇,則取出這個UFO
{
newUFO = freeQueue.Dequeue();
}
newUFO.setAttr(attr); // 將UFO的顏色速度大小設(shè)置成參數(shù)指定的樣子
usingList.Add(newUFO); // 將UFO加入使用中的隊列
newUFO.appear();
return newUFO;
}
public UFOController[] produceUFOs(UFOAttributes attr, int n)
{
// 一次性產(chǎn)生n個UFO
UFOController[] arr = new UFOController[n];
for (int i = 0; i < n; i++)
{
arr[i] = produceUFO(attr);
}
return arr;
}
public void recycle(UFOController UFOCtrl)
{
// 回收一個UFO,將其加入空閑隊列
UFOCtrl.disappear();
usingList.Remove(UFOCtrl);
freeQueue.Enqueue(UFOCtrl);
}
public void recycleAll()
{
while(usingList.Count != 0)
{
recycle(usingList[0]);
}
}
public List<UFOController> getUsingList()
{
return usingList;
}
}
除了UFOFactory以外婉刀,還有一個ExplosionFactory吟温,作用一樣,用來獲取和回收“爆炸對象”突颊,因為爆炸也像飛碟一樣溯街,頻繁產(chǎn)生、消失的洋丐。ExplosionFactory的實現(xiàn)很相似呈昔,代碼我就不放在這里了,要查看的話下載我的項目就可以了友绝。
使用場景控制器協(xié)調(diào)各個場景組件
FirstController是場景中最高級別的控制器堤尾,所有的部件相互之間不會直接通信,只能與FirstController直接通信迁客,這樣可以大大降低各個組件之間的耦合郭宝,當我們更改某個部件時,最多只需要修改一下FirstController中的代碼就可以了掷漱。
public class FirstController : MonoBehaviour, SceneController
{
Director director;
UFOFactory UFOfactory;
ExplosionFactory explosionFactory;
FirstSceneActionManager actionManager;
Scorer scorer;
DifficultyManager difficultyManager;
float timeAfterRoundStart = 10;
bool roundHasStarted = false;
void Awake()
{
// 掛載各種控制組件
director = Director.getInstance();
director.currentSceneController = this;
actionManager = gameObject.AddComponent<FirstSceneActionManager>();
UFOfactory = gameObject.AddComponent<UFOFactory>();
explosionFactory = gameObject.AddComponent<ExplosionFactory>();
scorer = Scorer.getInstance();
difficultyManager = DifficultyManager.getInstance();
loadResources();
}
public void loadResources()
{
// 初始化場景中的物體
new FirstCharacterController();
Instantiate(Resources.Load("Terrain"));
}
public void Start()
{
roundStart();
}
void Update()
{
if (roundHasStarted) {
timeAfterRoundStart += Time.deltaTime;
}
if (roundHasStarted && checkAllUFOIsShot()) // 檢查是否所有UFO都已經(jīng)被擊落
{
print("All UFO is shot down! Next round in 3 sec");
roundHasStarted = false;
Invoke("roundStart", 3);
difficultyManager.setDifficultyByScore(scorer.getScore());
}
else if (roundHasStarted && checkTimeOut()) // 檢查這一輪是否已經(jīng)超時
{
print("Time out! Next round in 3 sec");
roundHasStarted = false;
foreach (UFOController ufo in UFOfactory.getUsingList())
{
actionManager.removeActionOf(ufo.getObj());
}
UFOfactory.recycleAll();
Invoke("roundStart", 3);
difficultyManager.setDifficultyByScore(scorer.getScore());
}
}
void roundStart()
{
// 開始新的一輪
roundHasStarted = true;
timeAfterRoundStart = 0;
UFOController[] ufoCtrlArr = UFOfactory.produceUFOs(difficultyManager.getUFOAttributes(), difficultyManager.UFONumber);
for (int i = 0; i < ufoCtrlArr.Length; i++)
{
ufoCtrlArr[i].appear();
}
actionManager.addRandomActionForArr(ufoCtrlArr, ufoCtrlArr[0].attr.speed);
}
bool checkTimeOut()
{
if (timeAfterRoundStart > difficultyManager.currentSendInterval)
{
return true;
}
return false;
}
bool checkAllUFOIsShot()
{
return UFOfactory.getUsingList().Count == 0;
}
public void UFOIsShot(UFOController UFOCtrl)
{
// 響應(yīng)UFO被擊中的事件
scorer.record(difficultyManager.getDifficulty());
actionManager.removeActionOf(UFOCtrl.getObj());
UFOfactory.recycle(UFOCtrl);
explosionFactory.explodeAt(UFOCtrl.getObj().transform.position);
}
public void GroundIsShot(Vector3 pos) {
// 響應(yīng)地面被擊中的事件(直接產(chǎn)生一個爆炸)
explosionFactory.explodeAt(pos);
}
}
其他的類
其他的類實現(xiàn)非常簡單粘室,都是三、四十行代碼卜范,也沒有涉及新的知識衔统,我就不在這里一一講解了,大家可以下載我的代碼自己查看海雪。
可以做的改進
- 設(shè)計失敗的規(guī)則锦爵,比如規(guī)定時間內(nèi)沒拿到多少分就失敗。
- 設(shè)計一套UI奥裸,讓用戶可以控制游戲的難度险掀。
- “子彈”發(fā)射的速度太快了,如果按住鼠標的話湾宙,會每一幀發(fā)出一條射線樟氢。讓射速慢下來吧。
- 增加換彈機制侠鳄。
- 讓飛碟在主角身邊生成埠啃,或者會自動飛到主角附近。
感悟
我們在上一個游戲的時候畦攘,我們定義了幾個關(guān)于動作的類(ObjAction霸妹、MoveToAction、SequenceAction知押、ActionManager)叹螟。在這個游戲中鹃骂,我可以幾乎一字未改地復(fù)制到了這個游戲中(后來調(diào)整了一些參數(shù)的順序),為什么能夠復(fù)用如此之多的代碼罢绽?
這是因為關(guān)于動作的基本類與上一個游戲的業(yè)務(wù)邏輯沒有任何關(guān)系畏线,這些代碼是很容易復(fù)用的。我們上一個游戲的業(yè)務(wù)邏輯封裝在了一個FirstSceneActionManager
類中良价,通過調(diào)用這些基本類的API來控制動作寝殴。
在這個游戲中,我們也是只需要重新寫FirstSceneActionManager
類就可以了明垢,底層的代碼不用改變蚣常。
這就告訴我們在實現(xiàn)底層代碼的時候不要實現(xiàn)具體的業(yè)務(wù)邏輯,我們只實現(xiàn)抽象的痊银、通用的抵蚊、基礎(chǔ)的一些功能,當我們針對游戲需要實現(xiàn)業(yè)務(wù)邏輯的時候溯革,通過調(diào)用這些底層的基本功能來完成具體的功能贞绳,這樣可以讓代碼的復(fù)用最大化。
在實現(xiàn)底層的類的時候必須要從長遠來考慮致稀,我們將來可能需要底層代碼來做什么冈闭?底層代碼的API是否能滿足我所有可能的需求?怎么設(shè)計API來讓它們使用起來更方便抖单?
如果你以后實現(xiàn)各種業(yè)務(wù)邏輯的時候萎攒,發(fā)現(xiàn)一點也不用修改底層的代碼,就說明底層這套API實現(xiàn)足夠的健壯臭猜、通用了躺酒。
職責分離也有利于代碼的模塊化押蚤、減少耦合蔑歌。比如說不要在工廠中直接給產(chǎn)生的飛碟添加動作(因為管理動作不是工廠的職責),而要將飛碟傳遞給FirstController以后揽碘,讓FirstController去調(diào)用動作管理器來添加次屠。這樣就將工廠和動作管理器之間的耦合降低了。將來你想要給飛碟添加更多種運動方式的時候雳刺,只需要更改動作管理器類就可以了劫灶,完全不用管工廠類。否則掖桦,你會發(fā)現(xiàn)飛碟一旦生成就會按照舊方式來運動本昏,這樣你就要修改更多的代碼(既要改動工廠類、又要改動動作管理器類)枪汪。
感謝閱讀涌穆!