學習Unity(8)打飛碟小游戲——用戶輸入镣屹、使用工廠圃郊、代碼復(fù)用、職責分離

任務(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)飛碟一旦生成就會按照舊方式來運動本昏,這樣你就要修改更多的代碼(既要改動工廠類、又要改動動作管理器類)枪汪。


感謝閱讀涌穆!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怔昨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宿稀,更是在濱河造成了極大的恐慌趁舀,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祝沸,死亡現(xiàn)場離奇詭異矮烹,居然都是意外死亡,警方通過查閱死者的電腦和手機罩锐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門奉狈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涩惑,你說我怎么就攤上這事嘹吨。” “怎么了境氢?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵蟀拷,是天一觀的道長。 經(jīng)常有香客問我萍聊,道長问芬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任寿桨,我火速辦了婚禮此衅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亭螟。我一直安慰自己挡鞍,他們只是感情好,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布预烙。 她就那樣靜靜地躺著墨微,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扁掸。 梳的紋絲不亂的頭發(fā)上翘县,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天,我揣著相機與錄音谴分,去河邊找鬼锈麸。 笑死,一個胖子當著我的面吹牛牺蹄,可吹牛的內(nèi)容都是我干的忘伞。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼氓奈!你這毒婦竟也來了匿刮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤探颈,失蹤者是張志新(化名)和其女友劉穎熟丸,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伪节,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡光羞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了怀大。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纱兑。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖化借,靈堂內(nèi)的尸體忽然破棺而出潜慎,到底是詐尸還是另有隱情,我是刑警寧澤蓖康,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布铐炫,位于F島的核電站,受9級特大地震影響蒜焊,放射性物質(zhì)發(fā)生泄漏倒信。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一泳梆、第九天 我趴在偏房一處隱蔽的房頂上張望鳖悠。 院中可真熱鬧,春花似錦优妙、人聲如沸乘综。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卡辰。三九已至,卻和暖如春熟菲,著一層夾襖步出監(jiān)牢的瞬間看政,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工抄罕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人于颖。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓呆贿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子做入,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 改進描述 在我們之前完成的飛碟游戲中冒晰,UFO是在兩點之間來回飛行,我們是通過修改position來使得飛碟運動起來...
    csRyan閱讀 945評論 0 6
  • 在我們的游戲開發(fā)過程中竟块,有一個很重要的工作就是進行碰撞檢測壶运。例如在射擊游戲中子彈是否擊中敵人,在RPG游戲中是否撿...
    壹米玖坤閱讀 24,432評論 0 17
  • 本文同時發(fā)布至我的個人博客浪秘,點擊進入我的個人博客閱讀蒋情。本博客供技術(shù)交流與經(jīng)驗分享,可自由轉(zhuǎn)載耸携。轉(zhuǎn)載請在評論區(qū)或私信...
    DamonTo閱讀 1,018評論 0 2
  • 做事忌拖拉棵癣。開始做了之后往往會發(fā)現(xiàn)有新的之前沒想到問題。所以開始做才是解決問題的伊始夺衍。 送禮談事情忌裝作輕描淡寫狈谊,...
    Turtle1220閱讀 243評論 0 1
  • 濃云秋日遠, 宿雨鳴蟲閑沟沙; 懶起憑窗立河劝, 方覺暑氣淺!
    鼎馨藍閱讀 232評論 0 0