http://uh.9ria.com/space-12147-do-blog-id-2140.html
最近MVC框架炒得沸沸扬扬的。我并不是说游戏中就不该使用MVC框架,而是说,比起MVC框架,游戏里还有很多可以明显提高效率和质量的地方。MVC框架本身对于游戏来说帮助不大,由于成本还可能有反效果。
但我光这样说,显然没什么说服力。因此,在这里贡献一个游戏的架构思路出来。好坏则由大家判断。
//**准备**//
首先,第一步就应当分为通用和不通用两部分。通用部分就是大家所说的游戏引擎。这部分暂且不提,但对于一个长期开发的团队,不应该只以一个游戏作为目标。所以,应该在开发过程中尽可能通用部分移出。这部分和游戏的框架结构是相同的。
但,如果的确没有精力,这部分可以无视。
其实这部分任何项目都有的,因为有一个必要的部分必须放在这:UI框架。
//**开始**//
应用架构说起来很简单,就是一棵树。
树的根节点就是主程序,负责所有和后台的交互操作,模块间通讯,资源加载,以及提供其他一些通用服务。在这个地方,可以根据规模来进行细节,MVC框架也未尝不可。但这部分代码占总代码量的份额并不会太多,各个子模块分得也很清楚。虽然不难做,但做了也没什么用。
这里需要注意一点,不管内部是怎样开发的,这部分公开的接口应当简单清晰,因为它会被非常频繁的调用,所有的模块都会去引用他。如果使得其他开发者使用困难的话,会降低整个项目的开发效率,影响范围很大。
包括的内容可能是:
View:场景切换,公共UI(指的是提示框,小图标一类)。
Command:后台交互,跨模块通讯。
Model:系统公共数据(全局设置,玩家数据等)以及所有Model的引用,后台代理。
//**第二步**//
这个节点往下,会由于游戏类型不同具有不同的分支。一般可分为四大类:屏幕,容器,容器内的对象,UI。
屏幕:简单的说,就是游戏的各个场景。它是一个直接显示出来的东西,具体的逻辑代码全在里面,控制人物走动,调出资源。如果是小游戏的话,它就是整个游戏的全部。之所以叫屏幕,是因为有切换屏幕的操作(比如从开始界面进入到游戏内),每次切换自然是销毁上一个,然后重新生成下一个。如果说模块可能更容易理解。换场景也是如此,这样才能保证游戏的内容可以简单的进行横向扩展,新的场景?继承了加一个屏幕就可以了。一个场景里有特殊的功能,必须编码实现?继承了当原来那样用就是。多态特性要多多利用。分屏?两个屏幕同时存在就可以了。
容器和容器内的对象:这是密切结合的两个部分。就像FLEX的容器和UI对象一样。考虑到效率以及易用性,他们之间相互联系的代码需要封装在他们内部——或者容器内,或者对象内。而且为了避免误解,不允许在容器内加入其他的显示对象。这种强耦是必要的,否则复杂度会急剧上升。如果有意见,请先向现在所有的UI框架提意见。
这里有一个逻辑分配的原则:凡是单个对象和容器之间交互的代码,应当放在对象内。因为对象的种类肯定比容器多,这样分支会写的比较少,添加新的对象也比较容易。而多个对象和容器的交互,则只能写在容器内了。这是一般的想法就不用解释了。总之,代码应该尽量写在数量居多的一方。
虽然这样说,低成本的解耦依然是必要的。这是一个比对的过程,怎样才能既能保证效率(编码以及运行效率),也不至于太伤害到之后的扩展性和可维护性?作为架构者必须考虑这个问题。如果这样的问题不需要考虑的话,如果架构就是照搬一个完美的标准然后执行,电脑比你干得更好。
UI:UI属于一个相对比较独立的部分,而且通用性很高。如果UI比较复杂的话(诸如策略游戏),也可以使用MVC,就像一般的企业应用那样操作。但如果游戏内的UI比较简单的话,而且不可能复杂化(诸如动作游戏),就没有那个必要。
屏幕作为一个模块的主体,作为一个真正意义的容器,已经不适合再向下细分了。UI作为独立的一部分,则不在讨论范围内。容器分无可分,最麻烦的,就是容器里的对象。而它的种类,数量,以及逻辑也是最多的。
//**第三步**//
这个东西,应当在逻辑上分为三层。
第一层:类型和数据
类型:仅仅靠Class,是不可能表述游戏里千奇百怪的内容的。Class只能用来分大类,比如人物,建筑,地图,如此分开。但每个人物,建筑,地图都可能各有不同,而同时又有相同的部分。因此,需要一种对象,来专门表述类型。
比如:一棵树。它首先可以分在物品Class里。然后,它是一颗树,所以它的类型就是树。这个世界上有很多树,都必须由这个类型来生成。但这个类型往往不光是保存“树”这样一个字符,需要保存树的特性,树是不同的,但树是植物的一种,因此树有阻挡特性,树可以被烧掉。树的各个状态有不同的显示,和树交互时的会发生什么。这些都是“树”这种东西共有的特性,是所有树肯定都有的东西。
这就是一个类型。类型一般需要保存在前台脚本里,因为他是不变的。有一点可能容易忽略,就是人物(包括玩家人物),也算类型的一种。
类型也可能分层,分大类,分小类。但细分是必要的。游戏的扩展,很大程度就是物品的功能,数量增加。
数据:数据是可以重新生成对象的最小内容。因此,正常情况下他都是直接被用来进行数据传输的。人物的数据,就是用户名,人物形象,拥有的物品一类。物品的数据,则是他的位置,他的当前状态。当然,切不可少的就是它的类型。
数据是一个世界上的物品的唯一存在。因此,它不应当拥有clone方法,应当保证这个数据所生成的所有对象都能访问到这个唯一实例。真实的物质世界,是没有复制法术的,游戏里最好也不要有。一定要复制,也应该像AS3里的一样,重新实例化一个类型,“New”,然后给它相应的内容。这能有效避免混乱。
数据是唯一的。所以所有对象都能找到自己唯一的数据。但反过来并不是这样。一个数据可能对应着多个对象的副本。因此,不能作为对象之间交互的桥梁。它只是一个保证确定性的基准。某种程度上,和类型相似。
第二层:逻辑对象
它的基类是一个Object,或者EventDispatcher。存在最主要的目的不是为了层次清晰,而是为了性能。
逻辑对象和显示对象实际上,是一个完全的一对一关系。一个逻辑对象只能生成一个显示对象,一个显示对象也包含唯一的一个逻辑对象。显示对象的初始化时间和内存占用远大于逻辑对象,分出这样一个的东西,你就可以放心将所有数据生成对象,并保存起来,直接操作他们的属性,而不用理会他们是否在屏幕中显示,理会创建需要的时间,以及占用的大量内存。
应该在不涉及显示对象的同时,尽可能将逻辑归入这里。对象之间的交互,应当尽可能只通过逻辑对象完成,而不要去涉及显示对象。
逻辑对象应当具有clone方法,因为显示对象和他是处于一对一的关系的,而显示对象的clone很难做。一个图形,可能会在界面里显示,也可能同时在状态栏里显示,或者在提示中显示,因此是有复制对象的需求的。
逻辑对象并不是桥梁,它才是主体。
第三层:显示对象
显示对象是逻辑对象根据自己的数据,生成的一个用于显示的对象,主要的目的就是让他能被addChild。它是依附于逻辑对象的,很多属性都要从对应的逻辑对象取,许多操作也需要借助逻辑对象完成。但他本身的逻辑依然不少,即使只和显示有关。诸如行走动画,高亮显示,景深排序,输入设备控制,碰撞,非常多的功能都必须在这里实现。正常来讲,会比逻辑对象内容更多。
他应当只包括必须依赖现实对象才能控制的逻辑,诸如在屏幕里的坐标,用户操作。而它也是实际放在容器和屏幕里的东西,与他们会有千丝万缕的关系,所以不可能把代码都移走。
如果不分层的话,它就是全部的内容。本身也的确就是最复杂的部分。所以的确很需要减减负。换句话说,至少不要再随便给它增加东西了。在他的内部增加代码,应当是最后的选择。虽然,“最后的选择”只有显示对象,也是很常见的事情。
以简单的纸牌游戏为例,类型是纸牌(规定了背景和颜色),数据是A12345KQ,逻辑对象一次性会被全部生成,出牌等操作归逻辑对象管,然后由逻辑对象动态生成显示对象,计算,并触发他们的动画,显示以及销毁。容器辅助显示内容,显示牌之外的部分,屏幕管理游戏流程,控制大厅和游戏界面的切换,主程序则负责通信以及控制全局。
//**结束**//
这只是一个简单的思路,真做的时候有很多东西需要复合进去,诸如资源加载,数据解析,AI。开发过程则是将各个部分分给不同的人,先从简单类做起,之后再慢慢通过继承和复合增加功能,但从一开始就应当拥有这个结构。强藕的部分在不同的人手里,要尽可能协调,力图将影响减少到最低。
MVC是个很好的东西,但它用在这种满篇继承复合接口,交叉引用频繁,仅仅为了解耦成本过高。是没有办法的事情。我个人还是只建议在UI部分使用MVC框架。当然,用不用还是你自己的自由。