芊芊551 发表于 2020-12-8 18:07

游戏引擎技术点滴

[按] 在上个月 的 2016 金山技术开放日上,俺做了一个游戏引擎相关的分享。结束之后,俺断断续续地在零碎时间里把分享的内容整理了一下,为每张幻灯片配上了简单的文字注解,就形成了这篇小结。
参与了现场交流的同学可能会发现,这篇文字版跟现场版相比,虽然幻灯片保持原状,但文字注解部分略有出入 (增加的内容主要是第二部分“评估,运用和改造”,这一部分在现场时因为时间关系说得非常简略),还请见谅。
内容提纲



游戏引擎的十年变迁 (2006-2016) (对过去十年的游戏引擎发展的简要小结)游戏引擎的评估,运用和改造 (对一次技术迭代周期内的主要实践的归纳)游戏引擎的下一个十年 (一些很零碎的对未来十年可能值得深入的技术的思考和启发点)
第一部分:游戏引擎的十年变迁 (2006-2016)

在这次分享中,我着重对比了个人相对有所了解的五款商业引擎,分别是 Unity、Unreal、CryEngine、Gamebryo 和 Torque。而下面划掉的开源引擎 OGRE/Irrlicht/cocos2d-x 性质不同,放在一起对比并不公平,所以排除在这一回讨论的范围以外。


前几个 (Unity、Unreal、CryEngine) 比较知名,就不多做介绍了。这里简单介绍一下 Gamebryo 和 Torque。
Gamebryo 是当年 (2007-2010) 国内非常流行的商业引擎之一,业内不少大厂都曾拿这个引擎做过各类项目,也培养出一批熟悉 Gamebryo 的引擎程序员 (包括我在内——虽然我是经由 Unreal 2 启蒙,但只有在 Gamebryo 上的工作才让我看到了自己曾尝试着去实现的理想中引擎的影子)。 Gamebryo 的最强之处在于其设计上的普适性——与 Unreal 和 CryEngine 内含的 FPS 基因不同,Gamebryo 对于 MMO、赛车、休闲、动作等国内常见的游戏类型均能很好地适配。从基于 Gamebryo 的两大代表作《文明4》和《辐射3》在游戏类型上的跨度,就可以看出这种不同寻常的通用性。它的整体架构简洁有力,模块边界符合直觉,模块之间耦合性低,模块实现内聚性强。这些特性使得它的学习曲线相对平缓,新人容易培训和培养,因而也是我个人偏爱的一款引擎。Gamebryo 的弱点在于自身工具链不足,且与第三方的集成度偏低,以及由于前两者导致的引擎提升潜力受限。
而 Torque 则是前些年的低成本商业引擎的代表,其特色在于简单易用的编辑器和独具特色的脚本,基本上可以看作是 2010 年以前的 “史前版 Unity”,前述两大特色也被后来者 Unity 继承并发扬光大。


在这张基本时间线的图上,我们可以看到在过去十年里这五款引擎大致的发展轨迹。其中可以看到,贯穿始末的是 Unreal 和 CryEngine,而另外三款引擎则流行于特定的历史时期。而即使是 Unreal 和 CryEngine,在漫长的发展过程中也自有其高峰和低谷。


有了它们各自的时间线打底,我把这五个引擎各自比较有影响力的版本标注了上来,这样在以五年为跨度的单位 (垂直虚线) 上,每个引擎曾做过的大动作就一目了然了。有过相应项目经历的程序员,对这些曾经活跃一时的版本一定不会陌生。这里我们先不针对单独的版本一一细说,只是对每个引擎的大致活动情况了解一个大概。


细心的同学也许已经注意到了,看起来很凑巧的是,这些引擎的大版本或多或少地聚集在我标出的以五年跨度为单位的竖直虚线附近。而这其中最为显著的就是 Unreal。这仅仅是一个巧合么,还是说,跟其他的时期相比,这些特定的年份有什么不同?


