2017总结

简单的总结,没以前写得好了,但还是写一下,给自己一个回顾。

今年经历了两个项目(其实是3个,但中间有一个做了一点点就放弃了), 一个是已经做了两年多的RTS项目,被停掉了;另一个是接了杭州的一个大逃杀项目的海外版(下简称R项目),主要做本地化和国际化。

先说一下RTS项目。

·战斗服和游戏服压测

我们游戏的架构是区分了战斗服和游戏服,战斗服专门用来开房间跑战斗,而游戏服则是战斗外的其他系统,所以压测这块需要分开做,衡量的标准也是不一样的。

战斗服压测这块,主要复用房间匹配机制和玩家AI,利用profile工具,以不同核数和战斗数组合测试,结合掉帧率进行对比。从理论上说,同时跑同样多的战斗,一个进程跑n个战斗的设计,要优于一个进程只跑一个战斗。 因为操作系统做进程间切换,内核态的消耗会随着进程增多而增大。但从压测结果看,这两者相差无几。原因在于,当时的战斗没做任何优化,折算成CPU的核,都是一个核只能同时跑两场战斗,再多一场掉帧变得非常严重。 这种情况下,进程切换的消耗远远小于战斗本身的消耗,就没法起更多的进程,去做对比了。

游戏服的压测,是用我们的客户端,做一下修改,跑在Linux下,主要是做了登陆压测。 当时第一次压,相当于完全没做任何优化,游戏服的性能很低。 只压100个玩家,CPU就跑满。通过profile发现,主要耗在两个地方。 一个是entity的遍历,我们很多类继承了引擎的一个基类实现,这个基类的实例会被引擎每帧遍历,而实际上我们并不需要; 另外一个是动态import,引擎hack了__import__,使得import操作的开销上升了3倍,而我们登陆过程有很多地方用了动态import。 这两个问题我通过重构,把它们优化掉,性能提高了10倍以上,算是回升到一个比较正常的基线。

做这些工作,还有类似GM指令,外围的辅助性工具、功能,有一个很重要的思路,就是尽量减少重复开发,利用游戏本身的模块,进行组合。 这样的思路,跟Linux的shell,是类似的。你会发现shell里面很少巨无霸的软件,多数是一些类似grep、awk、sort的小工具,通过管道,或者自己写的脚本进行组合,从而完成复杂的任务。 我看到过其他项目搞压测,为了显得高大上,去折腾一套框架,为了适应这套框架,写了很多额外的代码。这种做法,不但提高了开发成本,还提高了维护成本,是不可取的。

·引擎维护

工作主要包括:(1)合并主干代码,冲突的处理,合并后的调试。 (2)战斗服迭代。由于引擎C++接口的修改,合并回来后,战斗服相应逻辑也需要重做。 新版本的引擎可以指定进程起service(中心服逻辑),所以做了架构的精简,把原来的BattleManager进程改成模块,跑在匹配服上。 另外增加了一些容灾的处理,比如某台战斗服进程挂了,管理器能快速的把它去掉;还有战斗服动态扩容的逻辑等等。 (3)通信协议的切换,从tcp到kcp。主要把引擎提供的接口集成到我们的框架,调通逻辑,修复相关的bug。

引擎这块工作,算是让我学到不少东西。因为开发调试这些偏底层的代码时,为了彻底搞懂其原理,很多时候需要查一些相关资料。 比如把tcp切到kcp时,我又重温了一遍tcp的东西,而且学习了kcp的东西,对比了它们之间的优劣,知道为什么要用kcp。 工作中很多东西都是这样,如果仅仅让它work起来,其实不难,难的是搞懂它背后相关的原理。 从而做到举一反三,解决更多更复杂的问题。持续学习,是提升工作能力的最重要的途径。

·几个大系统的实现,旧系统迭代

这是主要的工作量,外部系统很多都是我做的,从底层框架到具体模块,从服务端逻辑到UI拼界面。

这些系统,做到后面,其实都大同小异,关键还是在于数据结构的设计。 数据结构,很大程度上决定代码质量,产出效率,还有维护成本。 数据结构既要设计得简单,又要有一定的可扩展性,这里面是非常考究的。 很多看起来简单清晰的框架,其实来之不易,都是经过不断的思考,不断的重构得来的。

