基本概念
MVC全名是Model View Controller
是模型(model)-视图(view)-控制器(controller)的缩写
是一种软件设计规范,用一种业务逻辑、数据、界面显示 分离的方法组织代码
将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。
MVC在游戏开发中不是必备的,它主要用于开发游戏UI系统逻辑
前期准备
接下来要实现一个小的UI面板,分别实现不使用MVC框架和使用MVC框架的代码,以此作为对比。
Canvas设置
非MVC框架实现
主面板逻辑
MainPanel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class MainPanel : MonoBehaviour
{
//1.获得控件
public Text txtName;
public Text txtLev;
public Text txtMoney;
public Text txtGem;
public Text txtPower;
public Button btnRole;
private static MainPanel panel;
//2.添加事件
//3.更新信息
//4.动态显隐
//使用静态方法,让NormalMain能够调用
public static void ShowMe(){
if(panel == null){
//实例化面板对象
GameObject res = Resources.Load<GameObject>("UI/MainPanel");
GameObject obj = Instantiate(res);
//设置父对象
obj.transform.SetParent(GameObject.Find("Canvas").transform,false);
panel = obj.GetComponent<MainPanel>();
}
//如果隐藏形式是setacive,则显示也要set
panel.gameObject.SetActive(true);
//显示完面板 更新
panel.UpdateInfo();
}
public static void HideMe(){
if(panel != null){
//一. 直接删
// Destroy(panel.gameObject);
// panel = null;
//二. 隐藏
panel.gameObject.SetActive(false);
}
}
// Start is called before the first frame update
void Start()
{
//2.添加事件
btnRole.onClick.AddListener(ClickBtnRole);
}
private void ClickBtnRole(){
//打开角色面板的逻辑
Debug.Log("按钮点击");
}
//3.更新信息
public void UpdateInfo(){
//获取玩家数据 更新玩家信息
//获取玩家数据的方式 1.网络请求 2.Json 3.xml 4.2进制 5.PlayerPrefs公共类
//通过PlayerPrefs来获取本地存储的玩家信息 更新到界面上
txtName.text = PlayerPrefs.GetString("PlayerName","阿喆不想学习");
txtLev.text = "LV." + PlayerPrefs.GetInt("PlayerLev",1).ToString();
txtMoney.text = PlayerPrefs.GetInt("PlayerMoney",999).ToString();
txtGem.text = PlayerPrefs.GetInt("PlayerGem",888).ToString();
txtPower.text = PlayerPrefs.GetInt("PlayerPower",10).ToString();
}
}
NormalMain.cs
void Update()
{
if(Input.GetKeyDown(KeyCode.M)){
//显示主面板
MainPanel.ShowMe();
}
else if(Input.GetKeyDown(KeyCode.N)){
//隐藏主面板
MainPanel.HideMe();
}
}
角色面板逻辑
RolePanel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class RolePanel : MonoBehaviour
{
//1.获得控件
public Text txtLev;
public Text txtHp;
public Text txtAtk;
public Text txtDef;
public Text txtCrit;
public Text txtMiss;
public Text txtLuck;
public Button btnClose;
public Button btnLevUp;
private static RolePanel panel;
//2.添加事件
//3.更新信息
//4.动态显隐
// Start is called before the first frame update
public static void ShowMe(){
if(panel == null){
//实例化面板对象
GameObject res = Resources.Load<GameObject>("UI/RolePanel");
GameObject obj = Instantiate(res);
//设置父对象
obj.transform.SetParent(GameObject.Find("Canvas").transform,false);
panel = obj.GetComponent<RolePanel>();
}
//如果隐藏形式是setacive,则显示也要set
panel.gameObject.SetActive(true);
//显示完面板 更新
panel.UpdateInfo();
}
public static void HideMe(){
if(panel != null){
//一. 直接删
// Destroy(panel.gameObject);
// panel = null;
//二. 隐藏
panel.gameObject.SetActive(false);
}
}
void Start()
{
btnClose.onClick.AddListener(()=>{
HideMe();
});
btnLevUp.onClick.AddListener(()=>{
//升级就是数据更新
//这里就是获取本地数据
int lev = PlayerPrefs.GetInt("PlayerLev",1);
int hp= PlayerPrefs.GetInt("PlayerHp",100);;
int def= PlayerPrefs.GetInt("PlayerDef",10);
int atk= PlayerPrefs.GetInt("PlayerAtk",20);
int crit= PlayerPrefs.GetInt("PlayerCrit",20);
int miss= PlayerPrefs.GetInt("PlayerMiss",10);
int luck= PlayerPrefs.GetInt("PlayerLuck",40);
//然后根据升级规则去改变他
lev += 1;
hp += lev;
atk += lev;
def += lev;
crit += lev;
miss += lev;
luck += lev;
//存起来
PlayerPrefs.SetInt("PlayerLev",lev);
PlayerPrefs.SetInt("PlayerHp",hp);
PlayerPrefs.SetInt("PlayerAtk",atk);
PlayerPrefs.SetInt("PlayerDef",def);
PlayerPrefs.SetInt("PlayerCrit",crit);
PlayerPrefs.SetInt("PlayerMiss",miss);
PlayerPrefs.SetInt("PlayerLuck",luck);
//同步更新面板上的数据
UpdateInfo();
//更新主面板的数据
MainPanel.Panel.UpdateInfo();
});
}
//更新信息
public void UpdateInfo(){
txtLev.text = "LV." + PlayerPrefs.GetInt("PlayerLev",1).ToString();
txtHp.text = PlayerPrefs.GetInt("PlayerHp",100).ToString();
txtAtk.text = PlayerPrefs.GetInt("PlayerAtk",20).ToString();
txtDef.text = PlayerPrefs.GetInt("PlayerDef",10).ToString();
txtCrit.text = PlayerPrefs.GetInt("PlayerCrit",20).ToString();
txtMiss.text = PlayerPrefs.GetInt("PlayerMiss",10).ToString();
txtLuck.text = PlayerPrefs.GetInt("PlayerLuck",40).ToString();
}
}
MainPanel更新
public static MainPanel Panel{
get{
return panel;
}
}
private void ClickBtnRole(){
//打开角色面板的逻辑
RolePanel.ShowMe();
}
MVC框架实现
Model数据脚本
using System.Collections;
using System.Collections.Generic;
using System.Runtime.ConstrainedExecution;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 作为一个唯一的数据模型
/// 一般情况下 要不自己是个单例模式对象
/// 要么自己存在在一个单例模式中
/// </summary>
public class PlayerModel
{
//数据内容
private string playerName;
//用属性是为了能让外部得到它但不能改变他
public string PlayerName{
get{
return playerName;
}
}
private int lev;
public int Lev{
get{
return lev;
}
}
private int money;
public int Money{
get{
return money;
}
}
private int gem;
public int Gem{
get{
return gem;
}
}
private int power;
public int Power{
get{
return power;
}
}
private int hp;
public int Hp{
get{
return hp;
}
}
private int atk;
public int Atk{
get{
return atk;
}
}
private int def;
public int Def{
get{
return def;
}
}
private int crit;
public int Crit{
get{
return crit;
}
}
private int miss;
public int Miss{
get{
return miss;
}
}
private int luck;
public int Luck{
get{
return luck;
}
}
//通知外部更新的事件
//通过它来与外部建立联系 而不是直接获取外部的面板
private event UnityAction<PlayerModel> updateEvent;
//在外部第一次获取这个数据 如何获取
//通过单例模式 来达到数据的唯一性 和数据获取
private static PlayerModel data = null;
public static PlayerModel Data{
get{
if(data == null){
data = new PlayerModel();
data.Init();
}
return data;
}
}
//数据相关的操作
// 初始化
public void Init(){
playerName = PlayerPrefs.GetString("PlayerName","阿喆不想学习");
lev = PlayerPrefs.GetInt("PlayerLev",1);
money = PlayerPrefs.GetInt("PlayerMoney",9999);
gem = PlayerPrefs.GetInt("PlayerGem",8888);
power = PlayerPrefs.GetInt("PlayerPower",99);
hp= PlayerPrefs.GetInt("PlayerHp",100);;
def= PlayerPrefs.GetInt("PlayerDef",10);
atk= PlayerPrefs.GetInt("PlayerAtk",20);
crit= PlayerPrefs.GetInt("PlayerCrit",20);
miss= PlayerPrefs.GetInt("PlayerMiss",10);
luck= PlayerPrefs.GetInt("PlayerLuck",40);
}
// 更新 在这里是升级
public void LevUp(){
//升级 改变内容
lev += 1;
hp += lev;
atk += lev;
def += lev;
crit += lev;
miss += lev;
luck += lev;
//改变后保存
SaveData();
}
// 保存
public void SaveData(){
//把这些数据内容 存储到本地
PlayerPrefs.SetString("PlayerName",playerName);
PlayerPrefs.SetInt("PlayerLev",lev);
PlayerPrefs.SetInt("PlayerMoney",money);
PlayerPrefs.SetInt("PlayerGem",gem);
PlayerPrefs.SetInt("PlayerPower",power);
PlayerPrefs.SetInt("PlayerHp",hp);
PlayerPrefs.SetInt("PlayerAtk",atk);
PlayerPrefs.SetInt("PlayerDef",def);
PlayerPrefs.SetInt("PlayerCrit",crit);
PlayerPrefs.SetInt("PlayerMiss",miss);
PlayerPrefs.SetInt("PlayerLuck",luck);
UpdateInfo();
}
public void AddEventListener(UnityAction<PlayerModel> function){
updateEvent += function;
}
public void RemoveEventListener(UnityAction<PlayerModel> function){
updateEvent -= function;
}
//通知外面更新数据的方法
private void UpdateInfo(){
//找到对应的 使用数据的脚本 去更新数据
if(updateEvent != null){
updateEvent(this);
}
}
}
View界面脚本
MainView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class MainView : MonoBehaviour
{
//1.找控件
public Text txtName;
public Text txtLev;
public Text txtMoney;
public Text txtGem;
public Text txtPower;
public Button btnRole;
public Button btnSkill;
//2.提供面板更新的方法给外部
public void UpdateInfo(PlayerModel data){
txtName.text = data.PlayerName;
txtLev.text = "LV." + data.Lev;
txtMoney.text = data.Money.ToString();
txtGem.text = data.Gem.ToString();
txtPower.text = data.Power.ToString();
}
}
RoleView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class RoleView : MonoBehaviour
{
//1.找控件
public Text txtLev;
public Text txtHp;
public Text txtAtk;
public Text txtDef;
public Text txtCrit;
public Text txtMiss;
public Text txtLuck;
public Button btnClose;
public Button btnLevUp;
//2.提供面板更新的相关方法给外部
public void UpdateInfo(PlayerModel data){
txtLev.text = "LV." + data.Lev;
txtHp.text = data.Hp.ToString();
txtAtk.text = data.Atk.ToString();
txtDef.text = data.Def.ToString();
txtCrit.text = data.Crit.ToString();
txtMiss.text = data.Miss.ToString();
txtLuck.text = data.Luck.ToString();
}
}
Controller业务逻辑
MainController
using System.Collections;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;
/// <summary>
/// Controller要处理的东西 就是业务逻辑
/// </summary>
public class MainController : MonoBehaviour
{
//能够在Controller中得到界面才行
private MainView mainView;
//面板之间的交互都是通过Controller来实现,我们不想让mainView也变成静态被其它访问,
//因此可以设置个静态的 Controller,因为Controller也要被外部访问
private static MainController controller = null;
public static MainController Controller{
get{
return controller;
}
}
//1.界面的显隐
public static void ShowMe(){
if(controller == null){
//实例化面板对象
GameObject res = Resources.Load<GameObject>("UI/MainPanel");
GameObject obj = Instantiate(res);
//设置父对象
obj.transform.SetParent(GameObject.Find("Canvas").transform,false);
controller = obj.GetComponent<MainController>();
}
//如果隐藏形式是setacive,则显示也要set
controller.gameObject.SetActive(true);
}
public static void HideMe(){
if(controller != null){
controller.gameObject.SetActive(false);
}
}
private void Start(){
//获取同样挂载在一个对象上的 view脚本
mainView = this.GetComponent<MainView>();
//第一次更新
mainView.UpdateInfo(PlayerModel.Data);
//2.界面 事件的监听 来处理对应的业务逻辑
mainView.btnRole.onClick.AddListener(ClickRoleBtn);
//PlayerModel.Data.AddEventListener(mainView.UpdateInfo);
PlayerModel.Data.AddEventListener(UpdateInfo);
}
private void ClickRoleBtn(){
RoleController.ShowMe();
}
//3. 界面的更新
private void UpdateInfo(PlayerModel data)
{
if(mainView != null){
mainView.UpdateInfo(data);
}
}
private void OnDestroy() {
PlayerModel.Data.RemoveEventListener(UpdateInfo);
}
}
RoleController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
public class RoleController : MonoBehaviour
{
private RoleView roleView;
private static RoleController controller = null;
public static RoleController Controller{
get{
return controller;
}
}
public static void ShowMe(){
if(controller == null){
//实例化面板对象
GameObject res = Resources.Load<GameObject>("UI/RolePanel");
GameObject obj = Instantiate(res);
//设置父对象
obj.transform.SetParent(GameObject.Find("Canvas").transform,false);
controller = obj.GetComponent<RoleController>();
}
//如果隐藏形式是setacive,则显示也要set
controller.gameObject.SetActive(true);
}
public static void HideMe(){
if(controller != null){
controller.gameObject.SetActive(false);
}
}
void Start()
{
roleView = this.GetComponent<RoleView>();
//第一次更新面板
roleView.UpdateInfo(PlayerModel.Data);
roleView.btnClose.onClick.AddListener(ClickCloseBtn);
roleView.btnLevUp.onClick.AddListener(ClickLevUpBtn);
//PlayerModel.Data.AddEventListener(roleView.UpdateInfo);
PlayerModel.Data.AddEventListener(UpdateInfo);
}
private void ClickCloseBtn(){
HideMe();
}
private void ClickLevUpBtn(){
//通过数据模块 进行升级 达到数据改变
PlayerModel.Data.LevUp();
}
// Update is called once per frame
private void UpdateInfo(PlayerModel data)
{
if(roleView != null){
roleView.UpdateInfo(data);
}
}
private void OnDestroy() {
PlayerModel.Data.RemoveEventListener(UpdateInfo);
}
}
对比与总结
好处
1.各司其职,互不干涉 --编程思路更清晰
2.有利开发中的分工 -- 多人协同开发时,同步并行
3.有利于组件重用 -- 项目换皮时,功能变化小时,提高开发效率
坏处
1.增加了程序文件的体量 -- 脚本由一变三
2.增加了结构的复杂性 --对于不清楚MVC原理的人不友好
3.效率相对较低 -- 对象之间的相互跳转,始终伴随着一定开销
扩展
MVC的美中不足
M和V之间存在着联系,也就是数据和界面之间存在着耦合性,当数据结构改变时会牵扯界面逻辑随之改动。
在MVC中当需求变化时,需要维护的对象数量会增加
如这一段
改变了PlayerModel中的变量的话,则MainView里的这个函数可能也要调整。
MVX
数据和界面是必备的内容
我们可以通过改变X元素来优化原本的MVC
也就是改变联系和处理M(数据)和V(界面)的方式
MVP:切断View和Model的耦合,让Presenter处理一切
MVVM:MVP的升级版,让ViewModel和V进行双向数据绑定,更新VM等同于更新V,反之同理
MVE:用EventCenter事件中心来分发消息
学习MVX的目的
不要拘泥于框架结构和设计模式要找到一个适合自己项目的
一个稳定的,有序的,能满足项目需求的实现方式
MVP
全称为模型(Model)-视图(View)一主持人(Presenter)
Model提供数据,View负责界面,Presenter负责逻辑的处理
它是MVC的一种变式,是针对MVC中M和V存在耦合的优化
与MVC的区别:
在MVC中View会直接从Model中读取数据而不是通过 Controller。
而在MVP中View并不直接使用Model,它们之间的通信是通过Presenter来进行的,所有的交互都发生在Presenter内部。
同样是上面的项目
MVP_MainView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class MVP_MainView : MonoBehaviour
{
//1.找控件
public Text txtName;
public Text txtLev;
public Text txtMoney;
public Text txtGem;
public Text txtPower;
public Button btnRole;
public Button btnSkill;
// //2.提供面板更新的方法给外部
// public void UpdateInfo(string name, int lev, int money, int gem, int power){
// txtName.text = name;
// txtLev.text = "LV." + lev;
// txtMoney.text = money.ToString();
// txtGem.text = gem.ToString();
// txtPower.text = power.ToString();
// }
}
MVP_RoleView.cs
public class MVP_RoleView : MonoBehaviour
{
//1.找控件
public Text txtLev;
public Text txtHp;
public Text txtAtk;
public Text txtDef;
public Text txtCrit;
public Text txtMiss;
public Text txtLuck;
public Button btnClose;
public Button btnLevUp;
//2.提供面板更新的相关方法给外部
//方法可选 到时候可以直接在P里面通过访问控件 去修改
}
MainPresenter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainPresenter : MonoBehaviour
{
//能够在Presenter中得到界面才行
private MVP_MainView mainView;
//面板之间的交互都是通过Controller来实现,我们不想让mainView也变成静态被其它访问,
//因此可以设置个静态的 Controller,因为Controller也要被外部访问
private static MainPresenter presenter = null;
public static MainPresenter Presenter{
get{
return presenter;
}
}
//1.界面的显隐
public static void ShowMe(){
if(presenter == null){
//实例化面板对象
GameObject res = Resources.Load<GameObject>("UI/MainPanel");
GameObject obj = Instantiate(res);
//设置父对象
obj.transform.SetParent(GameObject.Find("Canvas").transform,false);
presenter = obj.GetComponent<MainPresenter>();
}
//如果隐藏形式是setacive,则显示也要set
presenter.gameObject.SetActive(true);
}
public static void HideMe(){
if(presenter != null){
presenter.gameObject.SetActive(false);
}
}
private void Start(){
//获取同样挂载在一个对象上的 view脚本
mainView = this.GetComponent<MVP_MainView>();
//第一次更新
//mainView.UpdateInfo(PlayerModel.Data);
//通过P自己的更新方法来更新
UpdateInfo(PlayerModel.Data);
//2.界面 事件的监听 来处理对应的业务逻辑
mainView.btnRole.onClick.AddListener(ClickRoleBtn);
//PlayerModel.Data.AddEventListener(mainView.UpdateInfo);
PlayerModel.Data.AddEventListener(UpdateInfo);
}
private void ClickRoleBtn(){
//RoleController.ShowMe();
RolePresenter.ShowMe();
}
//3. 界面的更新
private void UpdateInfo(PlayerModel data)
{
if(mainView != null){
//mainView.UpdateInfo(data);
//以前是把数据M传到V中去更新,现在全部由P来做
mainView.txtName.text = data.PlayerName;
mainView.txtLev.text = "LV." + data.Lev;
mainView.txtMoney.text = data.Money.ToString();
mainView.txtGem.text = data.Gem.ToString();
mainView.txtPower.text = data.Power.ToString();
}
}
private void OnDestroy() {
PlayerModel.Data.RemoveEventListener(UpdateInfo);
}
}
RolePresenter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RolePresenter : MonoBehaviour
{
private MVP_RoleView roleView;
private static RolePresenter presenter = null;
public static RolePresenter Presenter{
get{
return presenter;
}
}
public static void ShowMe(){
if(presenter == null){
//实例化面板对象
GameObject res = Resources.Load<GameObject>("UI/RolePanel");
GameObject obj = Instantiate(res);
//设置父对象
obj.transform.SetParent(GameObject.Find("Canvas").transform,false);
presenter = obj.GetComponent<RolePresenter> ();
}
//如果隐藏形式是setacive,则显示也要set
presenter.gameObject.SetActive(true);
}
public static void HideMe(){
if(presenter != null){
presenter.gameObject.SetActive(false);
}
}
void Start()
{
roleView = this.GetComponent<MVP_RoleView>();
//第一次更新面板
UpdateInfo(PlayerModel.Data);
roleView.btnClose.onClick.AddListener(ClickCloseBtn);
roleView.btnLevUp.onClick.AddListener(ClickLevUpBtn);
//PlayerModel.Data.AddEventListener(roleView.UpdateInfo);
PlayerModel.Data.AddEventListener(UpdateInfo);
}
private void ClickCloseBtn(){
HideMe();
}
private void ClickLevUpBtn(){
//通过数据模块 进行升级 达到数据改变
PlayerModel.Data.LevUp();
}
// Update is called once per frame
private void UpdateInfo(PlayerModel data)
{
if(roleView != null){
//直接在p中得到V界面的控件,断开M和V的联系
roleView.txtLev.text = "LV." + data.Lev;
roleView.txtHp.text = data.Hp.ToString();
roleView.txtAtk.text = data.Atk.ToString();
roleView.txtDef.text = data.Def.ToString();
roleView.txtCrit.text = data.Crit.ToString();
roleView.txtMiss.text = data.Miss.ToString();
roleView.txtLuck.text = data.Luck.ToString();
}
}
private void OnDestroy() {
PlayerModel.Data.RemoveEventListener(UpdateInfo);
}
}
MVP同样由缺点,Presenter中的逻辑很多。
但是之后逻辑修改也只用在Presenter中。
MVVM
全称为模型(Model)-视图(View)-视图模型(ViewModel)
Model提供数据,View负责界面,ViewModel负责逻辑的处理
MVVM的由来是MVP(Model-View-Presenter)模式与WPF结合应用时发展演变过来的一种新型框架
数据绑定
将一个用户界面元素(控件)的属性 绑定到 一个类型(对象)实例上的某个属性的方法。
如果开发者有一个MainViewMode类型的实例,那么他就可以把MainViewMode的“Lev”属性绑定到一个Ul中Text的“Text”属性上。“绑定”了这2个属性之后,对Text的Text属性的更改将“传播”到MainViewMode的Lev属性,而对MainViewMode的Lev属性的更改同样会“传播”到Text的Text属性
MVVM在Unity中水土不服
因为View对象始终由我们来书写,没有UI配置文件(如WPF中的XAML),要想实现传播需要事件/委托,很麻烦。
硬要在Unity中实现MVVM,需要写三模块,并且还要对V和VM进行数据绑定,工作量大,好处也不够明显
Unity的第三方MVVM框架
Loxodon Framework
https://github.com/vovgou/loxodon-framework
uMVVM
https://github.com/MEyes/uMVVM
MVE
全称为模型(Model)-视图(View)-事件中心(EventCenter)
Model提供数据,View负责界面,EventCenter负责数据传递
好处:
利用事件中心的观察者模式
让M和V层的之间的关系更加灵活多变
减少了目前数据层的负载
将数据层事件全部交由事件中心处理
总结
铁打的M和V,流水的X
数据和界面是必备的内容
我们可以通过改变X元素来优化原本的MVC
也就是改变联系和处理M(数据)和V(界面)的方式
不要拘泥于框架结构和设计模式要找到一个适合自己项目的
一个稳定的,有序的,能满足项目需求的实现方式