站在 2016 往回看,我们意识到——在这十年中,这是仅有的两次有着整体产业级影响力的根本性的变迁和进化。如果说是两次技术革命 (Revolution) 可能不甚恰当,但要说这是两次影响范围甚广,使得游戏产业的深度和方向产生了根本性的延展的两次行业范围的演化 (Evolution),似乎并不为过。
其中第一次演化,是起源于 2002 年并于四年后 (约 2006 年前后) 渗透到整个行业的,被广泛应用于游戏引擎中的可编程图形流水线 (Programmable Graphics Pipeline);而第二次演化,则是起源于 2007 年的初代 iPhone 并在四年后 (约 2011 年前后) 渗透到整个行业的,改变了整个游戏行业生态的移动平台游戏开发 (Mobile Development)。
可编程流水线刚刚被引入到实时图形渲染的领域中时,是 3D 图形处理器发展最迅速的时期,从 Geforce 3 开始,nVidia 在硬件方面每半年就有一次较大的更新,同时期的各类图形技术一时间层出不穷,可谓是画面渲染质量提高最快的黄金时代。在2006年时,我任职于育碧上海,有幸参与了 “细胞分裂:双面间谍” 的开发,把当时的游戏截图 (注意最后一张是游戏中上海关卡的东方明珠夜景图) 拿到现在来看都并不过时。得益于复杂度迅速提高的材质和各种开脑洞的特效实现的不断涌现,整个行业在图形和渲染表现方面的探索非常迅速和深入,通过真实感图形渲染出来的游戏画面达到前所未有的逼真程度。
而移动平台的游戏开发则是另一次重要的变迁。在新的平台上,随着大量非传统核心类玩家的涌入,人们有着与传统 PC/Console 迥异的关注点,从单一的超高画面和沉浸感追求拓展为更多元的交互和体验。可以说直到如今,我们仍处于这次演化的余波之中。
我们注意到,这两次演化的共同点在于:它们皆非短时间内剧烈的革命,而都是跨度若干年,并于图中所标示的年份上遍及整个行业的产业级演化。而不同之处是:前一次的演化,在图形领域内前所未有地挖掘了电子游戏作为可视化交互艺术可能触及的“深度”,而后一次则通过全新的平台和交互语言大大拓宽了行业所能触及的“广度”。


回到时间线中,我们可以发现,Unreal 和 Unity 对两次行业的演化有着强烈的预期和精准的判断——在这些决定命运的转折点上,他们要么在巨变来临前就做好了充分的准备,在行业变迁中始终屹立不倒,要么凭借着顺应趋势的特性集和服务乘风而起。


现在我们重点来看一看 Gamebryo 、CryEngine 和 Turque,它们是在第一次行业演化中崛起的佼佼者,在各自的量级上均是罕有敌手。Gamebryo 的标准材质 (NiStandardMaterial) 对应着可编程流水线在现代商业引擎中最简易和灵活的实现,是那个年代 (2006) 最容易上手的商业引擎之一;CryEngine 的图形表现在孤岛危机时代是最强的引擎之一 (这里的两个“之一”二字仅仅是意思一下);而 Torque 更是为小型独立开发者提供了极易上手的编辑器和非常丰富的着色器开发套件 (基本上可以认为他们自己弄了个内置的低配版 Asset Store)。所有这些让它们在第一次行业演化中脱颖而出,在http://devmaster.net (这是那个年代的一个专注游戏引擎和中间件目录的开发网站) 上成为提及率最高的几个商业引擎。
然而时过境迁,在推出了若干有影响力的版本之后,它们不约而同地在 2011 年前后平静了下来。很明显的是,它们并没有意识到时代的趋势,在移动平台崛起时,它们没有做出任何反应,反而把精力用在了事后看起来无关紧要的改进上。
Gamebryo 的用户希望引擎能提供更好的编辑器和工具链,而 Gamebryo 前后尽力做出的两版编辑器虽然看起来很不错但并未达到工业级的成熟度 (很像后期的 cocos2d-x) ——在拥有成功项目的支撑和回馈之前就陷入了兼容性的泥潭。而 CryEngine 似乎落入了“为了强大而强大”的漩涡,并未意识到在图形技术日趋成熟的时代,由于边际效应递减,图形上的优势越来越难以形成差异化。与老对手 Unreal 为 iOS 专门打造的瘦身版 UDK 截然相反的是,CryTek 精心打造的“新版” (Rebranded) CryEngine 甚至似乎刻意在逃避移动平台——它的主要特性是支持 Linux 和下一代主机 (PS4 / XBox One / Wii U)。
Torque 则是这三者中最为惋惜的。按照现在的眼光看,它是商业引擎中最便宜的 (~$200),它的目标群体是小型独立开发者和团体,它的编辑器像 Unity Editor 那样亲切好用,它的脚本 TorqueScript 神似 C#,它甚至有一个 Torque 3D Store (可以看做是 Asset Store 的前身,现在仍然可以访问)。在 StackOverflow 上,你甚至能看到 (2009 年) 这样一个有趣的比较:“Unity vs Torque game engines and IDE environment” 但是,由于对移动平台的无视,Torque 的用户源源不断地流向了 UDK 和 Unity。
Gamebryo 、CryEngine 和 Turque是三款定位迥异的游戏引擎,却有着极为相似的起落周期——在第一次演化中提供了各自领域最有价值的服务而崛起,和对第二次演化的无视甚至逃避导致的衰落。这三款引擎所有的有影响力的版本均在我们框定的第一次演化和第二次演化之间。在移动平台普及之后,这三款引擎再也没有推出一个有影响力的版本。