当初我在做玩家数据框架时,对于模块划分,有两种选择,一种是多继承,另一种是组合。 实际上,引擎默认推荐的模块划分,是用多继承,它的demo给了就是一个多继承的例子。 这也是由引擎的rpc机制决定的,它只能是玩家的客户端实例与服务端实例通信。

但是,多继承的问题是非常多的,最突出的就是命名空间的冲突。 不同模块,你不能起相同名字的属性或方法名。 万一重复了,它还不报错,查起来非常花时间。 所以,我最终并没有选择多继承,而是选择组合。玩家身上挂上不同的mgr(比如hero_mgr、solider_mgr),来管理不同的模块。 模块间的访问则通过玩家对象本身,来进行函数调用。这个设计,既降低了复杂度,也避免了模块间的命名空间冲突。

但这时候有另外一个问题,mgr并不是玩家自身,如何通信。 一开始我选择的方案是把mgr也当成玩家对象,丢到全局的EntityManager里管理。 这样就可以在不改引擎的前提下,直接利用它的rpc通信机制,实现模块间的对等通信。

这个方案平时运行是没问题的,但在正常关机的操作下,会有bug。 正常关机流程,会遍历EntityManager里的entity,执行里面的destroy。 假如该entity是玩家,这个destroy还会持久化到数据库。我们的玩家及mgr,都是挂在EntityManager下的,EntityManager遍历是无序的dict遍历。 所以一个玩家destroy前,它的mgr是否先被destroy,哪些被destroy,是不确定的。 所以如果这些mgr先被destroy,然后玩家才被destroy,那么mgr上的数据则会被清空。

后来的解决方案,是改引擎的rpc通信这块,把mgr的名称,“encode”到rpc的参数entity_id里。 比如"10001-1-HeroMgr",则是调用"10001-1"这个玩家的HeroMgr模块里的方法。 最终这个改动,底层利用了python的反射机制,对使用者是透明的,可以算是比较优雅的解决了模块划分问题了。

·正式环境

负责版署、testfight、渠测的部署运维,包括编译上传流程、外服环境架构的安排,解决外服环境的运行时问题等等。

这块主要是理了一个流程,让每个人,哪怕不是很熟服务端的人都可以参与到维护。 另外是配合SA进行外服部署,规划好进程、配置文件、离线脚本、日志、报错相关的东西。 外服测试期间,解决了一些线上的问题,比如存盘丢数据,reload脚本报错等。

从一开始,我就把我们的游戏架构设计成世界同服。玩家不再有物理服务器的阻隔,而只有逻辑服这个概念,这个逻辑服只作为一个变量存在玩家身上。 这样的设计最大的好处是:简单。 跟我们以前做游戏的思路不一样,以前我们做数值游戏,喜欢物理分服,相当于对玩家数据进行水平切分。 开新服,做跨服玩法时,要考虑新旧服务器的数值公平性,额外做一些平衡服务器之间数值的逻辑。 合服时,也要额外写一些处理玩家数据合并的脚本,这些脚本其实非常容易出错,不容易写对。

而世界同服,玩家数据逻辑上是合在一起的(当然底层本质上也是做了水平切分,这是数据库Mongodb的事情),我们应用层做的更多是功能上的垂直切分,把一个个功能模块化,微服务化。 总的来说,由于玩家规模没上来,所以必定还没有把所有的问题暴露出来。 但是,我对世界同服的架构很有信心。 目前我们公司另一款DAU过千万的大逃杀游戏,它的架构跟我们几乎是一模一样,也是世界同服,可以说这样的架构也是经得住考验。


从demo开始,这个项目也做了两年多了。 虽然项目并没有成功,但它对于我个人还是有比较重要的意义的。 首先,它是一个从头做起的项目。以前M项目,虽然我也是承担服务端核心的工作,但我在加入到这个团队的时候,它其实已经做完了,甚至已经成功。 这个项目不一样,整个服务端代码,我实现了从无到有,甚至从服务端做到了客户端,包括很多重要的模块和工具实现。 其次,这个项目的复杂度,对我来说是个非常好的锻炼。 当中遇到和解决了大量的问题,也为日后的工作积累了宝贵的经验。可以说,后面遇到复杂度相似的项目,甚至高一个级别的,我也有把握去实现。 最后,这个项目给了我足够的技术发挥空间,让我实现一些技术上的想法。 一些我在以前项目想做,但由于历史原因和架构限制,编程语言限制,而无法实现的想法。

