近日拜读了顾煜大佬天刀开发历程回顾,颇有所得,尝试记录一二(其中灰色底色文字是一些精彩内容的引用),以待后续工作能有所应用,参考文献[1]中给出了所有文章链接,感兴趣同学可以跳转过去查阅详情。
1. 缘起
第一个验证产品理念的Demo,应该足够小,足够敏捷,即使是单机也无所谓;纯单机开发快,但是后续移植会比较麻烦,早期开发选定了一个折中方案:针对当前MMO游戏,分析协议,做了一个极简版本的本地server,随客户端一起启动,满足了登录和移动角色、生成NPC。这样整个项目是自包含的,不用顾虑服务器客户端开发,也便于移动,也能确保我可以尽量重用原有MMO游戏的很多高层结构、UI等等,虽然那些将来都可能推翻,但至少在启动阶段,看上去有模有样,很快就有了一个雏形。
2. 破局
第一个切入点非常重要,但很难找到,难题主要来自两方面,一方面团队能做什么,另一方面是团队该做什么。
这样的启动团队勉强够用,一个多面手小方负责杂事,一个渲染Tough哥负责提升画面质量,我到处帮忙和救火。
管理不是谁有空谁堵漏,而是认清各位同学的专长与特质,并针对性的进行统筹安排,做到物尽其用,人尽其才。
如何认清呢?先从大的维度进行划分,比如个人经验能力兴趣,再做细分。根据他想做什么,他能做什么,可以划分成四个象限,按照T型方式进行培养(如果是可造之材),不断拓宽个人能力边界,同时又能在某个领域塑造自己的波峰与技术壁垒。
第二个问题是该做什么,怎么才能打动老板呢?市面上的MMO成百上千,各有特色,武侠作为一个中国玩家喜闻乐见的类型,往杀马特化、仙侠化、魔兽化方向一路狂奔。怎么在夹缝中找一个生存空间呢?
项目不是做慈善,老板也不可能无限制的等你出成果,早期设计阶段应该要尽快的确定突破方向,尽早用让人眼前一亮的成果来确保生命线的延续,否则可能就会面临demo还没完成就中道崩殂的风险。
时间就是生命,这句话在项目早期的时候体现得淋漓尽致。
早期引擎不成熟,美术只有一个半人。所以我们不能用画面作为第一个切入点。
项目常见的做法是前几个版本猛堆资源,画面表现做到最好,换得老板好感,以后再慢慢优化,所以性能也不能作为切入点。
我们希望有更多动画上的表现,更有动感,而不是通过特效上的表现,掩盖动画的缺失,这样才能让战斗有更强的观赏性。
先用美貌将人勾住,再找机会慢慢谈品德,上来就之乎者也的人家可能会说你神经病。
在天涯明月刀后续的开发流程中,类似的斗争不停的出现,老于总是扮演着上帝之鞭的角色,从最苛刻的角度,提出最奇怪的要求。期间大小冲突不断。老于低调、冷静、坚硬、固执,你认同也好,不认同也好,他就像最厚的墙,越不过(翻墙违法),推不动(体重问题),说不服(口才不敌)。
demo阶段的苛刻细致是好是坏,恐怕不同的人有不同的见解,天刀开发过程中老于的细致促进了品质的提升,但是也同时耗费了大量的人力物力在一些过于细节的打磨上,哪些细节是必须的,哪些细节则是可以忽略不计的,恐怕需要考验一下眼力。私人愚见,一些核心特性的细节需要着重打磨,一些边角特性的细节可以暂时忽略。
3. 初啼
前两篇文章批评的声音不少,有不少批评,真知灼见,言之凿凿,深表感谢。
对那些为喷而喷的同学,想说几句:
我写,从中找到乐趣,你喷,也找到优越感。有动机高下之分,无价值贵贱之别,都是为知乎服务,这原本无可厚非。
只希望纯喷的时候,可以喷得更有趣味,让其他读者也能快乐的阅读。
另外一时间看见这么多喷,心里也颇为忐忑。但我迅速调整了心态,文章光追求情节流畅和阅读乐趣还不够,还要追求更高的可喷性,后续行文会做调整,重要槽点会有特别提示的tag,方便大家集火秒杀。
大佬的心态就是与众不同,即使遇到了一些无理的评论,依然能够保持克制(貌似)心平气和的发文,仿佛看到了金庸老爷子笔下“他强由他强,清风拂山岗”的境界。
凝聚团队有很多方法,大致来说,短线靠哄骗,长线靠愿景。天刀团队有意无意间,在这几点上做得都不错。
短线怎么哄,基本是靠Leader们和组员谈心。天刀几个带团队的Leader性格大多柔顺,信奉天下没有咽不下的鸟气,默默做着居委会大妈的工作,劝员工们将就着把日子过下去。
美术原画来抱怨,表示老板审美不对啊,我画得辣么好,他不喜欢。主美LXP和她谈要接地气,审美要向劳动人民靠拢,劳动人民是先进生产力的代表,曲高和寡的艺术终究是没有生命力的。美术动画又表示动作不好做啊,老板要求太高,我们就只有这点能力,LXP和他谈人有多大胆,地有多高产,你的征程是星辰大海,怎能被区区一个老于挡住视线。
渲染的Tough哥表示新做的渲染效果又被老于砍了,多年的技术积累不能放进版本,心里不爽啊。我表示凡存在必合理,但死去也有价值。大海生生不息,孕育生命,死去的巨鲸,化作鲸落,腐化和孵化,滋养着小生态。每一行被砍的代码,每一次不甘的怨气,每一场失败的争论,终成鲸落。
从这里看出leader的另一个作用,团队的粘合剂润滑剂,用各种各样的技巧应付团队短期或者长期的散伙风险,自行车之所以不倒,关键还在于有一个不断向前的驱动力,这其实也跟国家危机时用外患压制内患的策略一样(参见Sam大叔),人民有怨气怎么办,通过外部矛盾转移一下,当然,这种做法并不是可持续的,一张一弛文武之道,关键还是要解决根本问题,让人民老百姓自发自驱的拥戴政府,换到管理工作上,可能就是让各位同学有自我驱动精益求精的追求与渴望,无需领导扬鞭就能做到奋蹄向前,这应该就是上面说的长期靠愿景了。
小方表示,老板太任性了,不就是做个小Demo嘛,怎么就这么高要求,咱们快点做新功能才是王道。我和他谈开发理念,说Vertical Slice
Vertical Slice是游戏行业引入的一个里程碑概念,指的是某个项目的阶段性目标的定义是整个项目在从上到下从里到外的各个模块在对应指标上的都应该要达标,举个例子,游戏可能会分为三个Layer,最上层的UI层,中间的逻辑层,底层的数据层,那么一个Vertical Slice定义的里程碑应该是上述三个Layer的对应完成度描述:比如UI应该完成到什么程度,数据框架应该搭建到什么程度,逻辑部分应该完成到什么程度等,详情可以参考相关wiki。
项目长线的凝聚力,来自于Surprising Driven Development,我所谓的惊喜驱动。Surprising Driven Development有两层含义,一个含义是对内的驱动力,指团队的自我激励,还有一个含义是指版本对外的目标设定。
长线驱动有两个作用力,一个是内部驱动力,来自于团队成员在各自工作上大干一场证明以及创造价值的渴望;另一个是外部压力,来自于上层对项目存续威胁的push。
内部驱动力从后面的描述来看,相关工作对团队成员的成就感的促进与增强是一个方向,这可以归属到马斯洛需求层次理论中最高一等的自我实现中。
凡是我不想做的,就弄成灵活的参数,让策划自己去调整。
By 何老师
据说有技术背景的老于一看众多参数,就晕了。但他临危不乱,强撑内心的不适,表示现在已经差不多了,就这么着吧,别调了...终于在汇报前最后一周搞定这个问题了。
这可能是程序应付策划的一点经验之谈了,不过如果这个工具是给用户使用的话,这种做法就不可取了。
4. 试炼
这篇主要介绍如何在项目开发过程中进行向上管理,通过持续不断的向上层管理与决策者输出惊喜的方式来增加项目的重量,其实是求生欲的一种体现,一些大公司,大工作室因为早年开发的爆款产品获得了源源不断的持续输血,在经济上不存在过多压力,对底层项目的期许期待与审核严谨程度相应就会降低,而这种做法可能就会导致一些60~80分的产品通过审核进入开发序列,相应的成员就没有精力与动力去思索如何将产品做到90分以上,最终导致了外发项目上的后继乏力,一句话总结就是缺少了乔布斯所说的stay hungry的精神,这种困境要时刻警惕,管理越往上走,越会面临这类考验。
经常做AAA游戏,意识到了消费者刁钻的口味和我们落后的制作能力之间有着不可调和的矛盾。AAA游戏既要有对得起水准的平均质量,又需要有超出玩家理解的精彩闪光点,也就是他们称之为Wow moment的时刻,意思就是玩家看见了会情不自禁的Wow叫出来,惊呼这个体验太牛逼了。这个意料之外的表现,对产品整体质量更上一层楼,对玩家口碑营造,都有很高的价值。
天刀的每一个里程碑,都试图设置一些超出预期的惊喜,这是对自己的突破,也能给上级一些继续支持我们的理由。
资源有限,做不了加法,就先做减法,把不重要的都去掉,再做乘法,把重要的特性强化。
5. 远征
这一篇介绍天刀开发过程中使用早期引擎所暴露出来的一些问题,虽然开发人员非常想要对底层进行重构,但是在站在制作人的角度,一切以项目目标为依归,在不影响整体项目里程碑的前提下,一切好说,如果对里程碑造成了影响,那么就需要将一些暂时没那么重要的工作先搁置,等后面挤出时间来再做考虑。
6. 原则
这篇文章介绍了在做上一篇提到的引擎升级与重制过程中的一些开发原则:
1. 不追求完美,Quick & Dirty,先能用再说
引擎跟项目都是一个非常庞大的工程,如果要精益求精的话,恐怕穷尽毕生之力都难以完成,因此首要的目标是聚焦在核心要点上,之后根据需要逐步完善周边逻辑,而在核心要点的开发上,也不必将所有的方方面面都考虑到了才着手进行代码编写,纸上得来终觉浅,理论分析终究过于表面,最佳的分析方式还是找一个能够接受的方案进行实践,之后再在这个基础上进行升级改造,当然,如果最开始就能够确定一个最佳方案那是最好不过。
2. 整个项目最重要的目的不是开发引擎,是产品本身
引擎是为项目服务的,不要为了开发引擎而开发引擎,而是应该从项目需求出发,解决实际问题为第一优先级。所以在引擎的开发过程中,要与项目的使用紧密结合,分兵两路互不干扰的开发方式看起来很好,在最终大坝合龙的时候可能会出现根本性的偏差,这一点要有所警惕。
3. 能用中间件,绝不自己写
经济实惠,能省就省。
在UBISOFT开发项目,也是大量应用中间件,但UBISOFT有一个原则,就是中间件必须有源码,license要经过严格确认才可以用。
4. 开发流程和易用性至上
开发过程最重要的三件事情,效率、效率、效率!时间就是生命,这已经是不言而喻的事情。
没有Undo/redo的工具就是耍流氓,Editor开发第一时刻就把Undo/Redo加上了。
5. 双线开发,主线+备胎线
短期收益与长期收益,我全都要!兼顾项目开发稳定性与引擎大功能的有序推进。
大量全局改动,有风险、带研究性质的feature,我们都安排一个长线的备胎任务,让某个同学在很长的周期里面完成,但主线永远不停。通过合理安排研究和主线比例,我们不停给版本加入了有趣的高端特性。
工作到3-4年的时候,天刀引擎才正式成型。
参考下天刀引擎的开发时间,如果后续团队有类似的需求可以大致心里有底,不过因为QuickSilver是为PC设计的,如果要考虑移动端跨平台,工作量可能还要递增。
7. 休整
我问老于招不到人怎么办,老于两手一摊:你要努力啊,我帮不了你。
我问HR怎么办,HR两手一摊:已经找猎头去物色人选了,耐心等等。
我向他表示:好棒,我等你好消息。
过了几天老于问:你招到人了吗?
我两手一摊,表示:我已经努力了,还是招不到人。
老于也不着急,安慰我:说慢慢招,我不着急,不过人力编制你用不掉,我就先给策划团队去用了哦...
有了竞争,才有动力,每个团队都被老于驱动着,想尽快找到合适的人。想必他们几个团队,也经常听到老于说:招不到人,我就先把编制给客户端团队去用了。
连HC都得竞争,老于深谙赛马精髓。
当我们在走一条确定的路上,团队都很了解我们要做什么,我们的目标在哪里,团队磨合都很好,有足够的经验和信任,那么Surprising Driven Development通常是一个不必要的事情。只有彼此心存狐疑,目标遥不可及,士气低落无依,我们才需要阶段性的输出打鸡血,定期集合,看看路有没有走偏,随时汇报,确保一切尽在控制。
SDD其实不只是向上管理的方法,同时也是给团队内部找坐标的手段。
8. 重启
天刀开发过程中,好多次面对这样两难的选择,需要在信息不完备的情况下做出技术选择。我们能做的非常有限,先尽其所能,收集更多的信息,做更多的评估,权衡更多的利弊得失,降低整个技术方案的不确定性。
信息不完备情况下的决策,非常依赖决策人的经验,有很大的风险,如果人力不足,就只能强制二选一,相当于一次赌博。如果人力资源充分,我们可以有fallback方案,一部分人尝试新方案,另一部分人继续做原来的方案,这样发现情况不妙还来得及退回原点。
虽然在决策前,大家的心都很慌,但是在明确了自己所站的位置以及所面临的问题之后,还是只能在两个按钮中按下一个,至于成王败寇,还是要称量一下才知道。
9. 工具
项目开发过程中的一些工具开发细节,略
10. 回收
天刀垃圾回收算法的设计与实践,无太大参考意义,唯有GC性能优化一节中对问题的不断深入与思考的探索方式值得学习,其他可略。
11. 首秀
里面简单介绍了天刀全局光照尝试与Streaming策略的引入,同时还介绍了游戏封测阶段的Content Freeze方案,到了今天,这些内容都是成熟的,没有过多介绍价值,略。
12. 新的旅程
这个版本暴露出了一些问题:
1.规模越来越大,开发迭代变慢了。光用ssd,已经不足以满足我们的胃口,我们还希望编译过程更快更好。
2.地图开发成为一个巨大的问题。传统的开发流程,已经很难支撑这个项目的规模,我们想做更大的地图,更多的内容。而且地图的质量也亟待提高。
3.性能也需要逐渐纳入考虑,技术评审亮了警告,在这个时刻,分明将开发过程分为两段。往前看,是预研,可以任性的随意开发,往后看,要见玩家,该收敛的特性,也需要逐渐收敛。
及时回顾,及时总结,针对具体问题提出解决方案,而不要等到问题拖无可拖避无可避才鼓起勇气决一死战,跟体检一样,早点把问题放到篮子里,才能最大程度降低危害。
13. 无垠
文中介绍了地图尺寸太小对游戏品质的局限,并同时给出了随机地图与大地图两种解决方案,两种方案同时动工,但随机地图虽然前期开发迅捷,但最终因为输出的品质还是差强人意且提升困难而被放弃,大世界方案虽然有较多的问题需要解决,但最终的回报也很让人满意。
大地图方案中,编辑是第一个需要考虑的问题,殊途同归,天刀也是采用PCG来解决这个问题,不过当时没有Houndini,所有逻辑全部自己实现,成本不低。
地形给出了一个叫做Terrain Skirt的技术,搜了一下没找到相关资料,听起来还挺神奇的,如果有了解的同学麻烦动手留个言,感激不尽。
14. 性能
肯定有人会说,GPU也有类似的裁剪功能,何必重造轮子。相比GPU的Occlusion Culling,Milo的CPU裁剪库也有巨大的优势。因为GPU的culling,结果会慢几帧,会带来架构上不必要的复杂性,有一些问题不好处理,对新进入显示区域的物体,需要各种复杂的处理。而CPU端的Culling,就没有这些限制,即裁即用,这样我们的整个渲染和逻辑pipeline,都可以依赖Culling结果,做最精准和暴力的裁剪。
讲清了软件光栅化的一个好处,解答了我一个由来已久的疑惑。
然后我们就发现,有了这样的系统,我们似乎并不需要传统意义上的scene management系统了,runtime的时候有streaming,控制加载的场景和物件数量有限,渲染的时候用culling,大幅度减少不必要的物件,编辑器中通过culling后,可以做各种激进的优化,而且编辑器中很多物件买的逻辑不用跑,计算量本就可控,这样看来,四叉树之类也不需要加上,简化了整体架构,也降低了运行时对四叉树的管理成本。
传统游戏的场景是通过四叉树来进行管理的,目的是出于剔除的效率考虑,但是在添加上软件光栅化之后,剔除就交由软件光栅化完成了,所以四叉树管理就没有必要了(软件光栅化是会对一定范围内的所有物件都光栅化一遍吗,听起来这种消耗应该会很高?即使使用了简模,场景中的大量物件依然是一个不小的挑战,抽时间要把软件光栅化的技术了解一下)
shader在在引擎运行时刻编译,就会Block整个pipeline,造成了卡顿。通常会有500-1000ms的停顿
最简单粗暴的解决方法,就是预编译所有用到的shader。Tough哥根据shader参数组合,穷举,在引擎load时候全部编译shader并加载。这造成了很长的预加载时间,好在只有第一次sync大量代码才会有,后续就不会有了
随着代码规模量的扩大,美术的材质参数变多,穷举显然不是一个好的方案,编译时间越来越长,已经无法忍受,占用内存也越来越多。于是Tough哥打了些补丁,过滤掉一部分不可能出现的材质组合,很大程度减少了需要穷举的数据集,但临时的方案撑不了多久的
不太完美的方法是引擎第一次用到这个shader参数组合的时候,进行后台编译,用另一个线程去编译shader。这个方案的缺点主要在于,在第一次遇到这个shader并编译的时候,因为shader在后台编译,并没有准备好,所以相关的物体并不能被渲染,需要等几帧后才可以被应用到,画面上会有一些artifacts,比如画面上出现各种丑陋的色块。
另一个方案是,在版本发出之前,尽可能多的玩游戏,收集各种游戏中实际会用到的shader组合。每次遇到一个shader,就记录参数到Log文件。
可是天刀项目太庞大了,而且没有那么多测试人员,我们没有办法依赖暴力测试,来人肉穷举版本中的所有shader。而人肉合并所有的shader参数Log文件,太麻烦了。
天刀写了一个小Server,这个程序只做一件事情,所有开发版本的游戏,启动后都会连接到这个服务器,进行数据通讯,把需要的数据发送过来 。在这个应用案例里面,每台开发机器把自己用到的shader组合宏全部发到服务器上,每次用到了新的shader参数,也会通知Server。在这个小服务器里面,就可以做各种自动化的合并、去重复工作,这样版本放出去后,不管是测试人员跑版本,还是美术在编辑器里面工作,还是程序在开发渲染效果,他们的工作,都会上传用到的shader组合,对大家来说没有任何影响。团队工作两三天,小服务器上就收集了整个项目所有开发人员用到过的所有shader参数组合。虽然还是有可能不完整,但从外网版本的实际情况来看,已经收集了相当多的量,基本覆盖了99%的情况。
还需要解决的一个问题,就是无效shader参数的退出机制。每次用到一个shader组合就记录下来,长此以往,肯定shader参数越来越多。偏偏天刀的shader并不稳定,程序员还在不停修改版本,开发特性,总有不少参数组合是会老化废弃的。于是又给参数组合一个生命周期,记录了最后一次使用的时间。如果一个组合很久没有用到了,就丢弃之。
15. 得失
不直接从零开发引擎,而选择逐渐转换技术底层,直到成为新的引擎,这看上去并不是一条最短的路径。但我们项目并不拥有前置的、奢侈的引擎开发时间。所有引擎开发工作都是和时间赛跑,伴随项目开发,逐渐成型。我们需要兼顾每一个milestone,考虑每一次评审,确保活下来。产出速度是最重要的,引擎的生成只是副产品,或者说,是一个路径,通向更高质量的画面表现。
天刀项目刚开工的时候也用商业引擎,但那个质量是不够的,不在讨论范围内。
那么问题就变成,为什么天刀不直接使用类似UE3之类的引擎,非要从头开发。
UE3这样的商业引擎,有一些非常大的好处:工具链成熟,便于数据开发团队快速产出内容;行业周边中间件成熟,接入无负担;历史成功项目多,引擎核心稳定;开发生态氛围好,有利于招聘熟练工人才,外包公司也熟练。
早年间的海外市场,引擎也是百花齐放,但随着产业成熟,竞争激烈,逐渐收敛成三大主流商业引擎,老牌引擎Unreal、技术先锋CryEngine和后期之秀unity。然而我们可以看见,依然有很多顶尖游戏,使用了自研引擎,而各大主流厂商,也都有自己的引擎技术。难道它们不知道使用商业引擎的优点么?显然不会。我认为原因有四。
一是当年商业引擎的授权模式,对开发商来说并不友好。早年售卖引擎的市场非常小,所以引擎一般售价都极其昂贵,这阻碍了小开发商的使用。而对于大开发商,引擎往往有利润分成的说法,还需要从最后的收入中分得不低的利润比例,基本无法被大厂商接受。如果开发商不愿意使用分成的模式,可以买断引擎使用资格,这又需要额外的一大笔投入。这个模式弊端已经显露很久。直到近年Unity领导了一波大变革,以相当低的售价提供了简单但够用的引擎,旧体系崩坏,新世界到来,UE和CryEngine也只能仓促间跟进。
二是大厂商往往有较长的开发历史,已经有一定的技术积累。比如UBISOFT在20年前,就有内部的引擎,即使质量一般,易用性不够,但依然是一个可以产出高质量产品的引擎。围绕这个引擎也产生了很多的开发工具和生态,总有一部分团队愿意围绕自己的引擎做很多工作。更何况在一些方面,大厂的内部引擎质量并不比外部引擎差,除了易用性外,其他很多方面甚至都可以超越商用引擎。放弃自己的积累全部转用新的引擎,成本上和情感上都不是那么容易接受的。
三是将自己的安危绑定在具体的引擎,对大厂来说是很难承受的风险,这个风险体现在授权模式变更、引擎自身发展等多方面。通常大厂会在应用引擎后进行深度定制。当年我们用了将近8年的Unreal2引擎,虽然Epicgames已经将引擎升级到UE3,但我们还是在UE2上耕耘,除了UE Logo和工具链框架,基本所有的底层、上层代码全被我们翻新了一遍,加入各种自己开发的高端特性。但随着授权期的接近,厂商对于引擎授权策略有可能有修改,这导致了我们被迫全部迁移到新的引擎,带来了极大的成本。再比如如果有厂商当年深度跟进CryEngine,那么随着CryEngine的引擎业务开展不顺利,可以预料的厂商也会受到极大的伤害。
四是很多效果高端游戏,都是需要利用足硬件机能的。如果满足于使用通用引擎,则不容易挖掘潜力,无法把效果做到足够好。所以我们看见,市场上以画面效果著称的商业游戏,采用自研引擎的比例相当多。只有使用了自研引擎,历经多年的打磨,才容易把效果做到极致。而商业引擎往往要兼顾更多不同团队的需求,追求易用性,对于市场、技术的反应不一定及时。而且商业引擎也很难为了一个具体的项目需求定制,而自研引擎就完全没有这方面的压力,哪怕某些功能不容易做到通用、易用,先把效果做出来就好嘛。
顾大这边总结了商业引擎的几个不足,总结起来就是授权费用高,不利于技术的积累与传承,在定制灵活度上面有所不足,而如果工作室有自研引擎,且引擎质量也并不太差,在开发者使用习惯的加成之下,商业引擎的吸引力就大大降低了。
16. 资源管理1
资源管理考虑的内容不少,如何组织数据,方便多人工作,如何提高工作效率或者加载效率,如何让数据有更强的表现能力,都是需要考虑的。
首先考虑的是,数据是否对合并(Merge)友好。包括Diff友好、编辑友好、理解友好。
合并友好的数据在外发的时候是不合适的,因此常用的做法是开发时一套数据格式,外发时将这套数据格式转换成性能跟效率更高的运行时格式。
下一个要考虑的问题,是编辑和运行时刻的效率问题。编辑效率问题比较简单,如果文件有好的工具可以修改,效率就比较高。加载效率问题是另一个需要考虑的问题。游戏运行时必然需要高效的加载文件,减少Loading时间。加载效率也分成IO效率以及解析效率两个环节。
IO效率,就是文件被读进引擎的速度有多快。IO一直是现代电脑的最大瓶颈,现代电脑在CPU、内存、网络速度都有成百上千倍提升,但IO速度始终是系统瓶颈。直到SSD硬盘的出现,IO才有了巨大的提升,但即使是SSD硬盘,依然需要开发者认真考虑IO问题,因为它比内存速度,依然有数量级上的差距。IO效率需要考虑文件大小,这个通常不是最大的问题,因为IO系统最大的问题在零星小文件的读取,不在于持续读取速度。只要读取文件没有数量级上的差异,多几百KB并不从根本上影响IO效率。
另一个问题便是寻道问题,这个需要重点解决。零散小文件是IO效率的杀手。以前做主机游戏的时候,这个问题更突出,因为那些游戏使用光盘来读取文件,而不是硬盘,随机寻道基本是不可能的事情。一般通过一些称之为Linear loading的手段加以解决,即有相当多的文件总是按照指定的顺序被读取的,如果我们能合理的分组,将它们一次全部读入,就可以大大减少寻道时间。说起来容易,做起来非常琐碎,因为这个改动很可能会影响上层的加载逻辑。当然有很多巧妙地办法可以解决这个问题,比如上层加载的时候,都通过统一的管理接口。管理接口底层其实是顺序读文件的,但在上层看来是透明的,上层只管不停的提出加载需求即可。这样的系统通常会有一个预先的记录环节,在正常加载过程中,录下所有需要读取的小文件,把这些小文件顺序输出到一个大文件中。然后把这个大文件烧录进DVD版本。后续正式加载游戏的时候,高层逻辑正常加载这些小文件,但底层的IO接口,其实是顺序读那个预先生成的大文件。如果上层和下层的读文件顺序严格一致,那就可以非常快速的读完这个文件。用这类方法,如果用在随机寻道比较多的游戏引擎中,比如Unreal,可以提升读盘速度10-100倍,非常惊人。更进一步可以做的,就是在最后DVD刻录的时候,把这些大文件,尽量放在DVD的外道,因为外道的读盘速度会更快。
在PC硬盘上,这个解决方法依然是非常有效的,但这个方法有几个缺点。一是它牺牲了读盘的安全性,我们的录制读盘顺序过程,和实际加载游戏过程中,读盘顺序必须完美一致,如果有不一致,游戏通常就会有不可知问题,至少也会crash。二是这个模式的开发流程更复杂,预先录制读盘顺序,需要遍历每个地图,耗时很长,稍有逻辑改动,又要重新做一遍,严重影响出版本的效率。三是这个模式应用受限,无法用在可变顺序的加载上。比如我们网游随时要按照地图块来进行streaming,我们很难预测玩家会如何移动,需要加载怎么样的场景块,这就大大限制了他的应用范围。
导致IO效率降低的主要原因是,大量小文件下的磁盘寻道。因为文件没有存放到一起,所以需要频繁的重新确定存放位置,打破了数据顺序读取的逻辑,而这里给出的解决方案是将文件按照读取顺序进行存放。
对于文本类型的数据文件,比如XML、Json,读进来只是很小的一部分工作,当内容到了内存,就要考虑如何处理。这些文件都要在内存中被处理,建立DOM树,不同的处理库的快慢,显然对效率有极大的影响。简单的库处理解析内容,对每一个需要生成的节点,都会用动态内存分配空间,或是动态分配字符串内容。这虽然简单,但并不高效。高端一点的库,比如Rapidxml,大量用了in placement的new,直接在需要解析内容的内存块上直接构建内容,对程序员来说,需要了解,这块内存已经被DOM树使用了,内存管理也特别小心,某种意义上,整个实现变得更dirty,高层也需要知道这些内存管理的细节。但效率的确极高。游戏开发,往往需要打破常规,做更多违反良好软件工程实践的优化,提高效率。
Milo受到了Rapidxml的启发,在周末写了一个RapidJson,速度极快,号称只需要strlen的一倍时间就可以解析完同等长度的Json文件。封装成公共组件后给大家用,有些同学会抱怨说这个API用起来不太顺手,需要先分配好内存才能在内存上构建这个DOM树,无法直接扔进去stream,从而得到新生成的DOM树。然而这是没有办法的选择,in placement的生成内容,自然需要上层帮助管理这块内存,为了运行效率,自然带来了额外的开发复杂度。
(开发格式下)运行时解析这边引入了RapidJson方案,后续有类似的问题可以以此作为切入点。
解析完对象,还需要实际在引擎中生成相应的对象。根据不同的引擎,生成效率也各不相同。最简单的方法可以在多线程加载的时候直接生成相关的对象,管理逻辑非常简单清晰。但这个方式有几个缺点,多线程的时候,生成一个新对象未必是线程安全的,需要很多保护,有些时候甚至是不可能做到的。另一个原因是不容易控制生成的效率,如果某一刻突然有大量的对象需要生成,就会block住加载逻辑非常久。
好一点的做法是把所有需要生成的对象放进一个队列。然后另外在主线程写处理代码,在线程安全的时候从队列里面拿出对象一一处理。这样我们就解决了线程安全问题。至于另一个生成效率问题,既然我们已经把对象解析和生成的逻辑分开了,那么什么时候生成对象已经变得相当灵活,我们完全可以给定一个固定的时间budget,每一帧只花2ms生成对象,所有来不及处理的对象放到下一帧处理即可。分割问题到更可控的小规模,是解决问题百试不爽的好办法。
解析完后的物件生成,可以采用分帧加载的方式解决。
17. 资源管理2
有几种不同的方式管理大量资源文件。
最简单的方式是离散的文件管理,每一个资源都是一个独立的小文件,放在目录中。这样管理模式的坏处,就是对资源管理工具不太友好,上传地图和其他数据的时候,往往需要上传一大堆数据文件,操作复杂度和出错可能都比较大。更进一步说,这样会造成下载版本、上传版本、copy版本以及游戏加载效率偏低,因为大量的小文件是磁盘IO的性能杀手,当规模大了以后,一个简单的copy或者loading地图,都会很慢,且很难提速。 但简单的系统,也有好处。好处之一是对Merge友好,多人协作的时候,如果不小心同时改了几个文件,至少大家还有一起Merge的可能性,能逐个文件检查哪些文件有冲突。另一个好处这个模式比较轻量,不需要太多的编程支持,文件系统天生就支持这个,很适合精益的开发流程。
稍复杂一点的系统,会从优化大量的零散文件出发,将文件做简单的打包,提供虚拟文件系统,封装了散落各地的小文件,变成几个大的虚拟文件。这类文件系统实现比较复杂,往往需要在editor或者特定的工具中打开才能看其中的内容。而且考虑到多人协作,会更复杂,资源包要可以部分增量更新,要有版本管理系统的集成,便于大家日常工作在版本管理工具上。这样的系统,对同步、copy版本非常友好,两三个文件就是一个版本的全部,管理版本速度非常快。但除此之外,好处也寥寥,之前用过一个in house的引擎,大文件的管理不够健壮,一直会出错,经常工作一段时间,整个大文件经过了几次sync,莫名其妙就损坏了
Unreal是一个高度工程化的引擎,对大规模团队合作考虑很多。于是Unreal引擎做了一些折中,使用了资源的package方式,把大量类型相同或者是逻辑上相关的资源,放进了一个个package,以package为基本文件单位来管理。至于package内部的资源,可以通过引擎的编辑器来进行浏览管理。这个方式是一个妥协,通过合适的文件粒度管理,来尽量确保大家的工作内容不要冲突。这个方法既有上面各种流派的优点,也继承了它们所有的缺点。只是优点不那么突出,因为Package的规模变小,但缺点也不那么明显了
大项目管理资源的方式都非常复杂,现代引擎往往还分成开发期的资源管理以及运行期的资源管理,用不同手段管理,适应开发阶段需要的灵活性以及发布版本需要的高效率。
所有的项目,都会努力制定一套针对开发者的工作流程,对目前资源管理体系的缺点,找一些操作的流程方案,规定了多人协作时候,大家操作使用数据的规范、顺序,大家的改动通知方法。合并友好的文件系统还好,开发者只要强调合并时候的检查和流程即可。不容易合并的文件,就要通过合理安排每个人工作范围,修改文件的流程,来确保大家尽可能减少冲突。
资源文件的管理是一个常见问题,小文件管理与大文件管理都有各自的问题,目前并没有一种完美的解决方案。
最后一个话题是资源的格式问题。
以地图数据举例,开发期我们非常简单的使用了零散的XML文件,用来表示地图中的各种object。XML的好处是描述性足够好,可以表达出对象之间的引用关系、层级关系,也可以表达各种复杂类型的数据。同时,XML文件是Merge友好,确保了我们在早期工具不完善的时候,减少数据出错的机会,即使出错,必要时候也可以人力阅读修改XML文件来定位问题和修复问题。这个数据格式一直伴随了我们的早期开发,有时候编辑工具来不及写,大家也就用文本工具直接修改XML了,效率低了点,但至少还能快速看见工作效果。
随着进一步的开发,XML的解析效率成为一个问题,加载完XML,重新解析以及生成游戏中需要使用的内容,都是一个开销很大的工作。但是XML对于开发期的确有很多便利性,于是我们对运行期的资源文件格式做了一些调整,开发了XML的二进制化,把XML二进制化以后,顺手也把解析的操作Streaming化,这样加载资源文件的速度大大增加。但所有二进制的文件,并不作为上传数据,平时版本库中存放的还是XML,确保可Merge性,方便大家协作工作。
后续为了进一步加速运行期效率,也提升策划美术上传地图资源的易用性,我们又把有相关性的一些XML单独打包,生成类似package文件的中型规模的文件。这个应用场合主要是关卡数据,关卡文件通常都一起出现,在一个目录里,方便打包。打完包以后,管理这些文件变得更容易。打包了以后必然降低可阅读性,对这些文件进行调试找错也更麻烦。好在打包格式也是很简单的压缩格式,如果有bug,直接在原地把文件解压缩了,就又回到了一堆XML文件,可以容易的查找Bug。
天刀的整体框架是随着引擎的不断成熟,以及项目的进展,才逐渐成型的,早期的纯文本、不打包文件,慢慢过渡到后期的二进制、小规模打包,都是反映了开发流程中的不断取舍。所有的看见的切片,都是过程,引擎开发永远没有结果,我们也在随着产品变化,不停思考如何更好的管理资源。
资源格式与文件存储方式也并没有一套十分标准的做法,不同的项目具有不同的需求,且同一款项目随着需求的变化,原有的方案也会随之变化,所以在项目之初,就没有必要设计一套能够贯穿始终的解决方案了,只要评估出当前的需求下最优的方案即可。
18. 冲刺
下雨天导致了场景变湿润,可以简单通过PBR中的参数调整来实现,只要动态改变PBR系统中的参数,物件就会呈现湿润的油光感觉,我们基本无需重做大量物件。
19. 首测和植树
大量植被渲染的性能问题,由植被带来的阴影问题等,Streaming的问题等等,有些可以通过更好的美术实现解决,比如树木的叶片形状用polygon而不是quad标识出来,有些可以通过更复杂的系统来解决,比如阴影shadow map用分帧的方式来生成,有些通过更复杂的技术、美术结合来实现,比如streaming树木,既需要底层引擎针对树木做特殊streaming,也需要在美术层面指定每个关卡有几种最主要的树木,这些树木常驻内存。
20. 繁星
略
21. 妥协
在编辑器模块,一直有着很多不同的思路。总体来说,更强大功能、更深度整合是一个大趋势,Unity、Unreal莫不如此,提供了一个巨大的all in one版本editor。整合编辑器带来了非常多的好处,便于开发者在一个统一的环境里开发、测试功能,但有一些小的不足。一个小问题是随着更多的功能被塞进编辑器,编辑器越来越庞大,导致启动一次编辑器的时间已经长到无法忍受。另一个问题是高度整合的架构,导致开发非常复杂,模块之间耦合非常的紧密。开发成本过高,会产生大量的冗余代码。
天刀出于程序维护效率考虑,将编辑器做成了单机版本,不过这种做法对于美术等制作同学而言,可能会不太方便,总体来说这个问题并没有两全其美的方案?
多人协作有两个重要的问题需要解决。
最核心的是如何避免数据的冲突,如果大家做不同的工作,修改的都是不同的数据,那就不存在冲突的问题,多个人也可以同时工作了。
不太重要的问题是如何快速同步大家的工作,把所有数据合到一起,看见最终的结果。
协作工作中另一个常常冲突的数据,是mmorpg中常用的表格。我们也在考虑有没有更好的方法。正好有公共团队做了一个表格的编辑工具,进一步缩小了表格修改的粒度,可以针对每一个表格行,做单行的锁定、修改和上传。(类似于在线文档一类的?)
22. 优化
优化中的主要原则有几个
1.能更好的收集性能问题。天刀建立了持续测试的框架。常见的性能问题点,都覆盖了非常多的性能收集点,经常跑一下,随时可以留心到性能异常。也有dashboard,可以定期根据地图,遍历各个位置,跑性能收集,画热度图。同时也在项目中普及了查找性能问题的流程,策划美术在运行游戏的时候,碰到性能问题,可以随时截取相关数据,然后放声尖叫,召唤程序员过来查找问题。
2.工具层面做好充分的研究。每一种工具,都有擅长解决的问题,和不擅长解决的问题,要对症下药。有些工具可以快速给你性能全貌,有些工具擅长解决卡顿问题,有些工具实时监控引擎性能,有些工具只能离线分析。在不同的场合,使用不同的工具处理,会有不同的惊喜。
3.定期有大型长期的优化目标。针对专项领域,天刀一般都会安排有人花整段时间去研究,且这个优化的人,每个阶段是轮换的。这有很多好处,从个人成长角度看,每个人去玩一下优化,有助于变成一个更好的程序员,知道了性能开销,就不会乱写代码。当然我们去优化的人一般比较senior,这个不是最大的考虑因素。最大的因素是考虑到每一个开发人员都有盲区。我一直profile一个东西,可能久而久之,就有盲点了,对很多问题视而不见,思路会受限。换一个人,一眼就有可能发现里面的问题。
4.在性能优化方面,重点关注卡顿问题,而不是平均帧数问题。很多时候游戏平均帧数其实不低,一直在50帧以上,但只要某一帧卡顿一下,有100ms以上的延迟,就很容易能感觉到帧数问题。我们在中后期,专注看卡顿问题,尝试解决所有100ms以上的卡顿问题,发现一个解决一个。这里的问题并不容易解决,大多数同类卡顿是因为不小心用了同步的硬盘io,但也有很多是一次创建太多材质纹理,一次创建过多对象,或者是一次初始化过多物件。修改这些问题,往往要深入引擎内部,进行细致调整,或者修改一些系统的实现方式,才能缓解卡顿。
23. 海战
怎么分类程序员。我们把能不能独立工作作为一个维度,需不需指导作为另一个维度,就可以看到有四类程序员。大致分为:不能干活不需要指导,不能干活需要指导,能干活需要指导和能干活不需要指导。
最低级的类别,不能独立工作且不需要指导。这类同学只能给最简单的工作,也没什么潜力,指导的念头都没了。
稍好点的,不能独立工作但需要指导。这类同学已经有不错的能力,但由于时不时需要leader投入一定的关注,而在一个大团队里面,leader的精力永远是团队的瓶颈,所以这类同学有价值,但团队里面如果这类人手太多,项目规模是很难扩大的。
更好点的,是能独立工作但需要指导的。这类人需要有简单的方向指导,就可以交付很好的成果,定期汇报成果,不用太操心做不出来,就是进度上要操心一下。
最好的,就是能独立工作且不需要指导的。你发现团队有啥搞不定,扔过去让他搞定即可。也不用管太多,对他有充分的信心,可以近似认为,如果他也没有搞定,那很有可能这个特性换了谁都没有办法搞定的。
这种象限分割人群的方法值得学习借鉴。
24. 疏离
略
网友评论