Unity——基于MVC的UI框架

前言

今天来学习一下MVC框架思想在Unity项目中的应用

 

 

 

 


MVC框架

概念

MVC全名是Model View Controller,是模型(Model)-视图(View)-控制器(Controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

Model(模型) 是应用程序中用于处理应用程序数据逻辑的部分。
通常模型对象负责在数据库中存取数据。
View(视图) 是应用程序中处理数据显示的部分。
通常视图是依据模型数据创建的。
Controller(控制器) 是应用程序中处理用户交互的部分。
通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

MVC开始是存在于桌面程序中的,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。比如一批统计数据可以分别用柱状图、饼图来表示。C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。

模型-视图-控制器(MVC)是Xerox PARC在二十世纪八十年代为编程语言Smalltalk-80发明的一种软件设计模式,已被广泛使用。后来被推荐为Oracle旗下Sun公司Java EE平台的设计模式,并且受到越来越多的使用ColdFusion和PHP的开发者的欢迎。模型-视图-控制器模式是一个有用的工具箱,它有很多好处,但也有一些缺点。

 

MVC与设计模式的关系

MVC是一种设计模式,但是却不在Gof总结过的23种设计模式中,所以确切说MVC不是一个设计模式,而是多种设计模式的组合,而不仅仅只是一个设计模式。
组成MVC的三个模式分别是组合模式、策咯模式、观察者模式,MVC在软件开发中发挥的威力,最终离不开这三个模式的默契配合。 那些崇尚设计模式无用论的程序员,请了解只要你们使用MVC,就离不开设计模式。
组合模式只在视图层活动, 视图层的实现用的就是组合模式,当然,这里指的实现是底层的实现,是由编程框架厂商做的事情,用不着普通程序员插手。

组合模式的类层次结构是树状的, 而我们做Web时视图层是html页面,html的结构不正是树状的吗,这其实就是一个组合模式的应用,只是浏览器厂商已经把界面相关的工作帮我们做掉了,但它确确实实是我们应用MVC的其中一部分,只是我们感觉不到罢了,这也是我们觉得View是实现起来最简单最没有歧义的一层的原因。

除网页以外的其他用户界面程序,如WPF、Android、ASP.NET等等都是使用树状结构来组织界面控件对象的,因为组合模式就是从界面设计的通用解决方案总提炼出来的。所以与其说MVC选择了组合模式,还不如说组合模式是必定会存在MVC中的,因为只要涉及到用户界面,组合模式就必定存。事实上即使不理解组合模式,也不影响程序员正确的使用MVC,组合模式本就存在于程序员接触不到的位置。

然而,观察者模式和策略模式就显得比较重要,是实实在在MVC中接触的到的部分。

观察者模式有两部分组成,被观察的对象和观察者,观察者也被称为监听者。对应到MVC中,Model是被观察的对象,View是观察者,Model层一旦发生变化,View层即被通知更新。View层和Model层互相之间是持有引用的。 我们在开发Web MVC程序时,因为视图层的html和Model层的业务逻辑之间隔了一个http,所以不能显示的进行关联,但是他们观察者和收听者的关系却没有改变。当View通过http提交数据给服务器,服务器上的Model接受到数据执行某些操作,再通过http响应将结果回送给View,View(浏览器)接受到数据更新界面,这不正是一个接受到通知并执行更新的行为吗,是观察者模式的另一种表现形式。

但是,脱离Web,当通过代码去纯粹的表示一个MVC结构的时候,View和Model间无疑是观察者和被观察的关系,是以观察者模式为理论基础的。即使在Web中因为http壁垒的原因导致真正的实现有点走样,但是原理核心和思路哲学却是不变的。

最后是策略模式。策略模式是View和Controller之间的关系,Controller是View的一个策略,Controller对于View是可替换的, View和Controller的关系是一对多,在实际的开发场景中,也经常会碰到一个View被多个Controller引用,这即使策咯模式的一种体现,只是不那么直观而已。

总结一下,关于MVC各层之间关系所对应的设计模式

View层,单独实现了组合模式

Model层和View层,实现了观察者模式

View层和Controller层,实现了策咯模式

MVC就是将这三个设计模式在一起用了,将这三个设计模式弄明白,MVC将毫无神秘感可言。如果不了解这三个设计模式去学习MVC,那不管怎么学总归是一知半解,用的时候也难免不会出想问题。

 


Unity中的MVC框架

预览

借助网上找的一张官剑铭老师的图片,很好的概括了Unity中的MVC

目录结构

我们以一个武器商店为例子,下面是我的目录结构:

文件详解

其中:UIRoot是我们的UI框架预制体:

StoreWindow是一个Panel的预制体,这个Panel就是我们的商店Panel,即本篇示例的主要面板:

另外,目录SingleIns中是我们两个单例我们的模板类,一个是继承于MonoBehavior的:

public class Singleton<T> where T:new()//T 约束 只能是class类型的
{
    static T instance;

    public static T Instance
    {
        get {
            if (instance==null)
            {
                instance = new T();
            }
            return instance;
        }
    }
}
public class MonoSingleton<T> : MonoBehaviour where T:MonoBehaviour
{
    static T instance;
    public static T Instance
    {
        get {
            if (MonoSingletonObject.go==null)
            {
                MonoSingletonObject.go = new GameObject("MonoSingletonObject");
                DontDestroyOnLoad(MonoSingletonObject.go);
            }

            if (MonoSingletonObject.go!=null&& instance==null)
            {
                instance= MonoSingletonObject.go.AddComponent<T>();
            }

            return instance;
        }
    }

    //有时候  有的组件场景切换的时候回收的
    public static bool destroyOnLoad = false;
    //添加场景切换时候的事件
    public void AddSceneChangedEvent() {
        //SceneManager自带属性activeSceneChanged,是一个委托,可以添加绑定方法
        SceneManager.activeSceneChanged += OnSceneChanged;
    }
    private void OnSceneChanged(Scene arg0, Scene arg1)
    {
        if (destroyOnLoad==true)
        {
            if (instance!=null)
            {
                DestroyImmediate(instance);//立即销毁
                Debug.Log(instance == null);
            }
        }
    }
}


//缓存一个游戏物体
public class MonoSingletonObject
{
    public static GameObject go;
}

MVCLibrary目录下有脚本Type.cs文件,内部定义了两个枚举,WindowType类对应的每一个值就是每一个UI界面;ScenesType是不同的场景,用来做预加载处理:

/// <summary>
/// 窗体类型
/// </summary>
public enum WindowType
{
    LoginWindow,
    StoreWindow,
    TipsWindow,
}
/// <summary>
/// 场景类型,目的:根据提供场景类型进行预加载
/// </summary>
public enum ScenesType
{
    None,
    Login,
    Battle,
}

UIRoot.cs用来管理UIRoot预制体,管理UIRoot中的三层面板,主要方法SetParent来给Panel设置所属父面板:

public class UIRoot 
{
    //UIRoot本尊
    static Transform transfowm;
    //回收的窗体:回收池
    static Transform recyclePool;
    //前台显示/工作的窗体
    static Transform workstation;
    //提示类型的窗体
    static Transform noticestation;

    static bool isInint = false;

    public static void Init() {
        if (transfowm == null) {
            GameObject obj=Resources.Load<GameObject>("UI/UIRoot");
            transfowm = GameObject.Instantiate(obj).transform;
        }
        if (recyclePool == null)
        {
            recyclePool = transfowm.Find("recyclePool");
        }
        if (workstation == null)
        {
            workstation = transfowm.Find("workstation");
        }
        if (noticestation == null)
        {
            noticestation = transfowm.Find("noticestation");
        }
        isInint = true;
    }
    //对窗体的父panel设置
    public static void SetParent(Transform window,bool isOpen,bool isTipsWindow=false) {
 
        if (!isInint) {   //没有初始化
            Init();
        }
        
        if (isOpen)     //是一个开启的面板
        { 
            if (isTipsWindow)  //是一个提示面板
            {
                //窗体父Panel是noticestation
                //第二个参数意思是“是否启用世界坐标”
                window.SetParent(noticestation, false);
            }
            else
            {
                //窗体父Panel是workstation
                window.SetParent(workstation, false);
            }
        }
        else {
            //窗体父Panel是recyclePool
            window.SetParent(recyclePool, false);
        }
    }
}

WindowManager.cs是对窗口的总管理类,这个单例类中定义了对Panel的打开、关闭等方法,来对各个Panel控制。

View.cs目录下的BaseWindow.cs是各个Panel管理类的总父类:

namespace Game.View
{
    public class BaseWindow
    {
        //窗体
        protected Transform transform;
        //资源名称
        protected string resName;
        //是否常驻
        protected bool resident;
        //当前是否可见
        protected bool visible = false;
        //窗体类型
        protected WindowType selfType;
        //场景类型
        protected ScenesType scenesType;
        //UI控件
        protected Button[] buttonList;
        protected Text[] textList;

        //需要给子类提供的接口
        //初始化
        protected virtual void Awake()
        {
            //参数为true表示包括隐藏的物体
            buttonList = transform.GetComponentsInChildren<Button>(true);
            textList = transform.GetComponentsInChildren<Text>(true);

            //注册UI事件(细节由子类实现)
            RegisterUIEvent();
        }
        //UI事件的注册
        protected virtual void RegisterUIEvent() { }
        //添加监听游戏事件
        protected virtual void OnAddListener() { }
        //移除游戏事件
        protected virtual void OnRemoveListener() { }
        //每次打开
        protected virtual void OnEnable() { }
        //每次关闭
        protected virtual void OnDisable() { }
        //每帧更新
        public virtual void Update(float deltaTime) { }

        //-----------针对WindowManager的方法 (被WindowManager调用)
        public void Open()
        {
            if (transform == null)
            {
                if (Create())
                {
                    Awake();  //初始化
                }
            }
            if (!transform.gameObject.activeSelf)
            {
                UIRoot.SetParent(transform, true, selfType == WindowType.TipsWindow);
                transform.gameObject.SetActive(true);
                visible = true;
                OnEnable(); //调用激活时的事件
                OnAddListener();  //添加事件
            }
        }
        public void Close(bool isForceClose = false)
        {
            if (transform.gameObject.activeSelf)
            {
                OnRemoveListener();  //移除事件的监控
                OnDisable();  //隐藏的事件
                if (!isForceClose)  //非强制
                {
                    if (resident)
                    {
                        transform.gameObject.SetActive(false);
                        //将窗口从work区域放到recycle区域
                        UIRoot.SetParent(transform, false, false);
                    }
                    else
                    {
                        GameObject.Destroy(transform.gameObject);
                        transform = null;
                    }
                }
                else
                {
                    GameObject.Destroy(transform.gameObject);
                    transform = null;
                }
            }
            //不可见的状态
            visible = false;
        }
        public void PreLoad()
        {
            if (transform == null)
            {
                if (Create())
                {

                }
            }
        }

        //获取场景类型
        public ScenesType GetScenesType()
        {
            return scenesType;
        }
        //获取窗口类型
        public WindowType GetWindowType()
        {
            return selfType;
        }
        //获取根节点
        public Transform GetRoot()
        {
            return transform;
        }
        //是否可见
        public bool IsVisible()
        {
            return visible;
        }
        //是否常驻
        public bool IsResident()
        {
            return resident;
        }

        //--------内部---------
        public bool Create()
        {
            //资源名称为空,则无法创建
            if (string.IsNullOrEmpty(resName))
            {
                return false;
            }
            //窗体引用为空,则创建实例
            if (transform == null)
            {
                //根据资源名称加载物体
                GameObject obj = Resources.Load<GameObject>(resName);
                if (obj == null)
                {
                    Debug.LogError($"未找到UI预制件{selfType}");
                    return false;
                }
                transform = GameObject.Instantiate(obj).transform;

                transform.gameObject.SetActive(false);

                UIRoot.SetParent(transform, false, selfType == WindowType.TipsWindow);

                return true;
            }
            return true;
        }
    }
}

另一个View目录下的文件是我们的商店实例StoreWindow的管理类,WindowManager调用该类的方法Open(继承于BaseWindow的方法)来加载预置体StoreWindow,这个类还实现这个Panel的具体的方法:

namespace Game.View
{
    public class StoreWindow : BaseWindow
    {
        public StoreWindow()
        {
            resName = "UI/Window/StoreWindow";
            resident = true;
            visible = false;
            selfType = WindowType.StoreWindow;
            scenesType = ScenesType.Login;
        }
        protected override void Awake()
        {
            base.Awake();
        }
        protected override void OnAddListener()
        {
            base.OnAddListener();
        }
        protected override void OnDisable()
        {
            base.OnDisable();
        }
        protected override void OnEnable()
        {
            base.OnEnable();
        }
        protected override void OnRemoveListener()
        {
            base.OnRemoveListener();
        }
        protected override void RegisterUIEvent()
        {
            base.RegisterUIEvent();
            foreach (Button btn in buttonList)
            {
                switch (btn.name)
                {
                    case "BuyButton":
                        btn.onClick.AddListener(()=> {
                            OnBuyButton(btn);
                        });
                        break;
                }
            }

        }
        public override void Update(float deltaTime)
        {
            base.Update(deltaTime);

            //每帧监听,按下C关闭此窗口
            if (Input.GetKeyDown(KeyCode.C))
            {
                Close();
            }
        }
        private void OnBuyButton(Button btn)
        {
            Debug.Log("点击了BuyButton");

            //通过Control修改Model
            if (StoreCtrl.Instance.Sell(1))
            {
                int count = StoreCtrl.Instance.GetProp(1).Count;
                btn.transform.parent.Find("Tips").
                    GetComponent<Text>().text = "已购买倚天剑,倚天剑剩余" + count;
            }
            else
                btn.transform.parent.Find("Tips").
                    GetComponent<Text>().text = "购买失败";
        }
    }
}

上面的View目录就是这样,主要是对视图的管理,我们再来看Model目录下的StoreModel.cs,这个文件就是StoreWindow对应的数据模块:

namespace Game.Model
{
    public class StoreModel : Singleton<StoreModel>
    {
        private Dictionary<int, Prop> propDic = new Dictionary<int, Prop>();

        private Prop yiTian = new Prop(1,"倚天剑",7,300);

        public StoreModel() {
            //给商店添加商品
            Add(yiTian);
        }
        //售卖
        public bool Sell(int propId) {
            //商品数量大于0且玩家金币大于价格,这里金币我定死了301
            if (propDic[propId].Count > 0 && 301>propDic[propId].Price) {
                //商店装备数减一
                propDic[propId].Count--;
                //玩家的得到该装备

                return true;
            }
            return false;
        }

        public void Add(Prop prop)
        {
            if (!propDic.ContainsKey(prop.Id))
            {
                propDic[prop.Id] = prop;
            }
        }
        public Prop GetProp(int id)
        {
            return propDic[id];
        }
    }
}


//道具类
public class Prop {
    private int id;
    private string name;
    private int count;
    private double price;
    public int Id {
        get { return id; }
        set { id=value; }
    }
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
    public int Count
    {
        get { return count; }
        set { count = value; }
    }
    public double Price
    {
        get { return price; }
    }
    public Prop(int id, string name,int count,double price) {
        this.id = id;
        this.name = name;
        this.count = count;
        this.price = price;
    }
}

而最后,我们的View不能直接和Model交互,必须要利用中间人——Controller,下面就是StoreCtrl.cs的内容,连接了视图与数据模块,提供的方法都是对于View的接口,由View下的各个Panel的管理类控制:

namespace Game.Ctrl
{
    public class StoreCtrl : Singleton<StoreCtrl>
    {
        //给Store View分配的接口,用来给Store Model添加道具prop
        public void SaveProp(Prop prop)
        {
            StoreModel.Instance.Add(prop);
        }
        public bool Sell(int id) {
            return StoreModel.Instance.Sell(id);
        }
        public Prop GetProp(int id)
        {
            return StoreModel.Instance.GetProp(id);
        }
    }
}

效果

 

 


 

 

 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

 

发表评论