我们拉近视角,近距离观察 Unreal 这个贯穿十年,历经波折却仍然稳步前行的游戏引擎,看看它的步调和动作是如何与时代趋势相匹配的。在图中我们不难看出,Unreal 总是能够捕捉并提前准备好对应的发布——细究每一条细节,Epic Games 在十年里完整地向我们诠释了“与时俱进”的真义——在行业需要深度的时候提供足够的深度,在行业需要广度的时候提供足够的广度。(呃,一不留神成了 Epic 吹了~~)


接下来是第一部分的结论——不是最强的,也不是最新最酷的,更不是最贵的,而是最适应变化的,活了下来。这个结论是由达尔文的进化论金句略作修改而来,原文见下面的方框。
(第一部分完)
第二部分:一次技术迭代周期



如果想要各种姿势自顶向下自底向上巨细无遗地了解游戏引擎的方方面面,可以看 Milo 老师的游戏程序员的学习之路,这里就不多说了。我们抓一下挈领,把游戏引擎的评估放在最重要的位置,试着解决一下关于 “How” 的几个问题——“How to evaluate” (评估)、“How to use” (运用)、“How to extend” (改造)。
游戏引擎的评估

此前我曾写过一篇文章:2014-07-28 如何判断一个技术(中间件/库/工具)的靠谱程度? 这篇文章里我集中讨论了评估中间件需要注意的一些情况,在文末我写道,
“对中小规模的技术而言,上面的“望,闻,问,切”已经足以应付了。而对大型代码库/框架/引擎而言,又有一套不大一样的评估标准,另有曲径可探,咱们择日另行讨论,此处暂且按下不表。”
当时给自己挖了个不大不小的坑。两年多过去,现在这个坑终于可以填上了。


关于引擎评估,首当其冲的是三个简短的问题,我们一个一个来看。
首先,“是否经受过同类产品的考验”是一个决定性的因素,这不仅仅意味着能否按时交付,技术风险高低——更多时候,这是你的团队不会因为计划外因素而意外搁浅的重要保障。我们知道,捡到一个存折带来的喜悦远远低于丢失一个等额的存折带来的痛苦,同样的,在幻想你的游戏大卖之前,先尽一切可能确保你的项目不会因为无法控制的因素夭折显然更有意义。没有经过考验的引擎就像是一杆没上过战场真刀真枪考验过的枪,在它有效地为你杀敌之前,先祈祷它不会伤到自己吧。这也是市场上看到的山寨产品 (对国产游戏而言) 和续作 (对 3A 游戏而言) 这么多的直接原因。
其次,“好招人吗”。这个问题表面上看是一个团队管理和人员招聘问题,而实际上却是一个学习曲线和培养成本的问题。在 Unreal 推出 UDK 之前,很多用 Unreal 引擎的小团队在快速出了原型之后都难以为继,这是因为那时的 Unreal 技术人员的培养成本很高,培养时间很长,在人员快速扩充时期,小团队很难消化新手和准新手给团队带来的负担。这就造成了巨大的反差:两三个高手可以拿着 Unreal 在两个月内迅速出一个华丽的原型,在期望值迅速提高之后,弄来一大票人吭哧了一年多,在项目节点上老板一看,哎哟我去,这可不还是那个原型吗~~等等,好像还不如刚开始那时候稳定了~
最后,“是否有代码”。这个问题在此前文章里已有表述,这里引用一下:
最后说一下这里面一条俺认为比较重要的,也是当年带队的MMO项目里,被我列为头条编程规范的原则:绝对,绝对,绝对不要使用没有100%提供源码的第三方技术。这是一条红线,不管这个技术有多强大,都绝对木有例外。程序猿们或多或少都有感触,在编程的世界里,CPU时序的不确定,存储IO的阻塞,其他进程对CPU/内存资源占用造成的扰动,后台进程如杀毒软件偶尔的锁定文件访问,公网路由的拥塞,都为运行着的程序施加了太多不可预知,不可控制的因素。而在这些不可控制的因素里面,允许在自己进程的地址空间内运行一些无法得知其本来面目的代码,是其中最危险也是最容易失控的那一类。反面例子太多,俺就不举了,也免得触物伤怀,影响心境。
......
关于“三个绝对”的问题,俺专门补充说明一下,
如果所谓的“软件大厂”是第一方,那通常咱们也没啥选择。就比如要在 Windows 上开发 3D 游戏,用闭源的 DirectX 也是理所当然。正如文中所说,所谓“三个绝对”是针对那些几乎总是有得选择的第三方库而言的。使用软件大厂的闭源技术,也会带来不小的潜在隐患。大公司通常是不太搭理小团队的,如果你掉进的坑恰好在朋友圈里和网上找不到类似的案例或方案,那尝试跟所谓“软件大厂”交流或反馈一般都是做无用功。最终要么花更多的时间吭哧吭哧workaround,要么去掉对应模块了事。作为程序员,当遇到问题时,你希望的是 a) 通过一路在源码中前后追溯,在解决问题之余,弄清楚前因后果,实实在在地增加自己相关领域的经验和认识呢,还是 b) 对着文档反复猜测和校验自己哪个参数有没有误传?
其实到了关键时刻,有代码在手上,就是一颗定心丸,正所谓“源码之前,了无疑惑”。当遇上奇怪的症状时,什么文档都比不上正在运行着的唾手可得的鲜活的代码。


