MVC基础
MVC是什么?
MVC是Model模型(数据)、View视图(界面)、Controller控制(业务逻辑)。通过将这三者分离,让其在一定程度上互不影响。
在View(界面)触发事件,Controller处理业务触发数据更新,在通过Model将数据带到View,最后View更新数据,导致页面变化。
在游戏开发中使用MVC并不是必须的,因为它有好处也有坏处。
我们主要去思考什么样子的应用场景会使用MVC?
使用MVC的情景一般都是与UI交互的时候,而且一般有很多的UI界面,就可以使用MVC思想。如果你开发的游戏只有3、4个界面,那没有必要使用MVC。
好处:降低耦合、方便修改、逻辑更清晰。
先说说降低耦合:什么是耦合?就是类与类之间的依赖程度,耦合越低,依赖越小,所以说我们要降低耦合。如果不使用MVC的话,那么在UI触发事件后写在一个类中,那么这个类将会特别多,当你改变某个业务逻辑时,你会去这个类中找,而且还可能会影响到其他的方法。
方便修改:当我们将一个UI面板逻辑一分为三时,我们就能够很清楚我们需要修改哪些部分。
逻辑更清晰:同理。
缺点:脚本变多,体量变大,流程更复杂。(当然这些对想要看源码的小伙伴们很不友好)
不使用MVC的情况
不使用mvc,所有的逻辑都会写在一个脚本里面,既要更新面板数据,也要处理面板显隐,还要处理事件逻辑
如下面的代码,如果在其他的脚本中更新MainPanel里面的面板数据,需要调用UpdateInfo方法
using UnityEngine;
using UnityEngine.UI;
public class MainPanel : MonoBehaviour
{
public Text moneyText;
public Text gemText;
public Text powerText;
public Text nameText;
public Text levelText;
public Button mainBtn;
private static MainPanel panel;
public static void ShowMe()
{
if (panel is null)
{
GameObject go = Instantiate(Resources.Load<GameObject>("UI/MainPanel"), GameObject.Find("Canvas").transform, false);
panel = go.GetComponent<MainPanel>();
}
panel.gameObject.SetActive(true);
panel.UpdateInfo(new Data());
}
public static void HideMe()
{
panel.gameObject.SetActive(false);
}
private void Start()
{
mainBtn.onClick.AddListener(() =>
{
print("打开其他面板");
});
}
private void UpdateInfo(Data data)
{
// 这里只是模拟一下数据,实际上是通过网络请求、json、xml获得数据
moneyText.text = data.money.ToString();
gemText.text = data.gem.ToString();
powerText.text = data.power.ToString();
nameText.text = data.name;
levelText.text = data.level.ToString();
}
}
public class Data
{
public int money = 10;
public int gem = 15;
public int power = 16;
public int level = 17;
public string name = "张三";
}
使用MVC的情况
使用MVC需要创建3个文件夹。我们来分析一下三个结构
Model层
首先是Model层,一般都是数据相关的。
这个类一般处理数据、数据初始化、数据变化相关的方法、保存数据、通知外部调用更新数据。
// 数据一般是私有字段(内部修改)+公有属性(外部只读)
private string playerName;
private int level;
private int money;
private int gem;
private int power;
private int hp;
private int atk;
private int def;
private int crit;
private int miss;
private int luck;
public string PlayerName => playerName;
public int Level => level;
public int Money => money;
public int Gem => gem;
public int Power => power;
public int Hp => hp;
public int Atk => atk;
public int Def => def;
public int Crit => crit;
public int Miss => miss;
public int Luck => luck;
如果这个数据是唯一的,也可以将其设计为单例模式。并且在首次调用时进行初始化
private static PlayerModel _instance;
public static PlayerModel Instance
{
get
{
if (_instance == null)
{
_instance = new PlayerModel();
// 并对数据进行初始化
_instance.Init();
}
return _instance;
}
}
public void Init()
{
// 使用Json数据存储、也可以使用其他的,如xml
PlayerData playerData = JsonManager.Instance.LoadData<PlayerData>("players");
playerName = playerData.playerName;
level = playerData.level;
money = playerData.money;
gem = playerData.gem;
power = playerData.power;
hp = playerData.hp;
atk = playerData.atk;
def = playerData.def;
crit = playerData.crit;
miss = playerData.miss;
luck = playerData.luck;
}
接下来就是数据变化,比如玩家受伤(hp减少)、获得装备(atk增加)等。将其封装为方法,通过控制层去调用其方法。
public void LevelUp()
{
level += 1;
hp += level;
atk += level;
def += level;
crit += level;
miss += level;
luck += level;
// 升级后,将数据保存到本地
SaveData();
}
public void SaveData()
{
var playerData = new PlayerData();
playerData.playerName = playerName;
playerData.level = level;
playerData.money = money;
playerData.gem = gem;
playerData.power = power;
playerData.hp = hp;
playerData.atk = atk;
playerData.def = def;
playerData.crit = crit;
playerData.miss = miss;
playerData.luck = luck;
JsonManager.Instance.SaveData(playerData, "players");
// 更新数据
UpdateData();
}
当model层的数据变化时,需要通知控制层,然后通过控制层去更新界面层
private event UnityAction<PlayerModel> updateEvent;
public void AddEventListener(UnityAction<PlayerModel> action)
{
updateEvent += action;
}
public void RemoveEventListener(UnityAction<PlayerModel> action)
{
updateEvent -= action;
}
// 外部调用UpdateData来更新数据,一但更新数据,监听它的函数就会触发
public void UpdateData()
{
if (updateEvent != null)
{
updateEvent(this);
}
}
View层
接下来是视图层,这层主要处理找控件,提供面板更新的相关方法给外部
using Core.MVC.Model;
using UnityEngine;
using UnityEngine.UI;
namespace Core.MVC.View
{
public class MainView : MonoBehaviour
{
// 找控件
public Button roleBtn;
public Button skillBtn;
public Text nameTxt;
public Text levelTxt;
public Text moneyTxt;
public Text gemTxt;
public Text powerTxt;
// 提供面板更新的相关方法给外部
public void UpdateInfo(PlayerModel player)
{
nameTxt.text = player.PlayerName;
levelTxt.text = "LV." + player.Level;
moneyTxt.text = player.Money.ToString();
gemTxt.text = player.Gem.ToString();
powerTxt.text = player.Power.ToString();
}
}
}
using Core.MVC.Model;
using UnityEngine;
using UnityEngine.UI;
namespace Core.MVC.View
{
public class RoleView : MonoBehaviour
{
// 找控件
public Button closeBtn;
public Button levelUpBtn;
public Text levelText;
public Text hpText;
public Text atkText;
public Text defText;
public Text critText;
public Text missText;
public Text luckText;
// 更新数据
public void UpdateInfo(PlayerModel data)
{
levelText.text = "LV." + data.Level;
hpText.text = data.Hp.ToString();
atkText.text = data.Atk.ToString();
defText.text = data.Def.ToString();
critText.text = data.Crit.ToString();
missText.text = data.Miss.ToString();
luckText.text = data.Luck.ToString();
}
}
}
Controller层
这一层主要处理业务逻辑:界面的显隐、界面的更新、事件的监听
private static MainController _instance;
public static MainController Instance => _instance;
public static void ShowMe()
{
if (_instance == null)
{
_instance = Instantiate(Resources.Load<GameObject>("UI/MainPanel"),
GameObject.Find("Canvas").transform).GetComponent<MainController>();
}
_instance.gameObject.SetActive(true);
}
public static void HideMe()
{
_instance.gameObject.SetActive(false);
}
监听事件
private MainView _mainView;
private void Start()
{
_mainView = GetComponent<MainView>();
_mainView.UpdateInfo(PlayerModel.Instance);
_mainView.roleBtn.onClick.AddListener(() =>
{
RoleController.ShowMe();
});
// 通知事件
PlayerModel.Instance.AddEventListener(UpdateInfo);
}
public void UpdateInfo(PlayerModel player)
{
if (player != null)
{
// 更新界面层
_mainView.UpdateInfo(player);
}
}
总结
这就是大致的效果,虽然看起来非常的复杂,但如果进行封装的话,那一定用起来非常的爽。
MVC变形-MVX
我们知道MVC架构下,M和V之间是存在着联系的。
我们看下面的代码,MainView中会使用PlayerModel的数据,当PlayerModel添加字段时,也会改变MainView类的方法,这就是界面和数据之间的耦合性。
// MainView
public void UpdateInfo(PlayerModel player)
{
nameTxt.text = player.PlayerName;
levelTxt.text = "LV." + player.Level;
moneyTxt.text = player.Money.ToString();
gemTxt.text = player.Gem.ToString();
powerTxt.text = player.Power.ToString();
}
那么我们应该怎么办?
铁打的M和V,流水的X。数据和界面是必背的内容。我们可以通过改变X元素来优化原来的MVC。这就是MVX的诞生。比如MVP、MVVE、MVE等,接下来我们将会介绍下面的这些变体。
MVP
由于MVC中M和V是存在联系的,而MVP则是会断开M和V之间的联系,通过presentor来建立M和V之间的关系。
首先M层与之前的代码是一样的。
而V层就不需要提供方法用来更新方法
using UnityEngine;
using UnityEngine.UI;
namespace Core.MVP.View
{
public class RoleView : MonoBehaviour
{
public Button closeBtn;
public Button levelUpBtn;
public Text levelText;
public Text hpText;
public Text atkText;
public Text defText;
public Text critText;
public Text missText;
public Text luckText;
}
}
using UnityEngine;
using UnityEngine.UI;
namespace Core.MVP.View
{
public class MainView : MonoBehaviour
{
public Button roleBtn;
public Button skillBtn;
public Text nameTxt;
public Text levelTxt;
public Text moneyTxt;
public Text gemTxt;
public Text powerTxt;
}
}
那么P层(主持人)就需要建立视图和模型层之间的联系。其实和之前的代码是一样的,主要变化的是就是在这一层更新控件。
using Core.MVC.Model;
using Core.MVP.View;
using UnityEngine;
namespace Core.MVP.Presentor
{
public class MainPresentor : MonoBehaviour
{
private MainView _mainView;
private static MainPresentor _instance;
public static MainPresentor Instance => _instance;
public static void ShowMe()
{
if (_instance == null)
{
_instance = Instantiate(Resources.Load<GameObject>("UI/MainPanel"),
GameObject.Find("Canvas").transform).GetComponent<MainPresentor>();
}
_instance.gameObject.SetActive(true);
}
public static void HideMe()
{
_instance.gameObject.SetActive(false);
}
private void Start()
{
// 加载视图
_mainView = GetComponent<MainView>();
// 更新数据
UpdateInfo(PlayerModel.Instance);
// 监听控件变化
_mainView.roleBtn.onClick.AddListener(() => { RolePresentor.ShowMe(); });
// 监听数据变化
PlayerModel.Instance.AddEventListener(UpdateInfo);
}
private void UpdateInfo(PlayerModel player)
{
if (player != null)
{
// 更新视图
_mainView.gemTxt.text = player.Gem.ToString();
_mainView.levelTxt.text = player.Level.ToString();
_mainView.moneyTxt.text = player.Money.ToString();
_mainView.powerTxt.text = player.Power.ToString();
_mainView.nameTxt.text = player.PlayerName;
}
}
}
}
using Core.MVC.Model;
using Core.MVP.View;
using UnityEngine;
namespace Core.MVP.Presentor
{
public class RolePresentor : MonoBehaviour
{
private RoleView _roleView;
private static RolePresentor _instance;
public static RolePresentor Instance => _instance;
public static void ShowMe()
{
if (_instance == null)
{
_instance = Instantiate(Resources.Load<GameObject>("UI/RolePanel"),
GameObject.Find("Canvas").transform).GetComponent<RolePresentor>();
}
_instance.gameObject.SetActive(true);
}
public static void HideMe()
{
_instance.gameObject.SetActive(false);
}
private void Start()
{
_roleView = GetComponent<RoleView>();
UpdateInfo(PlayerModel.Instance);
_roleView.closeBtn.onClick.AddListener(() =>
{
HideMe();
});
PlayerModel.Instance.AddEventListener(UpdateInfo);
_roleView.levelUpBtn.onClick.AddListener(() =>
{
PlayerModel.Instance.LevelUp();
});
}
public void UpdateInfo(PlayerModel player)
{
if (player != null)
{
_roleView.atkText.text = player.Atk.ToString();
_roleView.defText.text = player.Def.ToString();
_roleView.critText.text = player.Crit.ToString();
_roleView.hpText.text = player.Hp.ToString();
_roleView.missText.text = player.Miss.ToString();
_roleView.luckText.text = player.Luck.ToString();
_roleView.levelText.text = player.Level.ToString();
}
}
}
}
MVE
View第一次显示获取Mode数据用于更新自己,并通知事件中心监听事件
数据更新时(玩家操作或者服务器更新)通过告知事件中心触发并分发事件
数据从事件中心流入View进行更新
PureMVC框架
pureMVC是基于MVC思想+一些设计模式建立的一个轻量级的应用框架
可以在http://puremvc.org中获取
PureMVC基本结构
Model(数据模型):关联Proxy代理对象,负责处理数据
VIew(界面):关联Mediator中介对象,负责处理界面
Controller(业务控制):管理Command命令对象,负责处理业务逻辑
Facade(外观):MVC三者的经济人,统管全局,可以代理、中介、命令
Notification:通知,负责传递信息
通知名类
新建一个PureNotification类,用来声明各个通知的名字,方便使用和管理
namespace Core.PureMVC
{
public class PureNotification
{
public const string SHOW_PANEL = "ShowPanel";
}
}
Model层
在这一层有两部分组成,一个是数据对象、另一个是对数据对象的代理,用来处理数据更新相关的逻辑。
我们先写Obj对象
namespace Core.PureMVC.Model
{
public class PlayerDataObj
{
public string PlayerName { get; set; }
public int Level { get; set; }
public int Money { get; set; }
public int Gem { get; set; }
public int Power { get; set; }
public int Hp { get; set; }
public int Atk { get; set; }
public int Def { get; set; }
public int Crit { get; set; }
public int Miss { get; set; }
public int Luck { get; set; }
}
}
然后让Obj对象关联Proxy,处理数据相关的内容
using PureMVC.Patterns.Proxy;
namespace Core.PureMVC.Model
{
public class PlayerProxy : Proxy
{
// 1. 必须要继承Proxy
// 2. 必须要有构造方法
public new const string Name = "PlayerProxy";
public PlayerProxy() : base(Name)
{
// 关联对象
PlayerDataObj playerDataObj = new PlayerDataObj();
// 初始化
playerDataObj = JsonManager.Instance.LoadData<PlayerDataObj>("PlayerData");
// 赋值给Data
Data = playerDataObj;
}
// 与数据相关的逻辑,比如:升级
public void LevelUp()
{
if (Data is not PlayerDataObj playerDataObj) return;
playerDataObj.Level += 1;
playerDataObj.Hp += playerDataObj.Level;
playerDataObj.Atk += playerDataObj.Level;
playerDataObj.Def += playerDataObj.Level;
playerDataObj.Crit += playerDataObj.Level;
playerDataObj.Miss += playerDataObj.Level;
playerDataObj.Luck += playerDataObj.Level;
}
public void SaveData()
{
PlayerDataObj playerData = Data as PlayerDataObj;
JsonManager.Instance.SaveData(playerData, "PlayerData");
}
}
}
View视图层
在视图层中主要是两部分,一个是UI控件相关,另一个是Mediator用于对数据进行处理
using Core.PureMVC.Model;
using UnityEngine;
using UnityEngine.UI;
namespace Core.PureMVC.View
{
public class MainView : MonoBehaviour
{
// 找控件
public Button roleBtn;
public Button skillBtn;
public Text nameTxt;
public Text levelTxt;
public Text moneyTxt;
public Text gemTxt;
public Text powerTxt;
// 提供面板更新的相关方法给外部
// 安装MVC的思想,可以直接在这里提供 更新的方法
public void UpdateInfo(PlayerDataObj player)
{
nameTxt.text = player.PlayerName;
levelTxt.text = "LV." + player.Level;
moneyTxt.text = player.Money.ToString();
gemTxt.text = player.Gem.ToString();
powerTxt.text = player.Power.ToString();
}
}
}
接下来通过Mediator关联MainView
在PureNotification类中添加更新玩家数据通知
public class PureNotification
{
public const string UPDATE_PLAYER_INFO = "UpdatePlayerInfo";
}
using Core.PureMVC.Model;
using PureMVC.Interfaces;
using PureMVC.Patterns.Mediator;
namespace Core.PureMVC.View
{
public class MainViewMediator : Mediator
{
public static string NAME = "MainViewMediator";
public MainViewMediator() : base(NAME)
{
// 这里面可以去创建界面预设体等的逻辑
// 但是界面显示应该是触发控制的
}
public override string[] ListNotificationInterests()
{
// 你需要监听哪些通知?
// 把通知们通过字符串数组的形式返回出去
// PureMVC会帮我们监听这些通知
return new string[]
{
PureNotification.UPDATE_PLAYER_INFO
};
}
public override void HandleNotification(INotification notification)
{
// notification.Body:通知包含的信息
// notification.Name:通知名
switch (notification.Name)
{
case PureNotification.UPDATE_PLAYER_INFO:
// 当我们受到更新通知的时候,做处理
(ViewComponent as MainView)?.UpdateInfo(notification.Body as PlayerDataObj);
break;
}
}
}
}
同理我们将添加角色面板
using Core.PureMVC.Model;
using UnityEngine;
using UnityEngine.UI;
namespace Core.PureMVC.View
{
public class RoleView : MonoBehaviour
{
// 找控件
public Button closeBtn;
public Button levelUpBtn;
public Text levelText;
public Text hpText;
public Text atkText;
public Text defText;
public Text critText;
public Text missText;
public Text luckText;
// 更新数据
public void UpdateInfo(PlayerDataObj data)
{
levelText.text = "LV." + data.Level;
hpText.text = data.Hp.ToString();
atkText.text = data.Atk.ToString();
defText.text = data.Def.ToString();
critText.text = data.Crit.ToString();
missText.text = data.Miss.ToString();
luckText.text = data.Luck.ToString();
}
}
}
using Core.PureMVC.Model;
using PureMVC.Interfaces;
using PureMVC.Patterns.Mediator;
using UnityEngine;
namespace Core.PureMVC.View
{
public class RoleViewMediator : Mediator
{
public static new string NAME = "RoleViewMediator";
public RoleViewMediator() : base(NAME)
{
}
public override string[] ListNotificationInterests()
{
return new string[]
{
PureNotification.UPDATE_PLAYER_INFO
};
}
public override void HandleNotification(INotification notification)
{
switch (notification.Name)
{
case PureNotification.UPDATE_PLAYER_INFO:
(ViewComponent as RoleView)?.UpdateInfo(notification.Body as PlayerDataObj);
break;
}
}
}
}
Facade和Command的执行流程
Facade就是控制所有控件的,能够拿到所有View、Model、Controller。通过它去串联整个框架
接下来我们先串联一下简单的流程,首先是入口
using Core.PureMVC.Controller;
using UnityEngine;
namespace DY
{
public class Main : MonoBehaviour
{
private void Start()
{
GameFacade.Instance.StartUp();
}
}
}
所以说GameFacade有一个单例模式,而且会去发送通知,说我要启动。
public class PureNotification
{
public const string START_UP = "StartUp";
}
using PureMVC.Patterns.Facade;
namespace Core.PureMVC.Controller
{
public class GameFacade : Facade
{
public static GameFacade Instance
{
get
{
if (instance == null)
{
instance = new GameFacade();
}
return instance as GameFacade;
}
}
/// <summary>
/// 初始化 控制层相关的内容
/// </summary>
protected override void InitializeController()
{
base.InitializeController();
// 绑定命令和通知
RegisterCommand(PureNotification.START_UP, () =>
{
return new StartUpCommand();
});
}
public void StartUp()
{
SendNotification(PureNotification.START_UP);
}
}
}
在发送通知的时候我们需要去注册通知,所以我们要将制作一个Command类用来处理启动相关的逻辑
using PureMVC.Interfaces;
using PureMVC.Patterns.Command;
using UnityEngine;
namespace Core.PureMVC.Controller
{
public class StartUpCommand : SimpleCommand
{
public override void Execute(INotification notification)
{
base.Execute(notification);
// 当命令被执行,调用该方法
Debug.Log("StartUpCommand");
}
}
}
也就是说当我们 GameFacade.Instance.StartUp();
它就会去调用StartUpCommand中Execute方法的逻辑。而注册绑定就是一些常规操作,我们不需要去关心。
控制显隐
接下来我们将会举一个例子,并加深刻的使用这个框架,来串联整个项目
评论区