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);
}
}
}
效果
商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