MVC是(Model、View、Controller)三个单词的首字母简写,其应用的目的是为了将程序分层,便于应用程序的分层开发。
Model(模型)代表一个存储数据的对象,它也可以带有少量的逻辑,在数据有变化的时候通知Controller进行更新视图。
View(视图)代表模型包含的数据的可视化表现。
Controller(控制器)控制器控制数据流向模型对象,并在数据变化时更新视图。它使得视图于模型分离开。
以一个MMORPG背包系统为例,玩家可以在游戏主界面按背包按钮打开背包,背包中的数据来源于服务器,背包有自动整理、显示物品信息等等交互功能。我们用英文Pack来代表背包,应用于相应的Model、View、Controller类的命名。
Model
PackModel的初始化:
PackModel类使用单例模式(针对MMORPG游戏特性,每个玩家客户端只维护该玩家的背包数据)。
PackModel职责:
在PackModel中定义玩家持有物品的列表的数据存储结构并进行初始化、对数据操纵的函数以及向PackView传入数据更新表现等等。
向PackView传入数据的方式不止一种,例如PackModel存放PackView的引用或PackModel发送被PackView监听的消息类型。
View
将面板PackView制作成Prefab,面板PackView的GameObject挂载PackView.cs,一方面在PackView.cs中获取各个面板组件的引用,例如该面板的一些按钮,并在PackView.cs中定义这些按钮的回调函数(例如背包系统中子面板的呼出),另一方面等绑定与解绑视图显示逻辑事件,如创建与销毁、显示数据、更新界面等事件。
Controller
PackController的初始化:
对于MMORPG来说,一般在游戏初始化的时候实例化不同模块的Controller类,背包系统作为一个模块,其Controller类也是在游戏初始化的时候被实例化。
PackController的职责:
Controller类负责的是控制数据流流向模型对象,其中数据流来自游戏服务器。同时,Controller也是由User进行更新的,它也处理玩家的交互事件,例如点击背包按钮将背包面板调出。
故Controller会持有View和Model的引用,同时会定义网络协议并对网络协议进行注册和定义回调事件将网络数据更新至Model中,并注册玩家的点击事件回调事件,需要注意的是,虽然Controller持有View的引用,但是个人觉得不应该由Controller来更新View,因为View所需要的数据只有Model知道,所以应该由Model来驱动View更新其表现会比较好。
换一种更通俗易懂的说法,比如一个图形界面由三部分组成,给用户看到的是view就是场景,view这部分的代码是读入数据,model里面存放数据,view从model中取数据,然后根据这个数据把画面给画出来,其他的都不管,用户如果有鼠标的动作或键盘的动作,由controller来告诉model,我们需要怎么改数据。比如说我们在键盘上按了向右的的键,controller就会告诉model哪个地方需要改的,然后model告诉view它已经改过了。然后view去取数据,然后把它画出来。鼠标和键盘的动作引起的是数据的改变。
LuaFramework使用了PureMVC框架。百度百科上说:“PureMVC是在基于模型、视图和控制器MVC模式建立的一个轻量级的应用框架”。PureMVC框架可以做到较好的解耦,减少游戏代码的相互调用。然而LuaFramework整合PureMVC属于“杀鸡用牛刀”,实质上只用到了事件分发(也可能是我理解得不够透彻)。如果单纯写一套事件分发系统,可能不到100行代码就能完成。
如果没有很好的解耦设计,游戏功能越多,代码就越乱,最后没人敢改动。举个例子,假如游戏中背包(item)和成就(Achieve)两项功能,各用一个类实现。当玩家获得100个经验豆(一种道具)时,会获得“拥有100个经验豆”的成就;当成就点数达到300时,会获得道具奖励。一种常见的实现方法是调用对方的public函数,代码如下所示。然而如果一款游戏有几百上千个类,之间又相互调用,如果某些功能需要大改(例如删掉成就功能),那其他的类也得改动。
Class Item
{
public AddItem()
{
if(经验豆 > 100)
achieve.AddAchieve(“拥有100个经验豆”)
}
}
Class Achieve
{
public AddAchieve()
{
成就点数 + 10
if(成就点数 > 300)
item.AddItem(宝石)
}
}
如果使用事件分发,各个类之间的联系就减弱了。如下所示的代码中背包类(Item)监听了消息“添加道具”,成就类(Achieve)监听了消息“添加成就”。如果达成成就需要添加奖励,只需派发“添加道具”这条消息,由背包类去执行。这样类与类之间不存在相互调用,就算大改功能甚至删掉功能,其他类都受到的影响比较小。
Class Item
{
Start()
{
监听(“添加道具”,AddItem) //绑定
}
private AddItem()
{
if(经验豆 > 100)
分发(“添加成就”,“拥有100个经验豆”) //执行
}
}
Class Achieve
{
Start()
{
监听(“添加成就”,AddAchieve) //绑定
}
private AddAchieve()
{
成就点数 + 10
If(成就点数 > 300)
分发(“添加道具”, 宝石) //执行
}
}
LuaFramework中的Framwork目录存放着PureMVC框架的代码,个人认为在LuaFramework中属于过度设计(毕竟从其他地方拷过来的)。它的原理并不复杂,用一个列表把监听信息保存起来,在分发消息时,查找对应的监听表,找到需要回调的对象。
PureMVC框架便是实现了“注册/分发”模式(发布/订阅、观察者模式),可以调用RegisterCommand注册消息(命令),调用SendMessageCommand方法分发消息。RegisterCommand方法可以把某个继承ControllerCommand 的类注册到指定的消息下,在事件分发时调用该类的Execute方法。
例如新建一个名为TestCommand的类,让它继承ControllerCommand,然后编写Execute方法处理具体事务。
using UnityEngine;
using System.Collections;
public class TestCommand : ControllerCommand
{
public override void Execute(IMessage message) //message包含了Name, Body, Type三个成员。其中Name是命令名,Body是一个任意类型的参数。
{
Debug.Log("name=" + message.Name);
Debug.Log("type=" + message.Type);
}
}
接着,编写另一个类来处理消息。这个类先调用AppFacade.Instance.RegisterCommand()将TestCommand类注册到“TestMessage”消息下。
然后使用SendMessageCommand()派发“TestMessage”消息。框架将会创建一个TestCommand实例,并调用它的Execute方法。
public class Main : MonoBehaviour
{
void Start()
{
AppFacade.Instance.RegisterCommand ("TestMessage",typeof(TestCommand)); //将TestCommand类注册到“TestMessage”消息
AppFacade.Instance.SendMessageCommand ("TestMessage"); //分发“TestMessage”消息
}
}
分发消息后,TestCommand的Execute方法将被调用。
如下代码所示,在SendMessageCommand中可以给消息的Body传值,相应的Execute方法便可以获取它。
void Start()
{
AppFacade.Instance.RegisterCommand ("TestMessage", typeof(TestCommand));
AppFacade.Instance.SendMessageCommand ("TestMessage", "这是字符串");
}
总而言之,LuaFramework中所谓的pureMVC只是一套“注册/分发”机制,完全可以用C#的事件来实现。
例如,新建一个继承自View的TestManage组件,在Start方法中它注册了“msg1”、“msg2”、“msg3”三个消息的监听。在Update方法中,当按下空格键时,分发消息“msg1”。
当接收到消息后,指定对象(这里指定this)的OnMessage方法会被调用,参数message里面包含了命令名、Body等信息。代码如下所示。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class TestManage : View
{
// Use this for initialization
void Start ()
{
List regList = new List();
regList.Add("msg1");
regList.Add("msg2");
regList.Add("msg3");
RegisterMessage(this,regList); //当前对象中除生命周期函数外的override方法,参数为IMessage message
}
// Update is called once per frame
void Update ()
{
if (Input.GetKeyUp (KeyCode.Space))
{
facade.SendMessageCommand("msg1", null);
}
}
public override void OnMessage(IMessage message)
{
Debug.Log ("OnMessage " + message.Name);
}
}
此外LuaFramework的各个Manager(如GameManager,LuaManager,SoundManager等)也都继承自View类,可以使用“注册/分发”机制。
代码繁冗,当你阅读别人的代码,一个很简单的逻辑,被封装了多次,需要在多个代码文件中索引,阅读效率极低。代码文件分散,一些很简单的逻辑,例如按钮点击,都做了多层封装。
不太专业的某些程序的惰性,导致他们并不是真正理解MVC或者说这些框架的原理,他们的目标只是,把功能搞出来。他们要么绕过框架,穿插了很多调用,要么整体copy别人的一个功能,去掉逻辑,留下骨架,然后填充自己的逻辑,也不管这个骨架是否适用。这样的人,大大增加了项目的混乱,leader要做到充分的代码review,去除这些问题,在开发进度吃紧,每周都要出版本的情况下,是不可能的。这些快速堆出功能的程序员,反而得到策划等非技术人员的赞赏。而认真处理,把每一块都做好,但是慢一点的程序,反而不受夸奖,这导致了劣币驱逐良币。我相信,除了极少数精英团队,很多团队都有这样的问题。
这些设计和框架,被滥用。比如MVC也许适用于UI部分的设计,但是,他是否适用一个战斗模块呢?他是否适用一个剧情模块呢?有些团队,机械地运用某些框架,并不根据需求去思考,认为某个东西是好的,就到处使用。
当一个项目规模增长,人员难以保证精英,积累了大量的需求变更,运营期间,需要快速的迭代。这种繁杂冗余的框架式设计,会导致代码难以维护。有时候,并不是某个框架不够好,更多的是,我们没有仔细理清它的适用范围,也难以保证规范从头到尾的坚定执行。并留下大量过分设计的繁杂代码,一个一百行,几个funtion就能解决的问题,被包装成了多个class,层层调用,写了几大百行,逻辑处处分散。
那么,到底是某个框架,或者设计模式不行,还是我们使用得不够好呢?
我们回到最初,仔细考虑,MVC解决的核心问题是,一个M,多个V,那么,在游戏领域,这样的需求多吗?是强需求吗?我们到底应该根据需求来设计框架,还是应该根据框架来填充需求?一个框架,一套设计,适用不同的游戏,不同的逻辑吗?
我认真地考虑这块问题,发现很多教程、文章,他们介绍MVC、MVVM,介绍各种框架,包括uframe,StrangeIoC等,都缺少了思考和提问:
这个框架适用什么需求?解决了什么问题?
在什么情况下我该用,什么情况不该用?他带来了什么问题?
是否适合我的项目,我的团队?
我是应该项目整体使用,还是某些局部的需求使用呢?
等等问题,才是我们该问的关键。
我们游戏领域的技术,特别是游戏的框架,受到了太多应用软件,web开发和app开发的影响,但是,这些模式并不适用我们啊!正式因为web和app这些领域的通用性,需求的固定性(相对游戏开发而言),他们才会诞生出如此多的框架和模式,并且在技术领域发出了更多的声音,出现了看似更高级的设计方式。web和app的项目周期以及后续的维护周期是很长的,少则三两年,多则十几年,确实必须要谨慎设计。但是,我们游戏,特别是现在手游的生命周期又有多长呢?当需求不同,考虑问题的方式,解决问题的方式,是否应该做些改变?
参考:https://blog.csdn.net/DdogYuan/article/details/82706297
参考:https://blog.csdn.net/weixin_39706943/article/details/80679149
参考:https://blog.csdn.net/weixin_42224688/article/details/80937986