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

  1. View第一次显示获取Mode数据用于更新自己,并通知事件中心监听事件

  2. 数据更新时(玩家操作或者服务器更新)通过告知事件中心触发并分发事件

  3. 数据从事件中心流入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方法的逻辑。而注册绑定就是一些常规操作,我们不需要去关心。

控制显隐

接下来我们将会举一个例子,并加深刻的使用这个框架,来串联整个项目