接下来是针对引擎的两种主要的开发模式的选择,明面上的优势和劣势已经标注在图中了。简单地说,如果“求快” (往往是需求方更强势) 占了上风,往往是“钻进去改”的框架式更合适;而如果“求稳” (往往是实现方更强势) 的占了上风,则往往会以相对严谨的工程化思维来设计架构和实现。有的团队自打一开始就压根就没考虑过这问题,管它三七二十一先改了再说。一上来堆系统堆得很爽,进度喜人,到了后期处处是改到一半改不动的烂摊子,这种时候再加班加点加人手,硬啃硬怼硬编码,拼命拿战术上的勤奋去掩盖战略上的懒惰。


接下来就到了具体内容的考察了。这一页上列出的三个因素是需要考虑的一级要素。为什么说 (从引擎角度看) “这三个因素直接决定了项目按期交付的可能性”呢?这是因为,对于给定的游戏类型和设计,这些因素直接影响到一个项目实际工程量的大小。
特性集 (feature set) 是引擎“能帮你解决的问题的集合”。正如一个 CPU 的指令集那样,引擎的原生实现不仅能帮你省去自己实现这些特性的时间,而且往往是在特定环境下 (给定的问题域内) ,在这个引擎上所能实作出的最佳实践 (Best Practice)。有两个因素会把这种价值给迅速放大:第一,普通游戏开发团队的平均技术水准,通常是低于他们所使用的引擎的研发团队的;第二,普通游戏开发团队对引擎的熟悉程度和运用能力,在一定程度上同样也低于对应的引擎研发团队。所以已有的特性集与目标项目的匹配情况,是我们格外关注的焦点。工具链 (tool-chain) 是引擎提供的“团队工作流程的内部骨架”。缺乏工具链或工具链不成熟的引擎是双刃剑,需要显著高于平均水准的团队才能有效驾驭。Gamebryo 和 OGRE 这两款引擎设计优良,但在工具链上很弱,是两个很好的例子。虽然很多团队淹没在工具制作的泥潭里,但这种不成熟也恰恰给了那些强力团队一个难得的机会,围绕着特定的业务模型构建出以业务为导向的工具链,形成了真正的团队级的核心竞争力,而这种核心竞争力是那些有着成熟工具链的引擎的团队极度难以形成的。总得来说,能力强,不怕延期,敢于投入的团队,才有机会扭转,获得业务导向的工具链带来的红利。迭代时间 (iteration time) 是日常开发中无可争议的 No. 1 Time Killer,直接影响你的工程效率。我在 Unreal 3 上工作时,有大量的场合 (如 Console Build) 是需要静态链接的,而 UE3 的链接奇慢无比,即使在中高配的电脑上也需要不下 10 分钟。而编译速度也很让人抓狂,即使有 Incredibuild 的分摊,一旦不幸改到头文件也是相当感人。运气不好的时候,一个小时改个两三次就一闪而过了 (下面这个 xkcd 真的一点也不夸张)。


