3D Game Programming Design(三):空间与运动(单例模式,外观模式)

阅读以下游戏脚本


Priests and Devils

Priests and Devils is a puzzle game in which you will help the Priests and Devils to cross the river within the time limit. There are 3 priests and 3 devils at one side of the river. They all want to get to the other side of this river, but there is only one boat and this boat can only carry two persons each time. And there must be one person steering the boat from one side to the other side. In the flash game, you can click on them to move them and click the go button to move the boat to the other direction. If the priests are out numbered by the devils on either side of the river, they get killed and the game is over. You can try it in many >ways. Keep all priests alive! Good luck!

中文版:

三个和尚和三个恶魔要过河,他们只有一艘一次最多坐2人的船,船需要至少一个人才能划动。需要时刻保证任意一岸的和尚不少于恶魔,不然恶魔就会将和尚干掉。恶魔们是老实人,并不会逃跑。


程序需要满足的要求:

  • 用表格列出玩家动作表(规则表),注意,动作越少越好

  • 整个游戏仅 主摄像机 和 一个 Empty 对象, 其他对象必须代码动态生成!!!
  • 请使用课件架构图编程,不接受非 MVC 结构程序

本文重点解决MVC架构的书写,关于MVC里的M(Models)只提一嘴我的实现方案,因为这个还是比较简单的。

还是先看一眼效果图

扫描二维码关注公众号,回复: 1569281 查看本文章

显然一开始场景里只有一个empty物体,场景是通过点击UI生成出来的。

我把整个游戏关卡做成了prefab,也就是说models全部被集合在一个空物体下,游戏运行的逻辑让他在内部自己处理,Controller并不关心游戏是怎么运行的,他只负责生成游戏并且根据UI的指令控制他

我们来看类图,先说说这个标注了Singlton(单例)的类

GoF对于单例模式的定义是这样的,“确保一个类只有一个实例,并为其提供一个全局访问入口”

确保一个类只有一个实例:在某些情况下,如果一个类有多个实例就会不能正常运行。最常见的就是这个类与一个维持着自身全局状态的外部系统进行交互的情况。例如一个封装了底层文件操作API的类,因为文件操作是需要时间的,所以我们会选择异步处理,这样就要处理指令的冲突,例如我们调用一个方法复制文件,同时又调用一个方法删除这个文件,这时候就需要我们在类里解决这种冲突。但是如果这个类可以创建很多实例,就会导致冲突难以处理。打个比方,如果听一个人的指令做事,那很容易听懂,但同时听十个人的指令,就会什么都做不成。

提供一个全局访问入口:对于面向对象编程,很多时候会有一个问题,“我该怎么获得他的实例?”,我们知道,类本身是无法操作的,我们需要面对实例操作,就像你无法对“猫”这个概念操作,你要获得一只活生生的猫你才能调用方法。但我们上面说了,我们不希望系统可以自己创建这个实例,所以我们设计出单例模式,创建一个且仅一个实例,并且可以在全局访问它

SSDirector.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SSDirector{

    private static SSDirector _Director; //静态成员保存了这个类的一个实例
    public ISceneController currentSceneController { get; set; } //这行和单例模式无关

    private SSDirector() {} //私有的构造函数确保他不能用new的方法创建实例
    //PS:千万不要在继承了MonoBehaviour的类下使用构造函数,他有两个生命周期,无法保证构造函数的这个方法可以正常使用

    //公有的静态函数instance()为整个代码库提供了一个获得该类的实例的方法
    public static SSDirector instance() {
        //如果实例还没有创建,那么就创建一个
        if (_Director == null) {
            _Director = new SSDirector();
        }
        //然后返回这个唯一的实例
        return _Director;
    } 
}

这种写法的优点有:

1、如果我们不去调用instance()方法,这个类就不会创建实例

2、他在游戏运行时才会初始化。越简单的东西越好用,所以一般来说,我们会选择使用静态类而不是单例,但是静态类有一个缺陷——他在main()函数调用前就初始化了静态数据。这代表他不能使用那些在游戏运行后才会有的信息,并且静态类之间无法依赖,编译器无法搞清楚他们的编译先后顺序。单例模式的延迟初始化解决了这个问题,甚至只要不是循环依赖,一个单例初始化时可以引用另一个单例

3、这样的单例可以被继承。例如我们写了一个文件封装类,我们需要它跨平台。我们可以把他写作单例,对不同的平台写不同的派生类,这样我们整个代码库只需要通过.instance()来访问文件系统,不必和平台的代码发生耦合了

当然,单例模式提供了全局的便利的同时,也带来了全局变量的缺点,不过这属于比较高难的领域了


我们继续看类图,看到两个interface(接口)

interface有点类似抽象类,但是比抽象类更严格,我们对比下区别

