架构和框架这些概念听起来很遥远,让很多初学者不明觉厉。会产生“等自己技术牛逼了再去做架构或者搭建框架”这样的想法。在这里笔者可以很肯定地告诉大家,初学者是完全可以去做这些事情的。
架构和框架是非常接地气的,离我们其实并不遥远。
架构是一个约定,一个规则,一个大家都懂得遵守的共识。那这是什么样的约定、什么样的规则、什么样的共识呢?
我以包为例,我经常出差,双肩背包里装了不少东西。笔记本电脑、电源、2个上网卡、鼠标、USB线、一盒大的名片、一盒小的名片、口香糖、Mini-DisplayPort转VGA接口、U盘、几根笔、小螺丝刀、洗漱用品、干净衣服、袜子、香水、老婆给我带的抹脸膏(她嫌我最近累,脸有点黄)、钱包、Token卡、耳机、纸巾、USB线、U盘等。这个包有很多格子,最外面的格子我放常用的,比如笔、纸、一盒小的名片等;中间的格子一般放的是衣服、袜子、洗漱用品、香水等;靠背的那个大格子放了笔记本电脑,和笔记本电脑相近的小格子放的是两个上网卡、Mini-DisplayPort转VGA接口、大盒名片、记事本,和笔记本电脑相近的大格子放的是电源、鼠标、口香糖等。
我闭着眼睛都可以将我的东西从包里掏出来,闭着眼睛都可以将东西塞到包里!但是,非常不幸的是,一旦我老婆整理过我的包,那我就很惨了,老是因为找不到东西而变得抓狂!更不幸的,要是我那个不到两岁的“小可爱”翻过,就更不得了了。
框架(framework)是一个框子--指其约束性,也是一个架子--指其支撑性。——360百科
本小节对框架和架构概念做了简单的认识,得出了以下两个结论:
两年前,笔者毕业半年,刚从cocos2d转Unity不到两个月,当时所在的公司有一套游戏开发框架。笔者用它做了两个月的项目,使用框架做项目的时候并没有去思考框架是什么,只是开始的时候觉得很新鲜,而且越用越顺手,尝到了它的甜头。
后来笔者接到了一个跑酷游戏项目,于是就把工作辞掉了,决定出来全职做这个项目。辞职后,公司的框架由于保密协议就不可以用了。项目就只能从零开始开发,那么结果就是在跑酷项目的开发的过程中各种中水土不服。
于是,笔者就开始了市面上开源框架的选型,折腾了几天,发现要么上手太难,要么学习成本很高文档不齐全,有的框架光是理解概念就要很久,对于像笔者一样刚毕业的初学者来说,市面上的开源框架真的很不友好。
从那时候笔者就决定要为自己,开发一套符合自己使用习惯的框架,也就是现在的QFramework。
笔者在做cocos2dx的时候,市面上有个叫Quick-Cocos2d-x的开源框架,用两个词形容就是简单、强大。
笔者认为好的工具就应该简单。
QFramework的目标是要做到像Quick-Cocos2d-x一样“简单、强大”。当时笔者纠结过很多名字,比如QuickEngine,QuickUnity等等。Q代表Quick,并且Q这个字母给人感觉灵活有弹性,所以最终确定为QFramework。
在决定要做框架之后,笔者就开始了边搭建框架边进行着跑酷项目开发的工作生活。
一个项目开始立项的时候,最常见的一个情况就是:几个人一个小团队,开始什么也不做,开始写代码,验证逻辑,然后game就开始写起来了。公司的一些的所谓的领导层一开始就把游戏定义为“我们要做的一个大作”,那么这个事情本身就是一个笑话。没有任何的规划和设计,我们就妄图就写出一个所谓的杰出的作品出来是不现实的。Unity再好用,以这个心态去做游戏,一定会写不出来好的游戏来。——刘钢《Unity项目架构和开发管理》
看到视频中的这段话,吓得笔者赶紧为跑酷项目做了些准备。比如最常见的表现和逻辑的分离。
我们大家都知道,做项目尽可能地要把表现和逻辑分离。同样的跑酷项目也是如此,而最常用最经典的方式就是使用MVC。
跑酷项目代码的架构使用的是很简单的MVC。笔者当时是按照如下的方式去进行划分的:
从今天的来看,这种MVC设计是一种很粗糙的设计,尤其是其Model层,颗粒度太大,其实再可以分出个DataAccess层。不过粗糙的好处就是初学者能够驾驭,是有存在的意义的。
到这里,架构这个词终于出现了。MVC是一种架构模式,对程序进行MVC的划分是在进行架构活动。除了MVC架构模式,还有几种其他的架构模式。
而对程序进行MVC的划分,实际上是对代码进行结构的设计,所以对程序进行结构的设计是在进行架构活动。
到这里,我们知道了架构是我们每天都在做的事情(划分MVC或者说将代码的表现与逻辑层分离)。而对代码进行结构的设是否和架构的“约定、规则、共识”有关系呢?答案是肯定的。我们接着往下进行探索。
很多时候我们说的一个所谓的好的架构,直接就等于你要有一个好的标准,指定一些好的规则。——刘钢《Unity架构设计与开发管理》
我们在起一些文件夹的名字的时候,尽量和我们的GameObject对应起来,如果我们的GameObject叫做PoolManager,我下面所有的代码也都起一个同样的名字叫PoolManager,那么这样在以后找对应的程序结构的时候,会比较好理解一些。——刘钢《Unity架构设计与开发管理》
而跑酷项目最初的部分代码文件结构如下:
对于MVC的文件结构的划分,笔者在进行跑酷项目之前的其他项目的时候也尝试了别的方案。当时觉得这种方式找起来最方便。
在项目结束之后不久看到了一篇吴秦前辈的一篇好文《Unity3D手游开发实践》:
一般客户端用得比较多的MVC框架,怎么划分目录?
先按业务功能划分,再按照MVC来划分。“蛋糕心语”就是使用的这种方式。
根据使用习惯,可以自行选择。个人推荐“先按业务功能划分,再按照MVC来划分”,使得模块更聚焦(高内聚),第二种方式用多了发现随着项目的运营模块增多,没有第一种那么好维护。——吴秦《Unity3D手游开发实践》
而笔者所采用的方式就是“先按照业务功能划分,再按照MVC来划分”。在这里仅仅是个建议,并不是一定要使用的方式。
在跑酷项目的MVC中,笔者把Model层设计成了单例。为什么使用单例呢?
抛开粗糙的设计不说,先让我们简单分析下跑酷项目的Model层的特点:
很自然地就想到了使用单例模式实现。而很多开源库里都会提供可复用的单例模板,一般叫Singleton和MonoSingleton。所以单例的模板作为第一个工具被收录到框架中。
在写一个项目的时候,不要短视的说我就把这个项目做完了,就是交一个差上线了就完了,我们希望每写一个游戏的时候,我们都积累一些东西,把写的每一行代码,都当成是一个可以收藏的,甚至是可以传递下去的这样的一个资产。有了这样一个思想,可能我们在写代码的时候,整个的思维模式会完全不一样。——刘钢《Unity项目架构和开发管理》
听到上面这段话,在当时的笔者心中埋下了一颗种子。在这个基础上进行思考,会产生很多很有价值的想法。本小节先讲到这里。
图中的需求收集/业务分析是本小节没有讲的,由于这个跑酷项目是合作项目,有一些需求合作方本身也没太想清楚,在进行准备这个阶段之前,跑酷项目已经完成了一版Demo进行了需求上的确认。
由于受到“代码是资产”思维的影响,QFramework的开发模式最初是以收集工具为目的的,此时的QFramework并不是真正意义上的框架,而是一个库或一个工具集。
在准备完毕之后,跑酷项目就开始了大量的业务/逻辑开发。
在做跑酷项目时,笔者当时的水平怎么样呢?三个字,非常菜。有很多很基本的功能要学习UnityAPI才能完成,比如跑酷的关卡生成器等等。对C#语言的掌握也是靠着以前一点Java经验,才能勉强能应付逻辑开发,之前所说的单例的模板,也只是知道怎么去用,并不知道实现的原理。这时候笔者觉得必须要对C#进行基础学习。于是就开始每天看一点传智的C#基础视频。学习的过程中,一些语言特性不知道怎么用,而有的语言特性觉得很有用。所以此时只是为了完成项目而进行学习,自然而然地就没有太多的精力去深究语法细节。我们大家都知道,这不是一个很好的学习方式。
随着对C#语言的了解程度加深,慢慢地可以看懂一些工具的源码了,也可以自己实现一些很简单的封装。而笔者在跑酷项目的开发期间先后收录了有限状态机、消息分发器和一些数学工具。以上收集的工具与单例的模板一样,都是同一性质的工具,所以这里没什么好说的。值得一提的是笔者当时做了一件事,笔者按照之前cocos2dx的使用习惯把一些Unity的API简单封装了一下,最初这么做只是为了提高自己的开发效率,扩大自己在Unity里的舒适区(笔者之二前一种用cocos2d)。做了这件事之后给了笔者很大启发,笔者为什么不把一些新学习的Unity的API或者C#特性简单封装一下然后收录到QFramework中呢?这样以后使用这些API的时候就不用再查询搜索引擎了,直接使用封装的工具就好了。这样还能让QFramework帮助笔者“记住”Unity的API和C#的特性。从那以后QFramework不止是一个工具集,也是笔者的一个知识积累工具。这样耗能解决上文中笔者对“学习的知识没有用武之地”的困扰。这样既能激发笔者的学习动力,又对QFramework本身也有好处,一石二鸟。
我们都知道,做一个游戏项目,都会用到UI、音效、配置表和数据存储等模块。跑酷项目也是一样的,在刘钢老师的《Unity架构设计与开发管理》视频里提出了一个叫做ManagerOfManagers的架构方案,可以把以上模块全部做成一个单例,比如UIManager,AudioManager等。而笔者认为,这些工具模块都是为了支撑游戏业务的,比如游戏音频管理方案,界面层级管理方案等等。也就是说大多数项目都用得到。而不像单例的模板、有限状态这些工具,它们不是为了支撑业务而积累的。为什么这么说呢?单例的模板是设计工具,解决的问题不是业务问题是设计问题,而有限状态机则是一种数据结构,是简化一部分问题的思维模型。而UIManager、AudioManager等等。每个模块都是独立的解决方案,是为了解决某一业务问题而设计的。所以笔者在这里称它们为业务支撑工具。
而刘钢老师的在视频中列出了以下模块:
这里除了GameManager以外,其他的全部可以在别的项目中复用。
这时跑酷项目中已经实现了
这两个工具自然也收录到了QFramework中。这样QFramework的目标有了一些变化。而QFramework除了是工具集和知识积累工具之外,还是一个支撑业务工具的集合。而之后笔者要做的就是把就是ManagerOfManagers提出的这些模块一一收录。
很快,跑酷项目接近尾声。拿到结款后分析了一下当时工作生活状态的利弊。决定还是找一家公司继续沉淀。一是为了让QFramework多接触一些不一样的项目,二是笔者非常渴望与同行能交流的,三是开发跑酷项目的这两个半月对未知的恐惧太强烈了,比如对未掌握的技术,没把握实现的功能等等,四是合作方的第二个项目需要组个完整团队去做,当时身边没有太合适的人。总之跑酷这个项目到此完美结束了。
2016年3月下旬,跑酷项目做完之后来到了一家新的公司,来的时候公司已经具有一定的规模,其游戏技术团队也积累了一些Unity的插件和工具。而笔者所加入的团队是技术支持团队。技术支持的工作就是平时负责攻克技术难点,做一些预研,再做一些工具来给项目团队使用,有的时候项目人手不够了还要顶上去。而在这家公司的两年则是笔者成长最快的两年,一是遇到了好Leader,二是做的事情非常喜欢。
AssetBundleManager是看一个公司项目时候看到的,本身是一个开源免费的AssetStore插件。笔者之前对于资源管理没有太大的概念。像之前做的跑酷项目,都是直接都把GameObject拖到场景里完成的。很少用到动态加载卸载内存。但是看了AssetBundleManager之后,很看好它的SimulationMode。所以就收录到QFramework里了。使用AssetBundle的好处有很多,支持热更啊,控制包体大小啊等等。缺点就是坑多,而且有一些学习成本,但是还是非常值得去研究的。
本小节的UIManager和AssetBundleManager都是一种很普通的实现,没有太大的亮点。
Unity好的规则2:我们在命名的时候要起一个比较有含义的名字。——刘钢《Unity架构设计与开发管理》
在笔者刚毕业的时候,读了《代码大全》这一本书,其中第11章的《变量名的力量》反复读了三遍。这一章的内容对笔者之后养成良好的命名习惯产生很大的影响。书中重点讲了如何命名和编码规范的重要性。
以上当然要靠一个编码规范是无法完全解决的,除了编码规范之外,还有资源命名规范,项目结构规范等等。经过多次因为以上原因的加班之后,深有感触。编码规范是非常有必要做的。
在公司,笔者还是最基础的员工,没有什么权利。所以对于定义规范这种事情,在公司想想就好了。不过这并不阻碍笔者为自己制定一个编码规范。于是笔者根据自己的编码习惯,定制了如下的编码规范。
一幢有少许破窗的建筑为例,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快的,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上。这个现象,就是犯罪心理学中的破窗效应。
我们做项目也是一样的。一定要好好写代码,不要让“破窗”在我们的项目中发生,不能让项目有任何变混乱的趋势,保持项目清爽,这可以给我们开发者到来很好的工作体验,也就是所谓的心流体验。
在一些命名格式上,可以遵循编码规范就好了。但是如何给一个类/方法/变量/枚举命名呢?
在问这个问题前,我们来问另外一个问题,那就是程序语言,所谓的语言是给谁看的?一是给计算机或者编译器能看懂。二是给我们人类看的。让计算器或者编译器看懂很容易,只要遵循程序的语法去写就OK了。但是如何让人更容易看懂,当然答案也很简单,就是好好命名。关于如何命名,一些笔者至今受用的命名准则这里这里简单介绍下。
方法/函数命名用谓语+宾语方式命名比如PlayerData.Save,或者SavePlayerData
类名和方法参数使用名词
表示一个动作状态时通过动词的不同时态进行命名。
比如Connecting,Connected,Connect表示连接的三种状态。
关于命名和规范就先讲到这里,命名是一门学问,其内容多得可以去写一本书去介绍了。如果想深入学习,建议首先看《代码大全》的第11章《命名的力量》。
还记得在前边说的架构的定义嘛?架构是“约定、规则、共识”,而确定各种规范也是准备阶段要做的事情,也是架构的一部分。
publicenumMgrId{ UI=0*3000, Audio=1*3000, ...}publicenumUIXXEvent{ Start=(ushort)MgrId.UI, XX, YY, End,}publicenumUIYYEvent{ Start=(ushort)UIXXEvent.End, ZZ, End,}笔者当时看到这里才觉得自己对语言的了解真的是很浅,一个简单的ushort+枚举就可以很巧妙地设计出基于模块的消息框架,这种思想非常值得借鉴。笔者马上在QFramework中实现了一套类似的消息框架。很简单,一个QMsgCenter充当跨模块之间的消息转发。一个QMgrBehaviour作为模块的基类,负责收发和注册模块内的消息,一个QMonoBehaviour只要一个脚本继承它,就可以发送消息和注册处理消息。而事实上,有了这套消息框架,QFramework才算是一个真正的ManagerOfManagers框架。
公司的以为前辈也有一套类似的框架,不过在以上这套消息框架的基础之上,做了UI的脚本生成。在此之前笔者都是用transform.Find方式来获取感兴趣的UI控件的。比如Button、Image等等。而前辈的UI脚本生成省去了这些工作量。实现方式也是比较容易理解。就是在一个UI的Prefab上,对于感兴趣的控件挂上一个脚本,比如UIMark/UIBind。然后从UI的Prefab的Root开始进行深度优先搜索。搜索过程中记录每个标记脚本的路径,之后根据路径生成一行行的transform.Find(路径)就好了。而这个工具则是节省了制作UIPrefab过程中的体力劳动。是对工作流上的优化。QFramework又收录了一个工具。
C#真的是越用越觉得它的强大。QFramework的进步是离不开C#语言的学习的。这里笔者遇到了一个决定QFramework未来的语法特性,就是静态this扩展。语法细节这里不多说,大家自行百度。学习了这个语法之后,一些本来要靠继承才能实现的cocos2dx风格的API全部可以用这个语法实现。简直不要太好用!都后来的链式结构编程全都是以这个为基础的。
在笔者的坚持下,经过了团队的CodeReview之后,大家终于统一了使用QFramework作为公司的框架。从这时候开始QFramework开始飞速发展。
在做第一个项目的时候,来了一位大牛,带着一套MMO框架。框架好用的工具真的很多。
其中的EventSystem(消息系统)和ResSystem(资源系统)是两大亮点。EventSystem的EventId是用泛型进行注册的。把一个泛型转换为int。这个解决了之前笔者注册事件时需要把枚举强转成ushort的问题,这样的代码写起来很不愉快,于是笔者把原来MgrBehaviour和QMonoBehaviour里关于消息注册和转发的代码杀掉,直接换成了EventSystem就OK了,QMonoBehaviour和MgrBehaviour里的代码变得非常精简。而ResSystem使用非常简单和强大。ResSystem是在AssetBundleManager的功能基础之上有抽象出来了ResLoader。这样做有什么好处呢?
首先AssetBundleManager在哪里加载了什么资源和卸载了资源需要使用人脑进行记忆,项目体量很大时很容易由于忘记卸载资源而造成内存泄露。而ResLoader是一个对象,可以每个界面都申请一个ResLoader对象。所有在这个界面加载过的资源的信息都会记录到ResLoader里,而卸载很简单,只要在OnDestroy里直接进行ResLoader的卸载就好了,非常方便。但是这时候笔者已经用惯了AssetBundleManager的打包方式,所以只收录了ResSystem中除打包以外的代码。这里简单提一下,ResLoader是用对象池实现对象的申请和回收的。而ResSystem里的资源积累则是使用引用计数器决定资源的释放的。在这里QFramework收录了EventSystem、ResSystem、引用计数器、对象池,可以说收获颇丰。
架构是“约定、规则、共识”
框架具有约束性和支撑性
好的架构直接就等于你要有一套好的规则,好的准则
Unity好的规则
MVC文件结构:先按照业务功能划分,再按照MVC来划分
代码是资产
做完每一个项目都积累一些东西
QFramework*工具集*FSM*Singleton&MonoSingleton*QEventSystem*MathUtils*笔者知识积累工具*业务支撑工具集(支撑性)*UIKit*UIManager*代码生成*MgrBehaviour、MonoBehaivour(约束性)*ResKit*AudioManager*ActionKit*建议1.为每个UI界面都建立一个测试场景。2.为每个UI界面都提供一个Init接口。3.UI界面的Init接口传数据,而不是在UI里面去访问某个ManagerOrInstance。
在项目准备的架构活动
这里可以得出框架与架构关系的结论,框架可以解决一部分架构问题,使用框架本身就是一种“约定、规则、共识”。