由于在头文件的包含传染性和预编译的物理依赖关系上有欠考虑,不当依赖导致修改时的重编传染性极强,进而导致我们深入研究出一系列“避免动到头文件就能搞定需要的功能”的偏门神功,实在是说多了都是泪。好吧,反正这里已经黑了一把 UE3,干脆再来一个,负负得正吧。UnrealScript 是 Epic 专为虚幻引擎实现的脚本语言,无奈这货跟 C++ 的关系实在太紧密了 (如只要在涉及 native 的情况下改动变量就得重编 C++),紧密到运行时的动态能力已经损失殆尽,几乎已经没法拿来当脚本用了 (比如像 Lua 或 Python 那样轻松地在运行时 make change / run / reload )。好在 UE4 里已经把它去掉了,这里就不多说了。
这三个方面都会直接影响到一个项目的工程时间开销,是一级的重点考察因素。


接下来是次一级的考察因素。这一级的考察因素侧重于引擎的工程质量,毕竟说到底,游戏引擎是一个软件项目——功能再花哨,如果根基不牢,那么做出来的游戏多半也是摇摇欲坠。团队规模越大,受工程质量高低的影响也会越大,严重时会直接影响到整个团队的节奏和士气,进而成为影响交付的难以克服的风险。
我们先来看耐受力 (exception tolerance) 。什么是耐受力?简单说就是对非正常流程的处理能力。能够使用系统性的策略,而不是事到临头草草地 assert 来处理非法输入;能够结构性地把自己可以识别和处置的错误从无数的未定义行为中区分出来。说白了,耐受力意味着“工程上的安全感”。
举个例子,同样是使用未初始化变量,在C/C++里你可能啥事儿没有,也可能直接宕机,也可能在极为偶然的情形下宕机,也可能看起来没啥事儿而过很长时间以后宕机。而在 Lua 里却不会有问题,并不是后者比前者高明,而仅仅是因为它对这一类问题有良好的定义。
再比如说,一个编辑器,指定的操作稍微没有按照事先定义的流程来,立刻 assert ,非常敏感,一有风吹草动就 halt。很多程序员喜欢辩解说“尽早崩溃”是最佳实践——在遇到问题的第一时刻报错,报得越早越好,毕竟越接近问题发生的第一现场越方便他们调试。这个说法本身当然是没问题的——如果你用个默认值糊弄一下,或者是写某种容错逻辑来忍耐了破坏假定的行为,那么错误很有可能会蔓延到之后更加远离出问题的地方才爆发,那时丢失了源头和上下文,查错的成本会变得非常高。
以下详细说明部分节选自俺的一篇未发布的文章,详细说明了一下这个问题。
如果只考虑程序员,不考虑团队中同样依赖每日版本来工作的策划,美术和测试的话,这个思路是没有问题的。然而跟程序员不同,当发生崩溃时,团队内的其他成员能做的非常有限——以最快速度通知程序员,版本挂了 (The build is broken!!!)。如果坏的地方正好是他们工作的部分,那么他们只好停下来,等待修好才能继续工作。否则要么一直备有一个可靠的老版本 ,要么手动回滚。
现在请摘下程序员的帽子,假设自己是一个负责“测试多人副本,任务和活动”等业务逻辑相关的测试人员,每测一次都要花不少时间进入测试情境,偶尔甚至需要多人一起协同测试。那么一个跟你的工作毫不相关的底层崩溃,所带来的影响就会被迅速放大,很可能得完版本后的几个小时就在反复尝试和等待之中被消耗掉。
这是一种惊人的浪费。
给程序员巨大便利的“尽早崩溃”,对非程序员来说,意味着日常开发中的每一天,都要冒着被“不可控的因素”延误工作的风险。有人说,正确的姿势难道不是让程序员有更好的自律,在提交前做尽可能充分的测试,确保不要搞破坏吗?是的。可是即使是经验丰富的程序员也不能拍胸脯保证自己 bug-free, 更不能保证由若干人提交的若干“不相干”的改动集成到一起就能无缝地良好工作。让非程序员去承担这种因为版本不成熟导致的效率折扣,是既不公平也不高效的。
说到“巨大便利”,不得不补充一个前缀——“本机上的”,也就是说,只有崩溃恰好发生在制造这个问题的程序员的机器上 (或可以方便地即时远程调试) 时,这种巨大的便利性才得到体现。考虑到发生在非程序员环境下的崩溃,不少情况下是由于环境配置错误等杂音所致,“有经验的”程序员往往不会浪费他们“宝贵的开发时间”,第一时间赶往现场开始分析和调试(打断自己的工作跑去协助调试,满头大汗弄了半天,发现是环境配置的问题/版本问题/别人代码导致的问题,足以唤醒一个温顺的程序猿内心的洪荒之力了)。更多的,他们会在 IM 上回个消息:“嗯,这个功能我提交前测试是正常的——你的环境干净吗?需要的数据都干净地重新生成了吗?第三方库的二进制文件更新了吗?你们几个人测试的版本一致吗?要不你 Cleanup / 重启 / 重新保存 / 重新建个账号试试?”,试图通过尽可能小的时间开销来帮助诊断和解决问题。长远来看,这些试图节省调试时间的沟通,会让“尽早崩溃”所带来的巨大便利慢慢地挥发殆尽。
一个不那么容易觉察却更为严重的系统性问题是,总是采用“尽早崩溃”的实践的团队所产出的代码库,随着系统内不同模块之间的交互(以及随之而来的各种假定)越来越多,往往倾向于通过更多的断言来让系统变得越来越敏感和脆弱。因为,认真细致地考虑模块间的依赖时序,并系统性地从结构上解决过深的模块间耦合,总是比一个简单的断言要复杂得多。
“尽早崩溃”的主张是如此的简洁有力,以至于我们在那些应当通过改良结构,去除耦合来解决问题的时刻,往往简单地选择了使用断言来做一个时序上的约定。这种显式的指定会把系统的坏气味转化成太多的不必要依赖。的确,问题从表面上看起来变得更简单了——谁破坏了断言,导致了崩溃,谁就修呗——实质上,修来修去,把一个本质上可以剥离的简单交互,变成了严重依赖各种时序和条件的“靠巧合工作”的杂耍系统。
你看,“尽早崩溃”的简单性和便利性,在一些情况下反而成了一个让代码质量退化,鼓励系统熵不断增加的问题机制。那么问题来了,在满足了“必要的时候程序应当尽早崩溃”的基础上,还有什么可以选择的实践吗?
(以下略)...
到这里我应该已经基本说清楚什么是耐受力了。在图中我提到的三点分别是特殊需求,坏数据,破坏性的改动。这些都很直白,通过验证一个引擎在这些方面的表现,我们很容易对它的耐受力作出判断。
可见性也是重要的考虑因素。如果引擎能充分揭示自己的业务流程 (如何运作),生成的各类数据 (如何存储),关键模块的性能开销 (如何优化),那对各类基于引擎的二次开发才能更有信心,才能够最大限度地避免依赖了错误的假定。当出现问题时,也更容易查找和比对。而如何在维护最大的可见性的同时保持良好的封装和较低的耦合,同样是一个很大的话题,这里就不再细说了。


