游戏项⽬中的MVC架构(Unity和LuaFrameWork)
MVC模式
MVC是(Model、View、Controller)三个单词的⾸字母简写,其应⽤的⽬的是为了将程序分层,便于应⽤程序的分层开发。
Model(模型)代表⼀个存储数据的对象,它也可以带有少量的逻辑,在数据有变化的时候通知Controller进⾏更新视图。
View(视图)代表模型包含的数据的可视化表现。
Controller(控制器)控制器控制数据流向模型对象,并在数据变化时更新视图。它使得视图于模型分离开。
Unity游戏项⽬中的MVC模式
以⼀个MMORPG背包系统为例,玩家可以在游戏主界⾯按背包按钮打开背包,背包中的数据来源于服务器,背包有⾃动整理、显⽰物品信息等等交互功能。我们⽤英⽂Pack来代表背包,应⽤于相应的Model、View、Controller类的命名。
1. Model
PackModel的初始化:
PackModel类使⽤单例模式(针对MMORPG游戏特性,每个玩家客户端只维护该玩家的背包数据)。
PackModel职责:
在PackModel中定义玩家持有物品的列表的数据存储结构并进⾏初始化、对数据操纵的函数以及向PackView传⼊数据更新表现等等。
向PackView传⼊数据的⽅式不⽌⼀种,例如PackModel存放PackView的引⽤或PackModel发送被PackView监听的消息类型。
2. View
将⾯板PackView制作成Prefab,⾯板PackView的GameObject挂载PackView.cs,⼀⽅⾯在PackView.cs中获取各个⾯板组件的引⽤,例如该⾯板的⼀些按钮,并在PackView.cs中定义这些按钮的回调函数(例如背包系统中⼦⾯板的呼出),另⼀⽅⾯等绑定与解绑视图显⽰逻辑事件,如创建与销毁、显⽰数据、更新界⾯等事件。
3. 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
LuaFramework使⽤了PureMVC框架。百度百科上说:“PureMVC是在基于模型、视图和控制器MVC模式建⽴的⼀个轻量级的应⽤框架”。PureMVC框架可以做到较好的解耦,减少游戏代码的相互调⽤。然⽽LuaFramework整合PureMVC属于“杀鸡⽤⽜⼑”,实质上只⽤到了事件分发(也可能是我理解得不够透彻)。如果单纯写⼀套事件分发系统,可能不到100⾏代码就能完成。
1. 解耦的好处
如果没有很好的解耦设计,游戏功能越多,代码就越乱,最后没⼈敢改动。举个例⼦,假如游戏中背包(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)
分发(“添加道具”, 宝⽯) //执⾏
}
}
2. MVC的使⽤⽅法
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#的事件来实现。
3. MVC与Unity3D组件的结合
pureMVC与Unity3D组件之间有⼀些封装,只要让组件继承View类(View类继承MonoBehavior),即使⽤pureMVC框架的RegisterMessage和SendMessageComman⽅法实现“注册/分发”机制。
例如,新建⼀个继承⾃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<string> regList = new List<string>();
regList.Add("msg1");
regList.Add("msg2");
regList.Add("msg3");
RegisterMessage(this,regList); //当前对象中除⽣命周期函数外的override⽅法,参数为IMessage messageunity 教程
}
// 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模式可能存在的缺点
1. 代码繁冗,当你阅读别⼈的代码,⼀个很简单的逻辑,被封装了多次,需要在多个代码⽂件中索引,阅读效率极低。代码⽂件分散,
⼀些很简单的逻辑,例如按钮点击,都做了多层封装。
2. 不太专业的某些程序的惰性,导致他们并不是真正理解MVC或者说这些框架的原理,他们的⽬标只
是,把功能搞出来。他们要么绕过
框架,穿插了很多调⽤,要么整体copy别⼈的⼀个功能,去掉逻辑,留下⾻架,然后填充⾃⼰的逻辑,也不管这个⾻架是否适⽤。这样的⼈,⼤⼤增加了项⽬的混乱,leader要做到充分的代码review,去除这些问题,在开发进度吃紧,每周都要出版本的情况下,是不可能的。这些快速堆出功能的程序员,反⽽得到策划等⾮技术⼈员的赞赏。⽽认真处理,把每⼀块都做好,但是慢⼀点的程序,反⽽不受夸奖,这导致了劣币驱逐良币。我相信,除了极少数精英团队,很多团队都有这样的问题。
3. 这些设计和框架,被滥⽤。⽐如MVC也许适⽤于UI部分的设计,但是,他是否适⽤⼀个战⽃模块呢?他是否适⽤⼀个剧情模块呢?有
些团队,机械地运⽤某些框架,并不根据需求去思考,认为某个东西是好的,就到处使⽤。
4. 当⼀个项⽬规模增长,⼈员难以保证精英,积累了⼤量的需求变更,运营期间,需要快速的迭代。这种繁杂冗余的框架式设计,会导
致代码难以维护。有时候,并不是某个框架不够好,更多的是,我们没有仔细理清它的适⽤范围,也难以保证规范从头到尾的坚定执⾏。并留下⼤量过分设计的繁杂代码,⼀个⼀百⾏,⼏个funtion就能解决的问题,被包装成了多个class,层层调⽤,写了⼏⼤百⾏,逻辑处处分散。
那么,到底是某个框架,或者设计模式不⾏,还是我们使⽤得不够好呢?
我们回到最初,仔细考虑,MVC解决的核⼼问题是,⼀个M,多个V,那么,在游戏领域,这样的需求多吗?是强需求吗?我们到底应该根据需求来设计框架,还是应该根据框架来填充需求?⼀个框架,⼀套设计,适⽤不同的游戏,不同的逻辑吗?
我认真地考虑这块问题,发现很多教程、⽂章,他们介绍MVC、MVVM,介绍各种框架,包括uframe,StrangeIoC等,都缺少了思考和提问:
这个框架适⽤什么需求?解决了什么问题?
在什么情况下我该⽤,什么情况不该⽤?他带来了什么问题?
是否适合我的项⽬,我的团队?
我是应该项⽬整体使⽤,还是某些局部的需求使⽤呢?
等等问题,才是我们该问的关键。
我们游戏领域的技术,特别是游戏的框架,受到了太多应⽤软件,web开发和app开发的影响,但是,
这些模式并不适⽤我们啊!正式因为web和app这些领域的通⽤性,需求的固定性(相对游戏开发⽽⾔),他们才会诞⽣出如此多的框架和模式,并且在技术领域发出了更多的声⾳,出现了看似更⾼级的设计⽅式。web和app的项⽬周期以及后续的维护周期是很长的,少则三两年,多则⼗⼏年,确实必须要谨慎设计。但是,我们游戏,特别是现在⼿游的⽣命周期⼜有多长呢?当需求不同,考虑问题的⽅式,解决问题的⽅式,是否应该做些改变?
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论