UnityUIMVC模式
传统MVC
MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。
- Model(模型) - 模型代表一个存取数据的对象。它也可以带有逻辑,在数据变化时通知控制器对View进行数据更新。
- View(视图) - 视图代表模型包含的数据的可视化。
- Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
MVC设计模式主要应用于游戏开发中的UI设计,游戏逻辑很少使用MVC模式进行设计。
来自己写三个类试试:
Model层:
主要负责管理数据,其内方法主要为初始化数据,数据更新的方法(比如升级),保存数据,数据更新事件。
using UnityEngine;
using UnityEngine.Events;
namespace HaveMVC.Model
{
/// <summary>
/// 用户数据类,主要负责管理数据,其内方法主要为初始化数据,数据更新的方法(比如升级),保存数据,数据更新事件。
/// 其中数据更新事件对应的是MVC设计模式中的Model层通知Controller层,Model作为事件源,Controller作为中间响应者。View为实际响应者
/// 作为数据更新的事件源可以选择将事件注册方法封装到model类中。
/// </summary>
public class PlayerModel
{
/// <summary>
/// 玩家等级
/// </summary>
private int _playerLev;
public int PlayerLev => _playerLev;
/// <summary>
/// 玩家攻击力
/// </summary>
private int _playerAtk;
public int PlayerAtk => _playerAtk;
/// <summary>
/// 在单人游戏中,玩家数据是唯一的,所以这里就将玩家数据单例化,也可以让其继承单例模式基类
/// </summary>
private static PlayerModel _playerData;
public static PlayerModel PlayerData
{
get
{
if (_playerData == null)
{
_playerData = new PlayerModel();
_playerData.Init();
}
return _playerData;
}
}
/// <summary>
/// 数据更新事件,当保存数据执行完毕后调用此事件,并传出,此步也可以由事件中心类来处理
/// </summary>
public event UnityAction<PlayerModel> DataEventHandler;
/// <summary>
/// 数据初始化
/// 需要注意:PlayerPrefs的Get方法当使用默认值时,PlayerPrefs会自动Set保存一次默认值
/// </summary>
private void Init()
{
//举个例子
_playerAtk = PlayerPrefs.GetInt("_playerAtk", 10);
}
/// <summary>
/// 保存数据,每次保存数据被调用说明数据被改变了一次,所以需要调用数据更新事件
/// </summary>
private void SaveData()
{
//举个例子
PlayerPrefs.SetInt("_playerAtk", _playerAtk);
OnUpdateInfo(_playerData);
}
/// <summary>
/// 数据更新方法,这里是升级方法
/// </summary>
public void LevUp()
{
_playerLev += 1;
_playerAtk += _playerLev;
SaveData();
}
/// <summary>
/// 将事件注册的方法封装在事件源内可以减少外部注册事件时代码的复杂度
/// </summary>
/// <param name="func">响应函数</param>
public void AddListener(UnityAction<PlayerModel> func)
{
DataEventHandler += func;
}
private void OnUpdateInfo(PlayerModel playerModel)
{
if (DataEventHandler != null)
{
DataEventHandler(playerModel);
}
}
}
}
View层:
主面板View,View类主要负责处理显示内容,保证显示的实时更新。
using HaveMVC.Model;
using UnityEngine;
using UnityEngine.UI;
namespace HaveMVC.View
{
/// <summary>
/// 主面板View,View类主要负责处理显示内容,保证显示的实时更新。
/// 所以View会有一个实际响应函数UpdateData。
/// 为什么叫实际响应函数?
/// 因为实际上Controller层的响应函数只是一个中间商,Controller响应函数会进一步分发事件。
/// </summary>
public class MainPanelView : MonoBehaviour
{
//玩家等级
public Text playerLev;
private void Start()
{
Init();
}
/// <summary>
/// 数据初始化,初始化也可以使用UpdateData,只不过第一次要传入静态属性PlayerData
/// </summary>
private void Init()
{
//这里举个例子
playerLev.text = PlayerModel.PlayerData.PlayerLev.ToString();
}
/// <summary>
/// 数据更新函数,实际响应函数,由Controller中间响应者来分发
/// </summary>
/// <param name="playerModel">玩家模型参数,由Model传给Controller,再传给View</param>
public void UpdateData(PlayerModel playerModel)
{
//这里举个例子
playerLev.text = playerModel.PlayerLev.ToString();
}
}
}
Controller层:
负责处理主面板的交互逻辑,包括主面板显隐,主面板点击事件,数据更新事件中间商。
using HaveMVC.Model;
using HaveMVC.View;
using UnityEngine;
using UnityEngine.UI;
namespace HaveMVC.Controller
{
/// <summary>
/// 主面板Controller类,负责处理主面板的交互逻辑,包括主面板显隐,主面板点击事件,数据更新事件中间商
/// </summary>
public class MainPanelController : MonoBehaviour
{
public Button btnRole;
/// <summary>
/// 绑定对应的View
/// </summary>
private MainPanelView _mainPanelView;
/// <summary>
/// 无论什么面板在游戏中应当是为唯一的,所以这里应当有一个单例标识,防止创建多个相同面板
/// </summary>
private static MainPanelController _mainPanelController;
public static MainPanelController MainController
{
get
{
return _mainPanelController;
}
}
/// <summary>
/// 注册事件,寻找对应的View组件
/// </summary>
private void Start()
{
PlayerModel.PlayerData.AddListener(OnUpdateDataFunc);
btnRole.onClick.AddListener(OnBtnRoleClick);
_mainPanelView = GetComponent<MainPanelView>();
}
/// <summary>
/// 显示面板方法
/// </summary>
public static void Show()
{
//此单例标识为空说明当前场景还没有此面板,创建即可
if (_mainPanelController == null)
{
//资源读取面板资源
GameObject res = Resources.Load<GameObject>("UI/MainPanel");
//面板实例化
GameObject go = Instantiate(res);
//设置面板显示
go.SetActive(true);
//将面板的父类设置为Canvas
go.transform.SetParent(GameObject.Find("Canvas").transform);
//获得Controller组件,此后将不会进入这段逻辑
_mainPanelController = go.GetComponent<MainPanelController>();
}
_mainPanelController.gameObject.SetActive(true);
}
/// <summary>
/// 隐藏面板,面板失活
/// </summary>
public static void Hide()
{
if (_mainPanelController != null)
{
_mainPanelController.gameObject.SetActive(false);
}
}
/// <summary>
/// 中间响应函数,负责调用View中的响应函数
/// </summary>
/// <param name="playerModel"></param>
private void OnUpdateDataFunc(PlayerModel playerModel)
{
_mainPanelView.UpdateData(playerModel);
}
/// <summary>
/// 添加点击事件
/// </summary>
private void OnBtnRoleClick()
{
}
}
}
MVP模式
传统MVC结构中Model层和View层存在联系,即View实际是直接从Model中读取的数据,如果我们想要断开Model和View的联系,我们就需要用一个MVC的变式:MVP模式
MVP的主要思想就是:View层不需要再关心自己的数据更新,而是由Presenter(之前的Controller)来负责对应View的数据更新。简单来说,上面的Controller将直接为对应View的属性赋值,而不需要第二次传递事件。
MP模式
MVP的变式MP模式:
P即是面板,在Unity中我们的UI大了来说就是由一个一个的面板构成的,面板上的控件都会以面板子对象的形式呈现。前面我们所介绍的MVC也好MVP也好,View都是在寻找控件,P/C都是在处理交互逻辑和事件响应。所以当我们将View和Controller/Presenter融成一个时,我们的P面板类也需要做这几件事:
- 寻找控件
- 处理交互逻辑
- 事件响应
其中寻找控件和事件响应逻辑框架都是一样的,寻找控件简单来说就是要遍历面板的所有子对象,而事件响应中的点击事件则可以在这个过程中添加。
所以这样来想我们就可以提出一个面板基类:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/*
Editor:
Version:
The last time for modification:
Time for Creation:
*/
namespace CsharpBaseModule.UI
{
/// <summary>
/// UI面板基类 负责封装面板的子类查找,控件事件添加操作和面板显隐。
/// </summary>
public class BasePanel : MonoBehaviour
{
/// <summary>
/// 存储所有的控件
/// </summary>
private Dictionary<string, List<UIBehaviour>> UiDic = new Dictionary<string, List<UIBehaviour>>();
/// <summary>
/// 子类的此处重写来选择具体查找哪类UI控件
/// </summary>
protected virtual void Awake()
{
FindChildrenWidgets<Button>();
//。。。。。。
}
/// <summary>
/// 显示面板时执行的逻辑
/// </summary>
public virtual void Show()
{
}
/// <summary>
/// 隐藏面板时执行的逻辑
/// </summary>
public virtual void Hide()
{
}
/// <summary>
/// 获得某个控件
/// </summary>
/// <param name="widgetName">控件名字</param>
/// <typeparam name="T">控件类型</typeparam>
/// <returns></returns>
public T GetWidget<T>(string widgetName) where T : UIBehaviour
{
if (UiDic.ContainsKey(widgetName))
{
for (int i = 0; i < UiDic[widgetName].Count; i++)
{
if (UiDic[widgetName][i] is T)
{
return UiDic[widgetName][i] as T;
}
}
}
return null;
}
/// <summary>
/// 寻找所有的子类控件,加入字典统一管理并添加UI事件
/// </summary>
/// <typeparam name="T">控件类型</typeparam>
private void FindChildrenWidgets<T>() where T : UIBehaviour
{
//查找子类中所有的此类行的控件
T[] widgets = GetComponentsInChildren<T>();
//循环添加到控件字典中
for (int i = 0; i < widgets.Length; i++)
{
string objName = widgets[i].gameObject.name;
if (!UiDic.ContainsKey(objName))
{
UiDic.Add(objName,new List<UIBehaviour>());
}
UiDic[objName].Add(widgets[i]);
//此步采用lambda的闭包操作来实现无参函数调用有参函数
if (widgets[i] is Button)
{
(widgets[i] as Button).onClick.AddListener(() =>
{
OnClick(objName);
});
}
else if (widgets[i] is Toggle)
{
(widgets[i] as Toggle).onValueChanged.AddListener((value) =>
{
OnValueChanged(objName, value);
});
}
}
}
/// <summary>
/// 统一提供的点击事件接口,子类只需要重写此方法并使用switch判断名字即可实现不同按钮的点击事件
/// 所有的点击事件都在查找控件的时候就已经添加了
/// </summary>
/// <param name="name">按钮名字</param>
protected virtual void OnClick(string name)
{
}
/// <summary>
/// 和OnClick同理,统一提供的Toggle事件接口,子类只需要重写此方法并使用switch判断名字即可实现不同按钮的点击事件
/// 所有的点击事件都在查找控件的时候就已经添加了
/// </summary>
/// <param name="name">Toggle名字</param>
/// <param name="value">toggle布尔值</param>
protected virtual void OnValueChanged(string name, bool value)
{
}
}
}
此面板基类囊括了控件绑定,点击事件绑定,面板显隐,所以继承该基类的Panel只需要自己处理数据更新事件即可。
MPE模式
MPE模式算是个人的一种改良,就是将M和P之间的更新事件交由事件中心EventCenter来集中处理。