接下来的页面上这些内容是更次要一些的因素,它们大多都很直白,这里我们挑着简单说一下。
(teamscale-friendly) 不少引擎的工具链适应不了团队级的开发,尤其是大规模团队的开发。这类问题会在团队规模迅速增长,对引擎的平均理解程度迅速降低时暴露出来。(3rd-party-friendly) 商业引擎通常会有不少与第三方中间件的交互。考察这种交互的一个简单方法是观察那些“三不管”的薄弱地带。此前工作在 Unreal 上时,我曾短期地维护过一个模块,那个模块的业务逻辑依赖于 Unreal / Scaleform / Bink 这三方的适配。因为它们各自为政,在两两集成时,本来就是形式胜于实质,三方交互时就更为薄弱了。当大批量数据需要高频地在不同的中间件之间传递时,潜在的问题就会很容易集中爆发。(industry standard-friendly) 使用行业标准这一条就不多说了,我曾在一个游戏项目里见过七八个有着不同的 internal representation 的字符串类 (还不包括 std::string) 。光是字符串转换之间的各种细节,潜规则,优化手段,都够得上出本书了~ 想想你的工程师把他们宝贵的脑力消耗在这些事情上,实在是惊人的浪费~
游戏引擎的运用和改造

说完了引擎的评估,接下来的运用和改造是很大的题目,对不同的项目类型也不尽相同。到这里不知不觉已有上万字了,为免冗长,我们提炼出一些相对通用的考虑和实践,就不考虑在单方面深入讨论了。


