学习Unity(9)打飞碟小游戏改进——使用适配器模式

改进描述

我们之前完成的飞碟游戏中,UFO是在两点之间来回飞行,我们是通过修改position来使得飞碟运动起来的。

现在,为了练习对Unity物理引擎的使用和适配器模式的使用,我们想要加入另一种飞碟运动模式:物理运动模式,飞碟受到向下的力,向地面撞去。玩家要在飞碟撞到地面之前击中飞碟才能得分,飞碟撞上地面则不得分。

并且,我们不仅要实现物理运动模式,还要保留着原本的普通运动模式,通过鼠标右键,用户可以在两种模式之间切换。

游戏截图

正常运动模式下飞碟来回飞行
鼠标右键可以切换运动模式
物理模式下飞碟会缓缓掉落地面

在自己的电脑上运行这个游戏!

我的github下载项目资源,将所有文件放进你的项目的Assets文件夹(如果有重复则覆盖),然后在U3D中双击“hw5”,就可以运行了!

实现物理模式的动作管理器

这次的改进有一点特别。正常运动模式不能删除,而是与新的物理运动模式共存,我们要在游戏运行的时候来决定使用哪种运动模式。也就是说,原本的动作管理器类不能删除,它们是管理正常运动模式的。我们还要再实现一个动作管理器,用来管理物理运动模式。最后想一种办法将两个动作管理器结合起来。
首先我们实现物理模式动作管理器:

public class PhysicsActionManager : MonoBehaviour {

    public void addForce(GameObject gameObj, Vector3 force) {
        ConstantForce originalForce = gameObj.GetComponent<ConstantForce>();
        if (originalForce) {
            originalForce.enabled = true;
            originalForce.force = force;
        } else {
            gameObj.AddComponent<Rigidbody>().useGravity = false;
            gameObj.AddComponent<ConstantForce>().force = force;
        }
    }

    public void removeForce(GameObject gameObj) {
        gameObj.GetComponent<ConstantForce>().enabled = false;
    }
}

这个管理器的实现非常简单,只需要负责增加\移除ConstantForce组件就可以了。

要使物体受到力的影响,必须先让他具有Rigidbody(刚体)组件。对物理引擎的使用,网上有很多教程。你可以查看官方文档学习其他作者的博客


适配器模式

如何将两种动作管理器有机地结合起来呢?让FirstController(场景控制器)同时拥有两个变量,分别指向这两个动作管理器吗?这样不好,如果我们以后又要增加新的动作管理器呢?如果我们要增加新的飞碟工厂类呢?这样的话FirstController就需要管理太多功能相同的部件了,FirstController会越来越臃肿,可扩展性很差。

我们希望FirstController只需要为同一个用途的所有组件保存1个变量

这就是为什么我们需要适配器模式。
让我通过一个生活中的例子来解释适配器模式:现在大部分的的平板电脑只有一个USB接口,现在我想在我的平板电脑上同时使用键盘和鼠标,怎么办?很简单,买一个这样的USB扩展器:


USB扩展器就是一种适配器

将USB扩展器插在平板的USB接口上,然后将键盘、鼠标插在USB扩展器的USB接口上,你就可以同时使用键盘和鼠标了!

在这个例子中,我们的平板就像是FirstController,两个输入设备就像是两个动作管理器。要将两个动作管理器同时接入FirstController,我们要实现一个适配器,让FirstController连接适配器,然后让适配器连接两个动作管理器。

我们先将FirstController中原本保存ActionManager的变量删掉,然后添加这一行:

ActionManagerTarget actionManagerTarget;

ActionManagerTarget是一个接口,它就相当于平板电脑上的USB接口:

public interface ActionManagerTarget {
    void switchActionMode();
    
    void addAction(GameObject gameObj, Dictionary<string, object> option);

    void addActionForArr(GameObject[] Arr, Dictionary<string, object> option);

    void addActionForArr(UFOController[] Arr, Dictionary<string, object> option);

    void removeActionOf(GameObject obj, Dictionary<string, object> option);
}

然后实现一个适配器类ActionManagerAdapter,这个类要实现这个接口:

public class ActionManagerAdapter: ActionManagerTarget {
    FirstSceneActionManager normalAM;
    PhysicsActionManager PhysicsAM;

    int whichActionManager = 0; // 0->normal, 1->physics

    public ActionManagerAdapter(GameObject main) {
        normalAM = main.AddComponent<FirstSceneActionManager>();
        PhysicsAM = main.AddComponent<PhysicsActionManager>();
        whichActionManager = 0;
    }

