学习Unity(8)打飞碟小游戏——用户输入、使用工厂、代码复用、职责分离

任务概述

这次我们重新制作一个打飞碟小游戏。游戏每一轮生成10个飞碟,每个飞碟随机飞行,玩家要在这一轮结束之前尽快地射击飞碟,击中了就加分,分数达到一定的程度就提升难度。这个游戏很基本,也很简单,我们通过它来学习玩家输入、使用射线、使用工厂来获取和回收对象,并且体会代码复用的技巧。

游戏截图


下载我的项目在本地查看!

我的github下载项目资源,将Assets文件夹覆盖你的项目中的Assets文件夹,然后在U3D中双击“hw5”,就可以运行了!

学会使用他人的资源

这个游戏有一些资源是从外部导入的,比如说RigidBodyFPSController(第一人称控制器,可以像CS一样控制主角)来自标准资源库的Characters包(在这篇文章中我教大家导入了标准资源的Environments包)。

枪支的预制和爆炸的预制,是从Asset Store中免费下载的资源,下载好之后会弹出选择框,让你从下载的资源包中选择自己需要的资源。适当地使用他人的资源能够让你专注于自己的游戏内容。

玩家输入、使用射线

在Update中使用Input.GetButton(string buttonName),在某一帧如果这个按键出于按下状态,就返回true,否则返回false。通过这个方式来监测用户的输入并做出反应。

使用GetButton可以得到“扫射”的效果,也就是说如果你按着这个键不放,那么就一直返回true。Input.GetButtonDown则不一样,只有你“按下”的那一帧会返回true,只能得到“点射”的效果。
Input还可以监测键盘按键、鼠标移动等,其他的使用方式可以查找官方文档或搜索其他博客,这里我们专注于这个小游戏。

射线:

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)将这条射线发射出去,如果射线击中了物体则返回true,并将射线击中的信息保存在参数hit中,你可以从中获得击中的物体、击中的位置等信息。

out是一个关键字,类似于传递引用、只不过函数会将out传进去的参数清空,再放入数据。也就是说如果使用ref关键字,信息有进有出;使用out,信息只出不进。


Shooter

在我们的游戏中,Shooter就是用来监测鼠标点击并发射射线的,挂载在枪支对象上,射线击中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;  // 枪口火焰的预制,我已经将预制拖动到了Inspector中
    bool muzzleFlashEnable = false; // 是否显示枪口火焰
    float muzzleFlashTimer = 0; // 记录枪口火焰已经显示了多久
    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参数使这个射线只能打中指定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;
            }
        }
    }
}

关键的代码我都已经注释说明。物体被击中以后的事情交给sceneController来安排,Shooter只专注于“射击”的功能。


使用工厂来获取、回收对象

GameObject.Instantiate是一个非常消耗系统资源的函数。如果每一次我们需要飞碟的时候,我们都使用GameObject.Instantiate,游戏的性能会很差。所以我们现在使用一个工厂来回收利用使用完毕的UFO。UFO销毁的时候,我们不调用系统的destroy函数,而是仅仅setactive(false),下次需要UFO的时候让它出现在应该的位置就可以了。这样做减少了Instantiate和Destroy的调用。

public class UFOFactory : MonoBehaviour
{
    Queue<UFOController> freeQueue; // 储存空闲状态的UFO
    List<UFOController> usingList;  // 储存正在使用的UFO

    GameObject originalUFO; // UFO的原型,以后创建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的颜色速度大小设置成参数指定的样子
        usingList.Add(newUFO);          // 将UFO加入使用中的队列
        newUFO.appear();
        return newUFO;
    }

    public UFOController[] produceUFOs(UFOAttributes attr, int 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,作用一样,用来获取和回收“爆炸对象”,因为爆炸也像飞碟一样,频繁产生、消失的。ExplosionFactory的实现很相似,代码我就不放在这里了,要查看的话下载我的项目就可以了。


使用场景控制器协调各个场景组件

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都已经被击落
        {
            print("All UFO is shot down! Next round in 3 sec");
            roundHasStarted = false;
            Invoke("roundStart", 3);
            difficultyManager.setDifficultyByScore(scorer.getScore());
        }
        else if (roundHasStarted && checkTimeOut()) // 检查这一轮是否已经超时
        {
            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)
    {
        // 响应UFO被击中的事件
        scorer.record(difficultyManager.getDifficulty());
        actionManager.removeActionOf(UFOCtrl.getObj());
        UFOfactory.recycle(UFOCtrl);
        explosionFactory.explodeAt(UFOCtrl.getObj().transform.position);
    }

    public void GroundIsShot(Vector3 pos) {
        // 响应地面被击中的事件(直接产生一个爆炸)
        explosionFactory.explodeAt(pos);
    }
}

其他的类

其他的类实现非常简单,都是三、四十行代码,也没有涉及新的知识,我就不在这里一一讲解了,大家可以下载我的代码自己查看。


可以做的改进

  • 设计失败的规则,比如规定时间内没拿到多少分就失败。
  • 设计一套UI,让用户可以控制游戏的难度。
  • “子弹”发射的速度太快了,如果按住鼠标的话,会每一帧发出一条射线。让射速慢下来吧。
  • 增加换弹机制。
  • 让飞碟在主角身边生成,或者会自动飞到主角附近。

感悟

我们在上一个游戏的时候,我们定义了几个关于动作的类(ObjAction、MoveToAction、SequenceAction、ActionManager)。在这个游戏中,我可以几乎一字未改地复制到了这个游戏中(后来调整了一些参数的顺序),为什么能够复用如此之多的代码?

这是因为关于动作的基本类与上一个游戏的业务逻辑没有任何关系,这些代码是很容易复用的。我们上一个游戏的业务逻辑封装在了一个FirstSceneActionManager类中,通过调用这些基本类的API来控制动作。

在这个游戏中,我们也是只需要重新写FirstSceneActionManager类就可以了,底层的代码不用改变。

这就告诉我们在实现底层代码的时候不要实现具体的业务逻辑,我们只实现抽象的、通用的、基础的一些功能,当我们针对游戏需要实现业务逻辑的时候,通过调用这些底层的基本功能来完成具体的功能,这样可以让代码的复用最大化

在实现底层的类的时候必须要从长远来考虑,我们将来可能需要底层代码来做什么?底层代码的API是否能满足我所有可能的需求?怎么设计API来让它们使用起来更方便?

如果你以后实现各种业务逻辑的时候,发现一点也不用修改底层的代码,就说明底层这套API实现足够的健壮、通用了。

职责分离也有利于代码的模块化、减少耦合。比如说不要在工厂中直接给产生的飞碟添加动作(因为管理动作不是工厂的职责),而要将飞碟传递给FirstController以后,让FirstController去调用动作管理器来添加。这样就将工厂和动作管理器之间的耦合降低了。将来你想要给飞碟添加更多种运动方式的时候,只需要更改动作管理器类就可以了,完全不用管工厂类。否则,你会发现飞碟一旦生成就会按照旧方式来运动,这样你就要修改更多的代码(既要改动工厂类、又要改动动作管理器类)。


感谢阅读!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容