在一个项目内涉及到引擎相关的部分,首当其冲的就是工作流的管理。项目是由人构成的,一个进行中的项目包含了许多显式或隐式的流程、约定和步骤,这些交互随着开发的进展不断动态变化。正如左下图那样,每个团队成员作为其中的一环或多环,有机地交互并推动项目的进展。要想让这个系统持续地有效运转,很重要的一点就是通过不断的观察、定位、梳理,来改进系统中响应速度跟不上整体节奏的环节。
在做阶段性回顾时,我们容易把目光聚焦在“什么做了,什么还没做”上,却容易忽略对影响响应速度和导致效率损失的因素的及时处置。如果能够持续不断地观察和优化这些敏感点,我们就能发现,抛开每个成员技术方向和能力的差别不谈,绝大多数响应问题是由于 (过多的) 依赖导致的。这些依赖既有内部的,也有外部的;既有业务逻辑需求驱动的逻辑依赖,也有物理性的资源和数据依赖——当然最多的还是由于“针对工作流的分析和梳理严重不足” (俗话说的做到哪儿算哪儿) 导致的项目成员之间的无谓依赖。
有种常见的说法是 Daily Build 就是项目的心跳,保证每日构建的安全、自动和鲁棒是最重要的。然而我认为这只是工程意义上的心跳,一个游戏项目的真正心跳在于“持续的可感知的进展” (Continuous sensable progress)。一个游戏项目内,任何一个可感知的点,不管是策划针对某个角色某个技能的构思或数值调整,还是美术对某个场景内某个特定氛围的塑造和创作,还是服务器程序对于一个特定功能 (如快速组队匹配) 的效率上的优化,都会通过或长或短的工作流程,最后在某个 Build 内以游戏内的一个可感知的点体现出来。我深切地感到,这样的基于实际体验点的持续而有节奏的交付,才是一个健康的游戏项目的真实心跳。而这个真实心跳是否能良好运转,跟工作流的响应效率和依赖处置是直接挂钩的。
关于 hackers & scientists 的区分对待,也是值得讨论的一点。“黑客”式的工程师会准确而锋利地切中要害,他们敏感,敏锐,敏捷,有惊人的直觉和洞察力,对大部分问题都能在很短时间内直截了当地给出“行还是不行”的答案,相应地,他们多数时候“事了拂衣去”,不那么在意严谨和完备,不愿意陷入琐碎的工程细节,也对编写日志,测试用例等等一切“官僚主义的形式材料”满不在乎。而与此相对的是,“科学家”式的工程师们,普遍周全,周密,细心和细致,能不厌其烦地追究每一个细节并给出妥善的应对方案,他们无比在意工程的完整性和完备性,产出的代码精确、详实而可靠,充分而周祥地考虑了各种可能出现的隐患和边界条件。
很少有程序员能同时具备黑客和科学家的素质,所以这就要求我们能够感知并理解每个个体的行为方式,不断做针对性的调整和细化,从而让他们的积极特性在项目中能得到最大程度的放大。


这里是关于同步节奏的管理,图上已有表述,不再多说。


针对引擎局限性举的 Gamebryo 例子。


这是魔兽世界里的兔子,详细的材料可见这里:有哪些看起来很高端的技术其实原理很暴力很初级?。


引擎不支持某个特性,并非总是坏事,也许正是你的游戏体现出差异性的好时机。有段时间,使用 UE3 开发的游戏扎堆出现,其中那些质量低下的作品,几乎总是让你一眼就能看出是在 UE3 上随便堆砌了些美术素材就放出来的昧心之作。而那些真正下功夫的 3A 之作,却总是能跨越技术的藩篱,(至少是在某一方面) 塑造出超越引擎能力的独特的体验。举个例子,如果 Android 本身在国内体验足够好的话,当时小米 MIUI 的独特本地化体验在 Android 阵营里也就没法那么出挑了。
什么是伪命题呢?就是那些本质上不存在,却因为某种局限性,稳定性或性能问题而浮现出来的需求。作为程序员,我们经常在提炼 xx 需求的时候发现,只要能把 yy 和 zz 弄好, xx 的问题自然而然就消解了。但机会窗口并非总是存在,也许一下没想通,没理会 yy 和 zz 的潜在问题,手一抖把 xx 做了,之后叠床架屋地做了 n 层,然后再想回来改就已经改不动了。举例的话,不少游戏的热更都是如此,就不具体说了。


