什么是MVC
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
如图:
Model是数据模型,用来存储数据信息的。
View是视图界面,也就是平时我们看到的UI界面,用于显示界面信息的。
Controller是控制器,用于处理逻辑代码的。
当需要在界面上更新金币的显示时,用户发送请求,即SendEvent(),通过参数传递数据,通过名字进行标识是哪个view发过来的,事先要将view的名字与相应的controller进行绑定,然后对应的controller接收到发送过来的数据,进行相应的逻辑处理,即进行金币更新,通知view进行进行金币更新后的显示。
Model、View、Controller都定义成抽象类,因为它们都有一个抽象的属性或方法,定义成抽象类的话,子类继承自父类,子类必须实现父类的抽象属性或者方法,因为View和Model都需要一个名字来标识,区别出来是哪个View,然后注册这个View,将该View的名字和该View进行绑定。
Model代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class Model
{
//名字标识
public abstract string Name { get; }
/// <summary>
/// 发送事件
/// </summary>
protected void SendEvent(string eventName,object data=null)
{
MVC.SendEvent(eventName, data);
}
}
View代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class View : MonoBehaviour
{
//名字标识
public abstract string Name { get; }
/// <summary>
/// 事件关心列表
/// </summary>
[HideInInspector]
public List<string> attentionList = new List<string>();
public virtual void RegisterAttentionEvent()
{
}
/// <summary>
/// 处理事件
/// </summary>
public abstract void HandleEvent(string name, object data);
/// <summary>
/// 发送事件
/// </summary>
protected void SendEvent(string eventName, object data = null)
{
MVC.SendEvent(eventName, data);
}
protected T GetModel<T>()
where T:Model
{
return MVC.GetModel<T>() as T;
}
}
Controller代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public abstract class Controller
{
public abstract void Execute(object data);
protected T GetModel<T>()
where T : Model
{
return MVC.GetModel<T>() as T;
}
protected T GetView<T>()
where T : View
{
return MVC.GetView<T>() as T;
}
/// <summary>
/// 注册模型
/// </summary>
protected void RegisterModel(Model model)
{
MVC.RegisterModel(model);
}
/// <summary>
/// 注册视图
/// </summary>
protected void RegisterView(View view)
{
MVC.RegisterView(view);
}
/// <summary>
/// 注册controller
/// </summary>
protected void RegisterController(string eventName,Type controllerType)
{
MVC.RegisterController(eventName, controllerType);
}
}
在View中,有个string类型的attentionList,它是用来存储需要View执行的事件的名字,View中的public virtual void RegisterAttentionEvent()是虚方法,可重写,重写的时候可以在里面添加关心事件,即比如attentionList.Add(Consts.E_UpdateDis);然后public abstract void HandleEvent(string name, object data)是抽象方法,必须重写,当需要发送请求时,即SendEvent(eventName,data),首先是执行Controller里面的逻辑代码,然后就是执行View中重写的HandleEvent()方法,在HandleEvent方法中,就可以执行你想要的操作了。
在Controller中,不需要定义名字进行标识,到时候会在一个Consts类中定义一堆的Controller的名字进行标识,然后通过那个名字,和它对应的Controller的类型进行绑定,通过反射即可拿到该Controller的实例。
public abstract void Execute(object data)是必须重写的,因为每个Controller中进行的逻辑处理都是不一样的。每次SendEvent后,其中的参数名字和它对应的Controller对应上了,就会执行该Controller。由于继承了Controller的子类中可能会GetModel、GetView、RegisterModel、RegisterView、RegisterController,所以提前写好对应的方法,简化操作。
然后,需要一个脚本统一的管理Model、View和Controller,所以新建一个名为MVC的类统一管理它们。
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class MVC
{
//资源
public static Dictionary<string, Model> Models = new Dictionary<string, Model>();
public static Dictionary<string, View> Views = new Dictionary<string, View>();
public static Dictionary<string, Type> CommanMap = new Dictionary<string, Type>();
/// <summary>
/// 注册view
/// </summary>
/// <param name="view"></param>
public static void RegisterView(View view)
{
//防止view重复注册
if(Views.ContainsKey(view.Name))
{
Views.Remove(view.Name);
}
view.RegisterAttentionEvent();
Views[view.Name] = view;
}
/// <summary>
/// 注册model
/// </summary>
/// <param name="model"></param>
public static void RegisterModel(Model model)
{
Models[model.Name] = model;
}
/// <summary>
/// 注册controller
/// </summary>
/// <param name="eventName"></param>
/// <param name="controllerType"></param>
public static void RegisterController(string eventName,Type controllerType)
{
CommanMap[eventName] = controllerType;
}
/// <summary>
/// 获取model
/// </summary>
/// <returns></returns>
public static T GetModel<T>()
where T:Model
{
foreach (var m in Models.Values)
{
if(m is T)
{
return (T)m;
}
}
return null;
}
/// <summary>
/// 获取view
/// </summary>
/// <returns></returns>
public static T GetView<T>()
where T : View
{
foreach (var v in Views.Values)
{
if (v is T)
{
return (T)v;
}
}
return null;
}
public static void SendEvent(string eventName,object data=null)
{
//controller执行
if(CommanMap.ContainsKey(eventName))
{
Type t = CommanMap[eventName];
Controller c = Activator.CreateInstance(t) as Controller;
c.Execute(data);
}
//view处理
foreach (var v in Views.Values)
{
if(v.attentionList.Contains(eventName))
{
//执行
v.HandleEvent(eventName, data);
}
}
}
}
Model和View都是通过它们自身的名字和自身绑定在一起,通过字典存储起来,对于Controller的话,由于Controller自身没有名字,所以在Consts类中自己定义名字给它,然后将这个名字和这个Controller的类型绑定在一起。在注册View的时候,public static void RegisterView(View view),是进入了哪个场景时就注册哪个场景的View,由于可能重复进入一个场景,而原本却已经存在了,所以在注册View之前,进行判断,如果字典里已经存在这个View了,那就先移除该View,再进行重新注册,而注册之前,也先进行关心事件的注册,即view.RegisterAttentionEvent();
Model和Controller都是直接注册了,不需要进行判断,因为View和Controller都是一开始就只注册一次而已,以后不会有重复注册的可能。
对于发送请求的代码public static void SendEvent(string eventName,object data=null),需要传入事件的名字,object data=null表示传入的数据是object类型的,=null表示可不传,即该参数可为空。
Type t = CommanMap[eventName];
Controller c = Activator.CreateInstance(t) as Controller;
通过Activator反射类可获取到该Controller,然后执行该Controller中的c.Execute(data)方法,接着就是执行View中的HandleEvent(eventName, data)方法。
以上就搭建好了整个MVC框架,接下来就是如何使用该套MVC框架了。
首先在一个无摄像机、无灯光的0号场景中新建一个空物体,可命名为Game,然后挂载一个同名的脚本。
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.SceneManagement;
[RequireComponent(typeof(ObjectPool))] //这三个是在该物体上添加这三个脚本
[RequireComponent(typeof(Sound))] //如需了解我这两个ObjectPool和Sound是什么代码的,可以移步去https://blog.csdn.net/weixin_43839583/article/details/103408332了解Sound
[RequireComponent(typeof(StaticData))] //移步去https://blog.csdn.net/weixin_43839583/article/details/103405831了解ObjectPool
public class Game : MonoSingleton<Game>
{
//全局访问
[HideInInspector]
public ObjectPool objectPool;
[HideInInspector]
public Sound sound;
[HideInInspector]
public StaticData staticData; //先定义这个类出来,现在是空类,但后面可能会存些静态数据
private void Start()
{
DontDestroyOnLoad(gameObject);//表示这个游戏物体在跳转场景时不会被销毁。
objectPool = ObjectPool.Instance;
sound = Sound.Instance;
staticData = StaticData.Instance;
//初始化注册StartUpController,在该Controller中又注册其他所有的Controller
RegisterController(Consts.E_StartUp, typeof(StartUpController));
//游戏启动,执行StartUpController中的方法,即注册其他所有的Controller方法。
SendEvent(Consts.E_StartUp);
//跳转场景
Game.Instance.LoadLevel(1);
}
public void LoadLevel(int level)
{
//发送退出场景事件
ScenesArgs e = new ScenesArgs();
//获取当前场景索引
e.scenesIndex = SceneManager.GetActiveScene().buildIndex;
SendEvent(Consts.E_ExitScenes, e);
//发送加载新场景事件
//SceneManager.LoadScene(level, LoadSceneMode.Single);
StartCoroutine(StartLoadNewScenes(level));
}
/// <summary>
/// 进入新场景
/// </summary>
IEnumerator StartLoadNewScenes(int level)
{
Debug.Log("进入新场景:" + level);
AsyncOperation op = SceneManager.LoadSceneAsync(level);
while(!op.isDone)
{
yield return 0;
}
//发送进入场景事件
ScenesArgs e = new ScenesArgs();
//获取当前场景索引
e.scenesIndex = level;
SendEvent(Consts.E_EnterScenes, e);
}
/// <summary>
/// 发送事件
/// </summary>
protected void SendEvent(string eventName, object data = null)
{
MVC.SendEvent(eventName, data);
}
/// <summary>
/// 注册controller
/// </summary>
protected void RegisterController(string eventName, Type controllerType)
{
MVC.RegisterController(eventName, controllerType);
}
}
为什么不直接使用加载场景的方法呢?因为我们需要在加载场景之前,把当前场景(退出的场景)的下标发给对应的退出场景的Controller进行执行相应的操作,就是在这个Controller中,判断如果这个场景是游戏中的场景,则要执行清空对象池的操作,就是将所有用到对象池技术的物体都隐藏,不这样做的话,会出问题。
然后通过异步加载场景,这样可以获取到加载场景的进度,因为我们要在完全加载完场景的时候发送进入场景的请求,这个时候,EnterScenesController会接收到进入了哪个场景的消息,然后执行注册该场景中相应的View。
到此为止,MVC的使用也已经结束了,在UI中,我们要改变UI界面,就要先发送事件SendEvent(),然后Controller中进行相应的逻辑处理,或者是添加关心事件attentionList,执行View中关心事件的处理。
平时要多多使用MVC框架才比较好的理解这套框架该怎么使用,MVC框架可以有效的降低耦合度,是高内聚、低耦合的一种优化。