    public void switchActionMode() {
        whichActionManager = 1-whichActionManager;
    }

    public void addAction(GameObject gameObj, Dictionary<string, object> option) {
        if (whichActionManager == 0)
        //  use normalAM
        {
            Debug.Log("use normalAM");
            normalAM.addRandomAction(gameObj, (float)option["speed"]);
        }

        else
        //  use PhysicsAM
        {
            Debug.Log("use PhysicsAM");
            PhysicsAM.addForce(gameObj, (Vector3)option["force"]);
        }
    }

    public void addActionForArr(GameObject[] Arr, Dictionary<string, object> option) {
        if (whichActionManager == 0)
        //  use normalAM
        {
            Debug.Log("use normalAM");
            float speed = (float)option["speed"];
            foreach (GameObject gameObj in Arr) {
                normalAM.addRandomAction(gameObj, speed);
            }
        }

        else
        //  use PhysicsAM
        {
            Debug.Log("use PhysicsAM");
            Vector3 force = (Vector3)option["force"];
            foreach (GameObject gameObj in Arr) {
                PhysicsAM.addForce(gameObj, force);
            }
        }
    }

    public void addActionForArr(UFOController[] Arr, Dictionary<string, object> option) {
        if (whichActionManager == 0)
        //  use normalAM
        {
            Debug.Log("use normalAM");
            float speed = (float)option["speed"];
            foreach (UFOController ctrl in Arr) {
                normalAM.addRandomAction(ctrl.getObj(), speed);
            }
        }

        else
        //  use PhysicsAM
        {
            Debug.Log("use PhysicsAM");
            Vector3 force = (Vector3)option["force"];
            foreach (UFOController ctrl in Arr) {
                PhysicsAM.addForce(ctrl.getObj(), force);
            }
        }
    }

    public void removeActionOf(GameObject gameObj, Dictionary<string, object> option){
        if (whichActionManager == 0)
        //  use normalAM
        {
            Debug.Log("use normalAM");
            normalAM.removeActionOf(gameObj);
        }

        else
        //  use PhysicsAM
        {
            Debug.Log("use PhysicsAM");
            PhysicsAM.removeForce(gameObj);
        }
    }
}

可以看出,我们在实现适配器的时候,将两个动作管理器“焊死”在适配器上了,你还可以自己尝试,实现一个可以“自由插拔”的适配器:)。

然后我们在FirstController的构造函数中实例化一个适配器(相当于将USB扩展器插在平板电脑上):

actionManagerTarget = new ActionManagerAdapter(gameObject);

最后不要忘了在Update中监测用户鼠标的右键输入,切换动作管理模式。最终的FirstController是这样的:

public class FirstController : MonoBehaviour, SceneController
{
    Director director;

    UFOFactory UFOfactory;

    ExplosionFactory explosionFactory;

    ActionManagerTarget actionManagerTarget;

    bool switchAMInNextRound = false;

    Scorer scorer;

    DifficultyManager difficultyManager;

    float timeAfterRoundStart = 10;

    bool roundHasStarted = false;

    FirstCharacterController firstCharacterController;

    Text hint;

    void Awake()
    {
        // 挂载各种控制组件

        director = Director.getInstance();
        director.currentSceneController = this;

        // actionManager = gameObject.AddComponent<FirstSceneActionManager>();
        actionManagerTarget = new ActionManagerAdapter(gameObject);

        UFOfactory = gameObject.AddComponent<UFOFactory>();

        explosionFactory = gameObject.AddComponent<ExplosionFactory>();

        scorer = Scorer.getInstance();
        difficultyManager = DifficultyManager.getInstance();


        loadResources();
        Physics.IgnoreLayerCollision(LayerMask.NameToLayer("Shootable"), LayerMask.NameToLayer("Shootable"), true);
    }
    public void loadResources()
    {
        // 初始化场景中的物体
        firstCharacterController = new FirstCharacterController();
        Instantiate(Resources.Load("Terrain"));
        hint = (Instantiate(Resources.Load("ShowResult")) as GameObject).GetComponentInChildren<Text>();
        hint.text = "";
    }

    public void Start()
    {
        roundStart();
    }