抽象类:抽象成员可以有实现代码,或没有实现代码

接口:接口不能有任何实现代码,它也不能声明成员的修饰符,总是隐式为public

接口的作用大都用来派生类,它的强大之处在于,把引用变量声明为接口引用的方式,可以引用任何实现该接口的类,虽然这个引用只能调用接口里已经定义了的一部分方法。(这里听不懂没关系,在最后提一下model时我会给一个实际例子)

有了接口之后我们就可以方便地实现“外观模式(门面模式)”,外观模式在GOF里是这样定义的:为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

打个比方,例如客户想要吃饭,厨师给客户做饭,客户不需要调用厨师做饭的方法来指挥厨师做饭(万一换了一个厨师听不懂客户在说啥呢,是不是?),他只需要告诉厨师,做饭!!!,厨师就会自己用自己的方法来返回一份饭给客户了

一份示意图

用接口来实现的话,也就是客户只要获取了服务类的接口引用,直接调用接口的方法就可以了,不需要知道服务类的细节

我们把代码实现一下。首先是2个接口

IUserAction.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IUserAction{
    void SpeedUp();
    void SpeedDown();
    void GameOver();
}

ISceneController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ISceneController{
    void LoadResources();
    void Resume();
    void Pause();
}

然后是FirstController.cs,我们不需要关心实现,重要的是看Awake里的那一行代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//FirstController类继承了2个接口
public class FirstController : MonoBehaviour, ISceneController , IUserAction{ 

    GameObject currentScene = null;
    public float sceneSpeed = 1.0f;

    private bool isGameOver = false;

    void Awake() {
        //将Director的接口引用 的目标改成自己
        SSDirector.instance().currentSceneController = this;
    }

    public void LoadResources()
    {
        currentScene = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Scene"));
        currentScene.GetComponent<GameController>().sceneController = this;
    }

    public void Resume()
    {
        sceneSpeed = 1.0f;
    }

    public void Pause()
    {
        sceneSpeed = 0f;
    }

    public void SpeedUp()
    {
        sceneSpeed += 0.5f;
    }

    public void SpeedDown()
    {
        sceneSpeed -= 0.5f;
        if (sceneSpeed < 0) sceneSpeed = 0;
    }

    public void GameOver()
    {
        Destroy(currentScene);
        isGameOver = false;
    }

    void OnGUI() {
        if (isGameOver == true)
        {
            GUI.Label(new Rect(310, 500, 120, 20), "waiting!!!");
        }
    }
}

把Director的接口引用改为自己后(前提是自己有继承这个接口),就可以调用那个接口引用的方法,从而反过来使用自己对接口的实现,Director是不需要知道FirstController的实现细节的,把FirstController删了换成SecondController也一样,Director只需要使用接口的方法


最后说一下Models。架构不提了,基本上就是自己在内部处理逻辑,只有游戏失败,加速暂停,重开时,通过里面的Manager与Firstcontroller交互。因为不喜欢UI控制的方式,所以用了射线检测的方法,点击人物或船就会动。如图


下面是处理Input的代码

InputController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//处理鼠标输入点击
public class InputController : MonoBehaviour {

    void Update() {
        if (Input.GetMouseButtonDown(0)) {
        //如果鼠标左键有点击
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            bool isCollider = Physics.Raycast(ray, out hit); 
            //射线与collider发生了碰撞
            if (isCollider) {
                //hit.transform.gameObject.SendMessage("excuse", SendMessageOptions.DontRequireReceiver);
                IObjectAction _temp = hit.transform.GetComponent<IObjectAction>(); //获取碰撞物体上的继承了IObjectAciton接口的组件
                if (_temp != null) _temp.excuse(); //如果非空,则代表这个物体上有我们需要的组件,就使用
            }
        }
    }
}

IObjectAction.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IObjectAction{
    void excuse();
}

IObjectAction接口只有一个excuse方法,当鼠标点击到这个物体时调用。

但是我们写的Boat和Man组件是名字不同的类,我们也不知道我们点到的物体究竟是什么,有什么组件,通过查询名字反过来得到组件的话会与物体属性耦合,所以我们要用interface

例如下面的组件代码挂载在“船”物体,主要处理有关船运动和人物计数的功能

Boat.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Boat : MonoBehaviour , IObjectAction{

    public void  excuse()
    {
       ......
    }
}

只需要继承了对应的接口,getComponent也可以正确获取到第一个有这个接口的组件。我们就可以不用管我们点击到的是什么物体,一律让他excuse!!!


github链接:https://github.com/keven2148/3D-Game-Programming-Design-Lesson-Work/tree/master/Lesson3

版本为2017.3

猜你喜欢

转载自blog.csdn.net/keven2148/article/details/79760734
今日推荐