项目的失败我并不觉得意外,失败是一件很正常、很常见的事情。关键是,我们要从这个过程中总结出一些经验,好的继续发扬,不好的下次把它做好。 在M项目的时候,我认为服务端有很多东西,包括架构,模块的设计,数据存储的设计,跨服的设计,都不是做得很好。 使得大家在上面继续开发,心智负担非常高。 所以在新项目的时候,我就把这块从底层开始设计好,到上层具体实现都把它做好。定好一些规范,让不同人参与开发都能提高效率,减少维护成本。 可以说,这个项目的代码,比M项目那套好了很多,提升了一个档次。但是不是说已经很完美,后面就没有改进的空间呢?我认为也不是。 架构上,大体上,我觉得是可以的,但有些工具,或者有些细节的实现上,我觉得还能做得更好。 每次做新项目,我认为并不是把以前做过的东西再做一遍。而是把以前好的东西拿过来,不好的东西加以改进,从而做出一个更好的东西。


接下来稍微讲下R项目的工作。

这个项目,工作更偏向于维护、跟进,而不是开发。 代码需要每周合并国服的,所以避免冲突、解决冲突本身就要花很大的精力。 大的方向不能自主决定,得跟着国服走。 架构上过于复杂,本身是用来做mmo的框架,改造成开房间游戏。还架了一堆用不同语言写的外围服务,数据存到各个地方。 加上本身代码也写得不好,过度设计的地方很多,维护成本极高。 很多看不懂,理解不了的地方,经常要找国服的人问,沟通成本高。 无论是在自己分支还是在国服分支开发,代码都要来回合并,工作受到很大限制,效率低下。 收获肯定是有的,也学到不少东西。但我看到的,更多是一些缺乏经验,缺乏规范,缺乏思考,缺乏权衡,过度设计的东西。 这当中有一部分是历史原因,框架的问题,游戏的转型,工期的压迫,但主要还是代码没写好。所以这个项目代码里的东西,大部分我是不会带到下个项目中去,而仅仅作为一个反面教材。

很多人觉得,只要项目成功就行,代码写得好不好并不重要。这个观点并不是错,但太片面。对比了这两个项目,我的感受特别深。 我认为是这样的, 好的代码,让产品的质量提高一个档次,不但bug更少,维护起来也更加轻松;相反,不好的代码,更容易出bug,每次更新都一堆问题,时间都浪费在打补丁修复,维护起来心智负担很重。 好的代码,你在上面做迭代开发,很简单,需要耗费的时间基本等于这个制作必需要花费的人天;不好的代码,逻辑非常绕,阅读起来比自己开发还要耗时间。 而且本身设计很脆弱,稍微改下就出问题。本来一人天的工作,在这种代码上开发,得花到两人天。 好的代码,无论后面怎么加人,代码的质量和规范都能保持,因为一个新人总会倾向于模仿已有的代码风格和实现方法; 不好的代码,在团队扩大后只会变得越来越差,代码里有一种不好的实现方法后很快会出现第二种,恶性循环,直到无法控制。

但是,有一点我是非常佩服国服团队,就是执行力。他们跟我们RTS项目立项的时间差不多,本来是个mmo游戏,也做了两年多了。 直到2017年的7月份,突然决定把原来的类型改成大逃杀,到10月份上线,仅仅用了3个月时间。 一个产品成功,既有客观因素,也有主观因素。 客观因素可以简单的理解为,大逃杀这个游戏模式在全球市场非常受欢迎,是风口; 主观因素我认为是团队的执行力,这点是非常值得我们学习的。 反观我们原来做项目的时候,执行力就有很大的问题。 玩法设计上经常各执己见,工作无法开展; 每周周版本进度不主动跟进,经常拖到最后也无法完成,整个团队被动加班。 目标模糊,对产品开发把控不足,做了很多无用功,浪费了很多时间。 抛开RTS市场并不乐观的因素,我们这样的开发节奏,也是很难成功的。

做了两年多的项目挂了,却接手了一个受欢迎的游戏,目前在海外成绩不错,也算是换了一种方式成功。 但是,我们应该有更长远的目标。这个游戏毕竟不是我们自己从头做起, 我们后面还是需要做自己的产品,创造真正属于自己的价值。


2018年2月