    void Update()
    {
        if (roundHasStarted) {
            timeAfterRoundStart += Time.deltaTime;
        }

        if (roundHasStarted && checkAllUFOIsShot()) // 检查是否所有UFO都已经被击落
        {
            hint.text = "All UFO has crashed in this round! Next round in 3 sec";
            roundHasStarted = false;
            Invoke("roundStart", 3);
            difficultyManager.setDifficultyByScore(scorer.getScore());
        }
        else if (roundHasStarted && checkTimeOut()) // 检查这一轮是否已经超时
        {
            hint.text = "Time out! Next round in 3 sec";
            roundHasStarted = false;
            foreach (UFOController ufo in UFOfactory.getUsingList())
            {
                actionManagerTarget.removeActionOf(ufo.getObj(), new Dictionary<string, object>());
            }
            UFOfactory.recycleAll();
            Invoke("roundStart", 3);
            difficultyManager.setDifficultyByScore(scorer.getScore());
        }
        if (Input.GetButtonDown("Fire2")) {
            hint.text = "Action of UFOs will change in the next round!";
            switchAMInNextRound = true;
        }
    }

    void roundStart()
    {   
        // 开始新的一轮
        if (switchAMInNextRound) {
            switchAMInNextRound = false;
            actionManagerTarget.switchActionMode();
        }

        roundHasStarted = true;
        timeAfterRoundStart = 0;
        UFOController[] ufoCtrlArr = UFOfactory.produceUFOs(difficultyManager.getUFOAttributes(), difficultyManager.UFONumber);
        for (int i = 0; i < ufoCtrlArr.Length; i++)
        {
            ufoCtrlArr[i].appear();
            ufoCtrlArr[i].setPosition(getRandomUFOPosition());
        }

        actionManagerTarget.addActionForArr(ufoCtrlArr, new Dictionary<string, object>() {
            {"speed", ufoCtrlArr[0].attr.speed},
            {"force", difficultyManager.getGravity()}
        });
        hint.text = "";
    }

    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());
        actionManagerTarget.removeActionOf(UFOCtrl.getObj(), new Dictionary<string, object>());
        UFOfactory.recycle(UFOCtrl);
        explosionFactory.explodeAt(UFOCtrl.getObj().transform.position);
    }

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

    public void UFOCrash(UFOController UFOCtrl) {
        actionManagerTarget.removeActionOf(UFOCtrl.getObj(), new Dictionary<string, object>());
        UFOfactory.recycle(UFOCtrl);
        explosionFactory.explodeAt(UFOCtrl.getObj().transform.position);
    }

    public Vector3 getRandomUFOPosition() {
        Vector3 relativeToCharacter = new Vector3(Random.Range(-10, 10), Random.Range(10, 15), Random.Range(-10, 10));
        return firstCharacterController.getPosition()+relativeToCharacter;
    }
}

注意我没有在监测到鼠标右键输入以后马上切换动作管理模式,而是通过一点小技巧,延迟到下一轮开始的时候再切换。这是因为如果立刻切换,这一轮的“动作取消”会出很大的问题。你仔细想想,这一轮开始的时候,我们使用正常动作管理器给每个飞碟添加了一个普通的动作,而取消动作的时候却使用物理动作管理器!这样,飞碟上的普通动作就无法被回收,下一轮开始的时候飞碟依然在来回移动。

ActionManagerAdapter使用了一个非常灵活的方式来接收参数:Dictionary<string, object> option 其中的object可以传递任何类型的值,甚至是int、float原始类型。因为FirstController不知道当前的运动模式是什么,不知道应该给ActionManagerAdapter传递speed参数还是force参数,于是干脆两个都传进去,让ActionManagerAdapter自己选择:

actionManagerTarget.addActionForArr(ufoCtrlArr, new Dictionary<string, object>() {
            {"speed", ufoCtrlArr[0].attr.speed},
            {"force", difficultyManager.getGravity()}
        });

适配器模式补充说明

适配器模式定义

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。

需要接入2个类,而客户类只提供1个接口,这也是一种“接口不兼容”。

适配器模式的组成

  • Target:目标抽象类(USB接口)
  • Adapter:适配器类(USB扩展器)
  • Adaptee:适配者类(鼠标、键盘、U盘)
  • Client:客户类(平板电脑)

适配器的作用,除了我们刚才所说的,将多个类接入同一个接口以外,还有转接“不兼容”接口的作用。比如说,如果我们想将U盘插入USB-typeC接口中,我们要买另一种适配器:

USB转接器也是一种适配器

这个适配器也解决了“接口不兼容”的问题。当“客户类提供的接口”与“适配者类”不兼容的时候,可以实现一个适配器,让适配器实现“客户类提供的接口”,并在这个适配器中调用“适配者类”的方法。

如果还想深入学习有关适配器模式的内容,可以看看这个网站


谢谢阅读!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容