关于技术负债,只要能意识到这很大程度上并非负面的因素,不要有太重的心理负担就好。在处置这些欠下的负债时,要有勇气不断地断舍离,随时扔下负重,轻装上阵。不要舍不得删代码。
关于删代码有一篇非常有意思的文章,非常推荐阅读:
(需翻墙) Write code that is easy to delete, not easy to extend
如果上面的链接无法访问的话可以看下面这个 Internet Archive Wayback Machine 版本的:
(需翻墙) Write code that is easy to delete, not easy to extend (Internet Archive)
这篇文章在 Hacker News 上的评论也很有趣,一并推荐。


我曾不止一次地听到有项目组在一个进行中的项目里热切地讨论换引擎,当问到我时我常常会尴尬地笑笑。比较而言,这种动议的出发点往往是产品角度,一般出自项目经理或产品负责人,很少由工程师提出,故而讨论的结果不会影响到实际的执行,所以除了笑笑也贡献不了啥有意义的想法。实际情况是,更换引擎是一个难度指数显著较高的操作,一般的团队不一定能克服得了这个困难,而这种风险往往会被 (有意或无意地) 低估。哦,对了,我还发现,那些曾经投入地讨论换引擎的,往往是折腾完了之后最早开始怀念在老的引擎上的好日子的同学。当然了,他们会拿出一百个理由告诉你这么干是值得的,嗯,好吧,毕竟生命在于折腾嘛。
(第二部分完)
第三部分:游戏引擎的下一个十年



这一部分主要是我个人对游戏引擎方面的一些非常零碎的和个人化的整理和推断,以幻灯片的内容为主,文字材料从略,见谅。


首先是下一代无线的标准——5G。我们可以看到,相较 4G,5G 的带宽和延时指标均提高到原来的 20 倍。用户面延时降低到了难以置信的 0.5ms。不仅仅是视频和直播类应用会从带宽和延时上受益,不少游戏引擎内的同步模块,拿这把尺子一衡量,立刻可以看出巨多的值得改进之处,就好像现代的图形引擎里,九十年代大当其道的 BSP 已经不见踪影了那样。传统引擎里的很多预测,补偿(这里和这里),都可以用更简明的方式去处理。


在上图 (出处在这里:Latency Numbers Every Programmer Should Know) 我们可以看到,传统的磁盘寻道约耗时 10ms (百度百科上这个数据为 7.5~ 14ms),图上的问题值得思考:当你的网络连接速度远快于 (10x) 本机的磁盘寻道时,你的引擎将应该如何来设计和实现你的资源管理及网络同步机制?不夸张地说,这将是一个牵一发而动全身的问题。


在 2016 年,我们见证了首代消费级的 VR 产品。由于技术尚不成熟,在视觉呈现上有明显的纱门效应。


由于巨大的显示带宽需求,眼下的 VR 设备线材繁多,玩家行动受到颇多限制。


nVidia 的 MRS 技术,把四周的拉伸区域使用较低的分辨率渲染,用于提高渲染效率和降低带宽需求。


Abrash 在 Connect 3 上提到的凹式渲染,用于动态地进一步降低需要传输和渲染的像素量。


经由类似上面的技术,我们将有机会在不久后见到更细致的画面的同时,摆脱线缆的束缚。


而真正的临场感 (Presence) 由于各方面的限制 (见链接中结语的 “right problems” 一节),还需要更久后才能实现。


看到这些值得深入探寻一番的技术点之后,我们回头看了一眼第一部分的结论——物竞天择,适者生存。还记得大明湖畔的 CryEngine、Gamebryo 和 Torque 吗?


(这一页是充话费送的)


(嗯,这一页也是充话费送的)


居然在结束的页面上第三次提起达尔文,这得是个有多迷恋进化论的人啊~~~
(全文完)
Gu Lu

[注]
本文首先发布于公众号西山居技术。Permanent Link: http://gulu-dev.com/post/2017-01-15-game-engine-talk-2016幻灯片版本见:(http://github.com) gl-bits/(2016) 游戏引擎技术点滴 (2016金山技术开放日)。本文遵循 Creative Commons BY-NC-ND 4.0 许可协议。
页: [1]
查看完整版本: 游戏引擎技术点滴