开发『星际争霸』的艰辛之路

发布时间:2020-04-04 来源:未知 浏览次数:

  ,1991年加入硅与神经键(暴雪前身),三位创始人以外的第一个员工。自1994年起任暴雪研发副总裁,2000年从暴雪离职创办ArenaNet公司,后推出『激战』系列游戏。Pat作为元老级员工,参与了几乎所有暴雪早期游戏的开发——包括暴雪起家时的几款SFC游戏,也包括后来脍炙人口的三大系列:『魔兽争霸』初代和二代(制作人+主程)、『星际争霸』(程序)、『暗黑破坏神』初代和二代(程序)。

  Pat原本的计划是用三篇博文介绍如何在C++中使用侵入式链表来替代非侵入的std::list、提高程序的性能和可靠性。由于工作繁忙,实际只发表了两篇就中止了。第二篇博文是专门面向程序员读者的技术内容,这里翻译的是比较有故事的第一篇。在中文互联网上,此文已经有若干个版本的翻译,我见过的至少有三版,包括各种机翻、不忍直视的错误。希望我这次的翻译这样的问题少一些(尽管我的C和C++比较水,英语也算不上大师),如果有译错之处,请在评论区指正,谢谢。

  开发星际争霸经过了两年半的艰苦时光,光是发售前的压榨期(*译注1)就持续了一年多,当时这游戏的bug之多简直就像蚁巢(*译注2)一般。尽管它的前作(魔兽争霸1和2)跟市面上其它游戏比起来稳定性出众,星际争霸却是频频崩溃,乃至于测试工作直到发售都难以正常进行,发售后我们还得给它继续打补丁。

  原计划中的星际争霸本来是个中型游戏,开发周期只有一年,以便在1996年圣诞节发售。项目组原本来自『破碎国度』(Shattered Nations)——那是一个基于X-COM的回合制战略游戏,暴雪1995年5月公布了它的存在,但几个月以后就给取消了。然后这个团队就被重组来开发一个快餐作品,好让公司的发售空白期不会太长。

  这个赶工开发的决定,用现在的眼光看来确实有点奇葩,但是公司总裁Allen Adham当时正扛着业绩压力。暴雪之前的作品表现通通远超预期,这更加拉高了老大们对未来增长的期望。

  时间特别紧,人手又有限,星际争霸团队的目标是做一款中型游戏——说白了就是「太空兽人」(*译注3)。1996年第二季度,大概E3展那个时候,游戏还是这个鬼样子:

  >

  ——1996年6月E3展(*译注4)上公布的星际争霸,呵呵,换我我也不玩。

  但是,另一个高优先项目将星际争霸踢到了角落里,还夺走了一个又一个的开发人员。『暗黑破坏神』,坐落于加州Redwood市的秃鹰工作室开发的一款角色扮演游戏,正需要援手。Dave Brevik和Max/Erich Schaefer兄弟成立的这家秃鹰公司只有区区120万美元的预算,这点钱即使放在当年也少得可怜。(*译注5)

  要完成他们梦寐以求的游戏,秃鹰团队是有心无力,但他们起码为一个好游戏打下了创意基础,足以令暴雪收购秃鹰,将其改名为北方暴雪,并投入真正配得上这游戏的资金和人力。

  刚开始,我和Collin Murray(星际争霸的一个程序员)飞到Redwood去帮忙,而其他开发人员留在Irvine市的暴雪总部,搞战网、调制解调器、局域网游戏,还有屏幕界面(在暴雪内部俗称「glue screens」),用于角色创建、加入游戏等基本功能。

  暗黑破坏神的项目规模越来越大,最终暴雪总部的所有人——美工、程序、设计、音效、测试——都进来了,直到星际争霸那边一个人也不剩。就连项目负责人也得亲自上阵,把我写了一半太忙没时间完成的安装程序写完。

  1996年底,暗黑破坏神发售,星际争霸的开发被重新提上日程,大家这才有空重新审视一下这游戏的前景,然后发现它一点也不美好。这游戏一看就过时了,更是完全谈不上震撼人心什么的,尤其是和六个月前在E3展上demo效果出众的『领土』这种项目相比。(*译注6、7)

  暗黑破坏神的巨大成功重塑了暴雪的奋斗目标:星际争霸成了第一个贯彻暴雪「不完成就不上市」战略的游戏,但是在证明这战略有效性的道路上,却是苦难重重。

  所有人都用批判的眼光看待星际争霸,很明显,这个项目需要更大的雄心壮志,我们之前的两款魔兽争霸定义了即时战略游戏的发展方向,而这次还要更具突破性。

  根据当时最大的游戏杂志『Computer Gaming World』的主编Johnny Wilson所言,在星际争霸重新启动开发时,业界同在开发中的即时战略游戏超过80款。身后追着这么多竞争对手,甚至包括Westwood这个现代即时战略游戏的祖师爷,我们必须拿出一个屌炸天的作品。

  而且,我们也不是以前的小作坊了。有了魔兽的成功,再加上暗黑,无论是玩家还是媒体都对我们充满期待。游戏业界的法则是逆水行舟不进则退(*译注8)。我们必须远远超越之前的成就,而这需要冒一些风险。

  我们的编程管理能力也不够:当时我们并没有意识到应该在项目早期为新手程序员提供指导、让他们在游戏发售前掌握必备知识,对新手来说他们基本就是被一脚踹进水里自己要么扑腾要么呛死的感觉。问题的一大原因是我们人手实在太少了,搞得所有程序员为了完成目标都在拼命写代码,却没时间做Review、Audit和培训。(*译注10)

  而且不光是没经验的初级程序员,就连星际争霸的主程,也从来没有完成过一个成熟的游戏引擎。Bob Fitch(*译注11)倒是干过几年游戏编程,成果也不错,但是他之前干的一个是跨平台移植,引擎都是现成的;还一个是为魔兽争霸1和2开发功能,这也不需要设计大规模的引擎。然后他倒是也当过一次技术主管——在做破碎国度的时候,项目干一半就取消了,结果它的架构是否可行也是死无对证。

  团队对项目的投入之大前所未有,为了完成项目甚至牺牲了个人健康和家庭生活。我还没见过哪个项目能让每个人都如此拼命干活。但是,项目中的一些关键的技术决策搞乱了之后的整个开发过程。

  先是花了几个月完成暗黑破坏神,发售后又用了几个月清理和打补丁,然后我回来帮忙接着弄星际争霸了。我并不愿意扎进另外一个满是bug的大坑,但事情还是这么发生了。

  当时我本以为重归这个项目应该挺轻松的,因为我对魔兽争霸的代码了如指掌——毕竟每一块程序我都有参与。结果我震惊地发现,引擎的很多模块都被甩在一边没用,然后其中一些逻辑又在别的地方重写了。

  游戏的「单位」类当时正处于从头重写的过程中,而且单位调度器被废掉了。这个调度器是我做的机制,用来确保游戏中每个单位都有时间来计划自己的行动。每个单位都会周期性地问「我当前手上的事做完了,现在需要干啥?」「我是否应该重新计算行进路线?」「我要不要换一个攻击目标?」「玩家给我下达新指令了吗?」「我死了,如何清理自己?」等之类的问题。

  当你们打算从头重写的时候,最好记住,不要以为你们会比第一次干得更好。首先,你们的团队可能根本就不是原来版本1的团队了,所以你们其实并没有所谓的「经验」。之前犯过的大多数错误,你们会通通原样重来一遍,并且还会制造一些旧版本中没有的新问题。

  魔兽争霸引擎当初花了我们几个月工夫才完成,而一个全新的程序员团队需要为了新功能对它进行重写时,他们要花大把的时间重新学习游戏引擎的结构是怎么回事、为什么要设计成这样。

  面向MSDOS写魔兽争霸的原版引擎时,我用的是C语言和Watcom编译器。当游戏平台要切换到Windows时,Bob选用了Visual Studio编译器,并用C++重写了引擎。这两套选择都有道理,问题是当时团队里根本没几个人写过C++,尤其是这语言坑还特别多。

  C++有其优势,但特别容易被用错。Bjarne Stroustrup,发明这语言的人,有一句名言:「用C你容易不小心砸自己的脚;C++砸脚的几率低一点,但一但砸了,你整个下半身就废了。」(*译注13)

  历史证明,程序员在接触一门新语言时,总会迫不及待地在第一个项目试遍所有功能,星际争霸的类继承关系就是这么来的。看看游戏单位的这个继承链,老司机看了能气得直哆嗦。

  CThingy对象是精灵,可以在地图的任何地方出现,但不能移动,也没有行为,而CFlingy是用来创建粒子效果的,爆炸时会有几个这玩意随机乱溅。CDoodad——过了14年了我觉得好像是叫这个类名吧,这个类本身没有被实例化过,却包含了子类所需的一堆重要行为。然后就是CUnit,单位的行为分散在这么多模块的代码中,不论你想干什么,都得先弄明白所有的类才行。

  除了恐怖的继承关系以外,CUnit这个类本身也是一坨翔,它的定义用了好几个头文件。

  多年以后,「组合优于继承」这个理念才成为程序员一族的信条,但是星际争霸的开发团队们早就通过血泪得到了这个教训。

  由于早期曾被中断,项目重启后开发团队一直被催着交活,而且「两个月内就能发售游戏」的日程表已经被传出去了。

  考虑到需要增加那么多游戏单位和行为、把视角从鸟瞰改为等角投影(*译注14)的工作量、全新的地图编辑器,还有战网对战的新功能,游戏根本不可能在这么短时间内发售,就算是美工、设计、音效、平衡和测试全都按期完工也不可能。结果编程团队在「目标是两个月内完工」的状态下连续工作了整整十四个月!

  整个团队都一直在加班,Bob本人则是40小时、42小时甚至48小时连续编程。虽然我印象里拼到如此自虐程度的只有他,但是其它人的加班也很疯狂。

  我之前开发魔兽争霸时经常整宿地写代码,后来做暗黑破坏神时也曾连续数周坚持14×7工作,这些经历向我证明了熬夜没有任何意义。过了晚上某个时间点之后写出来提交的任何代码,都只会让你后悔并浪费清醒时间重写。长时间连续干活让人头昏眼花,在这种状态下进行创新性的脑力劳动,出现再多的错误和明显的bug也没什么可奇怪的。

  顺便说一下,这种疯狂加班并不是公司要求的——我们拼命只是因为我们自己特别想做出好游戏。现在看来当年真傻,欲速则不达,我们本来可以用更少的时间干得更好的。

  我最自豪的成就之一是,两年内给『激战』发布了四个篇章(*译注15)而没有把开发团队拖入加班泥潭。

  为星际争霸实现了一些重要功能——包括战争迷雾、视线检测、飞行单位的寻路和碰撞(*译注16)、语音聊天、AI强化……等等之后,我的主要工作就成了修bug。

  啥?语音聊天?1998年?是啊,1997年12月我就搞定了。我用了一个第三方的语音压缩算法将语音压缩成音素,然后用代码实现了通过网络发送这些音素、在其它七个玩家的电脑上解压还原并播放。

  问题是我们办公室里所有的声卡都需要升级驱动才能正常使用语音聊天,哪怕声卡本身支持全双工(录制和播放同时进行),于是我很遗憾地建议放弃这个功能。不然我们的技术支持工作量就太大了,卖游戏赚的钱还不够雇客服接电话的。

  总之我修了一大堆bug,其中当然有我自己造成的,但主要还是其它人在疲惫中写出的那些诡异的bug。我得到的最好表扬之一,大概是在几个月之前来的,Brian Fitzgerald(*译注17),我共事过的程序员当中最优秀的两位之一,提到了星际争霸的一次code review,他们发现我对整个代码库修复了多少问题时惊呆了。呵呵至少还是有人承认我的功劳苦劳了,都这么多年过去了!(*译注18)

  故事这么多灾多难,你可能会觉得很难评出一个罪魁祸首、万bug之源,但是根据我的经验,星际争霸中的最严重的问题都和双向链表的用法有关。

  游戏引擎中使用了大量的链表,用来追踪拥有共同行为的单位(*译注19)。两倍于前作的单位数量——星际的人口上限是1600,相对于只有800人口的魔兽2,如何优化对特定单位类型的搜索、使它们互相关联,就成了一个必需的工作。

  回想当年,到处都是链表,每个玩家的单位和建筑、每个玩家的「人口」建筑、每个航母上带的小苍蝇……很多很多。

  这些链表都是双向的,这样一来,添加和删除元素的时间复杂度就是固定的O(1);而不用像单向链表那样执行O(N)的遍历操作。

  问题在于,每个列表都是「手动维护」的——没有公用的方法来对这些链表进行添加删除元素操作,程序员们只好各自在需要用到的地方自己写。比起简单调用一个debug过的公用方法,这些手擀的代码超级容易出错。

  有一些链接指针字段被共享给多个链表,所以要想安全删除,还得先弄清楚对象到底链在哪个表上。还有一些链接指针字段,为了减少内存占用,甚至用了C Union来存储。

  悲剧的是,这些链表问题本来不应该存在。Mike OBrien(*译注20),跟我还有Jeff Strain(*译注21)一起创建了ArenaNet的那个哥们,写了一个叫做Storm.DLL的库,暗黑破坏神就用了这个。Storm的功能众多,其中就包含了一个用C++模版做的、非常棒的双向链表实现。

  在星际争霸的开发中,一开始用到了这个库。但是很快大家就把代码刨了出来然后开始手工实现链表,主要是为了更简单地写入存档文件。

  在开发魔兽争霸之前,我玩过的很多游戏的存档功能都很屎。玩过Origin(*译注22)随便哪款游戏的人,应该还记得存个档要花多长长长长时间。我的意思是,确实,当年的CPU很慢,硬盘性能也是,跟如今的标准之间的差距就如同瘸逼乐和超跑,但那也没道理会慢成这个球样子。于是我决定魔兽争霸不能出这个问题。

  所以在魔兽争霸中,我用了一些窍门,让它将大块内存整个一次性写入磁盘,而不是在内存中来回晃荡、这来一点那来一点。整个单位数组(600个单位×每个单位几百字节)是一次性写进磁盘的。所有的「非基于指针」的全局变量也是一次性写入,类似的还有地形、战争迷雾等。

  说来奇怪,这种一次性写入对存档速度的影响没那么可观,倒是大大简化了代码。只是,魔兽争霸中的单位没有「指针」类型的数据,不然这招根本不能用。

  星际争霸的单位,前面说过了,为了实现链表,里面有一大堆的指针字段,是完全不同的怪物。存档的时候必须搞定所有的指针(嗯尤其要小心Union里的那些),这样1600个单位才能一次性持久化,然后继续玩之前还得把这些指针还原。呸。

  修了好多好多链表相关的bug之后,我强烈要求大家改回用Storm版的链表,哪怕这样存档的逻辑会更复杂一些。这里说的「强烈要求」,基本上在暴雪我们想讨论点什么事都是这样的——大家个个年轻气盛、目空一切,一吵起来就会很激烈,除非话题是午饭吃什么这种没人愿意做主的事。

  结果我没吵赢。毕竟我们离发售「只剩两个月」,改底层引擎这种大动作没有获批,只能基于现成的低效方案修修补补,结果这造成了深远长久的痛苦,痛苦到从此改变(改进)了我编程的方式,下一篇博文会细说这个。

  我还有一个治标不治本的例子:星际争霸从鸟瞰视角改成等角投影视角时,后台的图块绘制引擎还是用的旧的——我1993年还是94年写的老代码。

  在方形图块引擎上绘制等角投影风格的图像并不算难,但是这样一来地图编辑器之类的东西就比较难搞了。因为这些斜着的图像其实是画在方块里的,图块互相叠加的时候需要做很多「边缘修正」工作。

  渲染还不是最复杂的,寻路才叫一个难。显示是斜着的、底层逻辑是正的,所以不能直接判断整个32×32像素的斜块能否通行,必须得拆成8×8像素的小块——这一下就让寻路算法的工作量乘了16倍,而且那种挤不过去的大型单位还得另行处理。

  要不是有Brian Fitzgerald这位编程大牛,光是寻路问题就能让这游戏永无发售之日。说起来,寻路是直到项目的尾声阶段才最终解决的,其中有很多值得一提的技术和设计细节,所以我打算多写一些关于星际争霸中寻路计算的东西。(*译注23)

  以上唠叨了这么多,把星际争霸这游戏做出来真是不容易。因为无论是游戏的方向、技术还是设计,公司上上下下每个层次充满了各种决策错误。

  幸运的是,我们不只是莽撞,还有英勇,而且充满智慧。最后我们全神贯注、不再添加新功能,让游戏得以发售,并且玩家们也看不到底层的一团糟。这可能也是编译语言相对JavaScript这种脚本语言的一个优点:代码再烂用户也看不见!(*译注24)

  在下一篇博文中,我会写一些更技术的内容,谈谈为什么很多程序员都没有用对链表,并且提供一个已经在暗黑、战网、激战中实战验证过的替代方案。

  还有,即使你不用链表,这个方案对于更复杂的数据结构也适用,如哈希表、B树还有优先队列。总之,我觉得这个基本思路对于一切编程都是通用的……还是别扯太远,要不然又要另写一篇东西了。