17 minute read

星际争霸的艰难之路

[作者]Patrick Wyatt;[译者]Louis Huang

Patrick Wyatt

本篇翻译自 Patrick Wyatt 的博客,原文创作于2012年9月7日。原文链接

Patrick 是暴雪星际争霸项目的制作人和主程,他之前也参与了魔兽争霸I,II以及暗黑破坏神的研发,下文的我,均指 Patrick 本人。

我曾经写过魔兽争霸的早期开发文章,但是最近看到的一篇 blog 让我在充满愤怒的写下了这篇三个部分的,二十多页的文章,关于星际争霸的开发和一些我对如何生成健壮的游戏代码的一些想法,我会将后面的部分陆续的发布出来。

  • 本篇:星际争霸的艰难之路
  • 第二部分:我们如何修正大多数的错误
  • 第三部分:解释修正内容的具体细节

本篇是Patrick这个系列的第一篇。第二篇原文点这里,由于内容并不有趣,所以没有翻译。第三篇根本就烂尾了,没有写出来。

星际争霸的起源

在整个星际争霸的开发过程中,包括两年半的大规模开发期以及一年的调整期,一直到游戏发前最后一刻,整个游戏都充满了 bug,就像是一个千疮百孔的白蚁巢。然而,暴雪在为星际争霸之前的作品(魔兽争霸 I 和 II)的产品质量却是相当稳定,开发过程中,星际争霸频繁崩溃,导致游戏测试无法正常进行,直到游戏发布之后,我们还必须继续给游戏制作稳定性的补丁。

为什么会这样呢。。。。慢慢说来。。。。

“太空兽人”

星际争霸最初是想做为一个开发为周期一年的游戏,可以在1996年的圣诞季发布。项目最早的人员全部是来自一个被取消开发的回合制策略游戏 《Shattered Nations》,这个游戏是作为 [X-Com] 这个世界观下面的一个游戏,暴雪在1995年3月宣布开发,但是在几个月后就取消了整个项目。

译者注:由于对于当初的游戏的定位并不是一款划时代的大作,并且希望可以一年内就能开发完毕,所以星际争霸理所当然得继承了当时暴雪已拥有的成熟技术:魔兽争霸的引擎及各种模块,下文很多内容都在此前提下展开。

译者注:Shattered Nations 是一个 3D 游戏,拥有回合制的核心战斗和宏大的未来世界的背景。显然在1996年要做出一个这样的游戏非常勉强,项目被砍是必然。。。

整个团队重组之后的目标是希望制作一个可以快速面向市场的游戏,因为暴雪不希望在产品发布之间留下太长的空白期。当初计划的时间表如下:

  • Q4 1994 - 魔兽争霸
  • Q4 1995 - 魔兽争霸 II
  • Q4 1996 - 星际争霸计划发布日期

然而星际争霸的实际发布日期是1998年Q2

事后看来,想要这么快的做出这个游戏是多么的荒唐,但是当时公司的董事会背负着业绩的压力。由于暴雪前期的游戏的盈利远远超出期望,导致了对未来的增长产生了极大的预期。

所以在一个较短的开发时间加上有限的团队人力的情况下,星际争霸的团队目标本来就是开发出一个还行的游戏,一个最多可以称之为“太空兽人”(相对于魔兽争霸的“地上兽人”)的游戏。这里有一张1996年 E3 的游戏图片大概可以让你感受到当时团队对这个游戏的定位: 星际争霸早起定位图片

但是在开发的早期,一个优先级更加高的项目阻碍了星际争霸的整个开发进程,团队的主力开始一个一个的被转移到了那个项目中,那就是《暗黑破坏神》,一个角色扮演游戏,由一家在加利福利亚 Redwood 城的游戏工作室 Condor 开发。Condor 是由 Dave Brevik、Max Schaefer 和他的兄弟 Erich Schaefer 共同创办。暗黑破坏神在立项的时候只有120万美金的预算,就算在那个年代,这也是非常非常少的开发费用。

Condor 很快就发现,凭借自己的力量是无法将自己想要做的游戏完成的,但是他们用有限的资源完成了一个足够打动暴雪的产品,一个充满了创新和趣味的游戏。很快暴雪就收购了 Condor,并且命名为暴雪北方工作室,于是人力和钱就源源不断的投进了这个项目。

最初,星际争霸项目组的一个程序员Collin Murray和我一起飞到了 Redwood 来帮助暗黑破坏神的项目开发,其它暴雪总部的开发人员通过网络远程的支援暗黑破坏神的周边功能开发,例如战网系统、局域网对战、以及一些游戏非核心玩法的界面和系统,例如角色创建、游戏加入和其它一些细碎的功能。

当暗黑破坏神开发规模越来越大之后,最终暴雪总部的所有资源都投入到了这个项目中,美术、程序、测试各个部门都转移到了暗黑破坏神的开发,直到星际争霸项目已经一个人都没有剩下了。最后游戏的制作人都亲自动手开始写游戏的安装程序,之前那个安装程序我写了一半,到后面完全没有时间完成。

直到1996年底,暗黑破坏神发布之后,星际争霸的开发终于开始重启,于是大家聚到一起来看现在这个项目如何,情况很悲剧。游戏给人的感觉是已经过时了,并且一点也不让人印象深刻,和六个月前的 E3 上发布的很多游戏 Demo 比起来,一点竞争力都没有。

暗黑破坏神极大的成功刷新了暴雪对于未来游戏的预期,于是,星际争霸称为了第一个贯彻了暴雪那著名的“不为游戏品质做任何妥协”的座右铭的第一个项目。为了达到这个目标,整个项目的开发的道路上充满了荆棘。

高品质预期

当公司上上下下都在瞩目于星际争霸项目时,这个游戏势必变得越来越有野心。对它的期望,甚至超过了定义了“即时战略”这个游戏种类的两款前作——具有划时代意义的魔兽争霸 I 和 II 。

在星际争霸项目重启的那段时期,根据《Computer Gaming World》这个当年发行量最大的游戏杂志的主编 Johnny Wilson 的说法,估计一共有80款以上的即时战略游戏在进行开发。在有如此大量的竞品的情况下,特别是现代即时战略游戏的鼻祖 Westwood 工作室的竞争情况下,我们必须拿出一个非常惊人的产品。 > 译者注:Westwood 是一家游戏工作室,在即时战略类游戏上专注开发现代化战争背景的题材,并做出了非常成功的产品:命令与征服,红色警戒等。

并且暴雪已经不是之前的小公司了,在有魔兽争霸和暗黑破坏神的成功下,公司的新项目持续在被媒体和玩家所关注。在游戏界,大家都认为你的作品至少要和上一款一样的好。我们必须使游戏达到更高的品质才能满足大家的期望,而这种需求,对产品最终交付是有一定的风险的。

团队新面孔们

具有划时代意义的魔兽争霸 II 只有六个核心开发人员和两个辅助的程序员,但是对于星际争霸来说,同样的人力配置实在是不够,所以团队需要快速的扩张,很多没有被仔细评估的新人进入了团队,而且他们必须在没有太多指导的情况下就开始游戏代码的编写。

当时我们的编程管理相当的薄弱,我们那时还没有建立一个足够详细的工作指引给那些没有太多的游戏开发经验的程序员,让他们在一开始就能正确处理很多事情,而不是等到游戏发布之后再来反省。造成这个问题的另外一大原因就是,我们的开发量实在是太大了,每个程序员为了完成目标像是疯了一样去编码,完全没有时间去做代码的检查、审核和培训。

不仅仅是那些初级的程序员有这些问题,那些主程序和高级程序员也完全无法将精力放在游戏引擎的设计和改善上。Bob Fitch 是一个经验丰富的游戏程序员,他在这个项目之前主要的成果是游戏的移植,并且掌握了一个成熟的游戏引擎。他也参与过魔兽争霸 I 和 II 的开发,但是这两个游戏并不需要设计一个大规模的引擎。他之前主导的游戏《Shattered Nations》又被砍掉了,因此也很难证明他在游戏架构上做出的决定都是正确的。 > 译者注:Bob Fitch 是当时被取消的游戏《Shattered Nations》的主程,并且做为主要的程序员参与了星际争霸的开发。

整个团队在项目上付出了大量的心血,每个成员都牺牲了自己的健康和家庭时间,我从没有在任何一个项目中看到几乎每个成员工作的如此投入。但是一些开发中的关键决定,还是给整个开发蒙上了阴影。

在暗黑破坏神发布之后的几个月,我完成了一些收尾的工作并发布了一些游戏布丁,最终我回到了星际争霸项目组,帮助大家重启这个项目。在那时我天真的以为我不会又进入一个充满 bug 的项目,事实证明我实在是太天真了。

我以为我可以很快就能熟悉游戏现阶段的代码,并且上手工作,因为我几乎编写过魔兽争霸 II 的所有模块。但是我惊恐的发现,引擎中的很多模块都被废弃使用,然后在游戏代码中被部分的重新写过。

游戏的基础单位类型是被从头重写过的,而且单位的分发器模块也被废弃使用。分发器是我在之前游戏中开发的一个核心组件,它的功能是确保每个游戏单位在运行时有时机去思考自己应该干嘛。每个游戏单位都会周期性的被分发器轮询:“我是不是应该重新评估我的移动路线?”,“我在做完当前的这个行为后我应该干嘛?”,“是不是有一个比我现在锁定的单位更加适合的单位让我来攻击?”,“我死了,我该怎么清理我自己?”。

我知道重新编写一些代码是有好处的,但是废弃老的代码也会带来风险。Joel Spolsky在《Things you should never do,part 1》中曾经形象的说过: > 当你决定重新来一遍的时候,你必须告诉自己,你绝对无法肯定自己重头写的代码会比原来的更好。首先,很可能为之前一个版本工作的团队已经不存在了,所以你并不会比当初的团队成员更有经验。你所能做的往往是将之前已经发成果的的老错误重新来一遍,并且引入一些新的问题。

译者注:Joel Spolsky的这篇文章收录在《Joel on Software》,Joel博客的同名书;还有另一本书名为《More Joel on Software》。这两本书的中文译本是两卷一套的《软件随想录》,上述文章收入在卷一,“永远不要做的事情(第一部分)”。感兴趣的同学可以找来看看。

魔兽争霸的引擎在当初游戏的开发过程中,也是经历了好几个月的调整才最终完成的。当一个新的团队想要重写一部分代码来达成新功能时,他们需要花费大量的时间去重新学习引擎为什么当初会设计成这样。

游戏引擎架构

在微软的 DOS 平台,我开发了最初的魔兽争霸的引擎,当时用的 Watcom 的编译器,一切都是用 C 语言开发。在将发布平台切换到 Windows 系统之后, Bob 选择了 Visual Studio 编译器,并且重现将整个引擎用 C++ 重构了一遍。这都是合理的选择,但是在当时的团队中却很少有人有足够的 C++ 经验。

虽然 C++ 有很多优点,但是也是出了名的会容易被误用。语言的发明者 Bjarne Stroustrup 曾经有一句非常有名的笑话:”用 C 你很容易一枪射到你的脚,用 C++ 让你不是那么容易犯错,但是你一不小心就会把你整个腿都轰掉!”。

众所周知,在程序员每个使用了新语言的第一个项目中,他总是急不可耐的想要尝试这个语言的所有新的功能,所以星际争霸中到处都是类型继承的代码。如今所有有经验的程序员看到星际争霸的类型继承体系,都会摸一把冷汗,然后庆幸自己没有把自己的项目折腾成这样,我们游戏中的继承结构是这样的:

CUnit < CDodad < CFlingy < CThingy

CThingy 类的实例是可以出现在游戏地图任意位置的精灵,但是没有移动或是任何其它行为的功能。CFlingys 则是用来创建粒子,当一个爆炸发生时一堆这个类型的对象就会旋转着以爆炸中心点随机向外发射。CDodad 类的名字我花了14年还是没有搞清楚是什么含义,这个类也没有被实例化过,这个类包含了一些重要的函数用来让子类去实现。CUnit 类就是这个子类,实现了战斗单位的很多通用功能。整个游戏单位的行为被分散在这么多类型中,所以你想要做任何事情,你都需要通读并理解上面的所有类型。

译者注: 精灵对象是一种计算机图形相关的术语,表示一个有图形显示的对象

粒子是游戏中各种特效(例如爆炸、射击、光环等)实现的方式,由多个可以移动、变形、旋转等多种行为的精灵组成。

类的实例化是指,通过一个类型的定义,生成一个游戏中真实存在的游戏对象,类似于设计图纸和制作出的产品之间的关系。

而且噩梦不只是类型继承体系,最终的 CUnit 类型自身的定义也是大大的一坨,他自己的头文件就被分散在多个文件中:

class CUnit ... {
	#include "header_1.h"
	#include "header_2.h"
	#include "header_3.h"
	#include "header_4.h"
};

每个头文件都有好几百行的长度,你可以想象最终这个类的实现文件有多么的长,你只能把阅读这个类当作看一篇幽默小说。

唯一可以庆幸的是,早在整个程序员群体接受了那句 “使用组合,不要使用继承” 的名言之前,星际争霸团队的开发者就从痛苦中深深理解了这句话的含义。

我们离发布“只有两个月”了

由于之前的一堆问题,星际争霸项目重启之后,就一直想要加速整个开发进程。项目的时间表在内外的压力下始终处于“只有两个月”就要发布的状态。

但是我们的工作却非常艰巨,首先我们有大量的游戏单位和单位的行为需要添加,同时我们需要将游戏的画面从顶视图变为斜45度视角的画面,还有一个全新的地图编辑器,对了,还要加上基于战网的对战功能。我们根本无法在计划的时间内完成,就算我们假设是美术团队、音效团队、数值平衡团队和测试团队可以做到;事实上,程序团队在这种“只有两个月”就要发布的状态下,坚持了整整14个月。

整个团队在这样的压力下都在超负荷的长时间的工作,Bob 甚至有超过连续48小时在编码的纪录。但是以我在魔兽争霸系列的开发经验,那些我通宵写出来的代码,还有我在暗黑破坏神项目中连着一周,每天14小时工作产出的代码,都是我后期审查完决定重构的代码。熬夜提交的代码总是充满了问题,以至于你依然会在之后清醒的时间花同样的时间重写一遍。

长时间的工作很容易造成人们反应迟钝、昏昏沉沉,而当你想要在这种状态下完成一些脑力劳动,并且需要很多的创新,你可以想象会产生多少额外的错误和不可思议的 bug。

顺便提一下,这种长时间的加班工作并不是被强制要求的,而是简简单单的因为我们想要做一个好游戏。虽然现在看起来这是一件很傻的事情,我们完全可以将事情做得更好,并且在一个更加合理的时间范围内。

我最值得骄傲的一个成就就是,在两年时间内带领团队完成了《激战》的四个资料片,并且没有将团队拖入加班的深渊。

游戏崩溃的一些原因

我在星际争霸项目组主要的工作包括实现一些核心功能,例如战争迷雾、视线检测、飞行单位互斥寻路、语音聊天、人工智能增强和一些其他功能,但是我另外一个最主要的工作就是修bug。

等等,我插一句,你肯定想问,1998年的游戏就有语音聊天??是的,在1997年12月份的时候,游戏中的语音聊天功能就可以正常工作了。我试了一个第三方的语音采集压缩算法,采集了客户端的语音信息并在网络对战的其他客户端上重放。但是几乎我们办公室中的所有声卡都需要升级驱动程序才能正常的进行工作,就算是那些已经支持全双工的声卡也不例外,所以我建议将这个功能移除,否则游戏后期的技术支持工作会大幅增加,公司将在游戏发布之后花大量的钱来维持一个技术支持团队。

译者注:全双工的声卡是指由两个通道可以同时进行录音和放音,这在当年算是比较好的声卡标准,虽然现在已经是基础配置。

回到正题,修bug,是的,我修了很多的bug。当然有些是我自己犯的错,但是其中绝大部分难以抓住的 bug 都是那些疲劳的程序员写出的。当时团队中的一个程序员 Brian Fitzgerald(是我共事过的最厉害的两个程序员之一)曾经和我说过,当他对星际争霸进行代码复查的时候,被我修改过的代码数量震惊到了,我修改过的代码分布在整个代码库的各个模块上。至少我因为修 bug 得到了一些赞扬,虽然我并不希望如此。

对于如此多的问题,你可能会想象应该不会有一个单一的问题造成这么多的 bug,但是基于我的经验,星际争霸最多的问题都是因为游戏中使用了大量的双向链表。

链表在我们的引擎里面被大量的使用,主要是用来保存游戏中的单位和共享的行为对象。由于星际争霸相对于它的前作魔兽争霸来说,对象的数量翻倍的增长,魔兽中的对象顶峰大概是800个,星际争霸中的对象数量顶峰会在1600个左右。所以优化对象查找的算法变成对整个游戏运行效率至关重要的一换。

在我的回忆中,游戏里面到处都是链表。每个玩家的所有单位和建筑都放在一个链表里,每个玩家所有提供人口的单位也放在一个链表里,神族航母的所有小飞机也都放在一个链表里,还有很多很多其他的链表。

所有的这些链表都是双向链接的,这样所有的对象添加和删除都可以做到 O(1) 的时间成本里,并不需要你遍历整个链表才能进行这些操作。

但是不幸的是,这些链表都是“手动维护”的,没有任何通用的函数用来对链表的元素进行操作。每个程序员都在自己的模块里面写了链表的插入、删除和维护的代码。这些每人一份的手写代码和用同一份已经仔细审查和出错之后的链表操作代码比起来,简直就是 bug 的天堂。

更加不幸的是,有些链表的元素(多半是指针)会被共享的放到多个链表中,所以想要知道一个链表元素是否可以安全的删除并回收他的资源是非常困难的。而且有些链表的指针字段是通过 Union 的方式存储的,这样虽然可以节省内存,但是造成更多可能产生 bug 的可能性。

于是我们的游戏就一直在崩溃,一直。。。在崩溃。

为什么我们要这么做

悲剧的是,我们完全可以避免如此滥用链表的问题。Mike O’Brien, Jeff Strain 和我曾经写过一个通用的软件库,叫做 Storm.DLL 。在暗黑破坏神中,我们就使用了这个软件库。它有很多的功能,其中也包括了一个完整实现操作双向链表的模板库。 > 译者注: Mike O’Brien 参与了暗黑破坏神的工作,并且全程参与了星际争霸的开发。 Jeff Strain 主要参与了星际争霸的战役编辑器的开发

在星际争霸立项的时候,我们就决定了要使用了这个库。但是在开发的早期,这个库就被弃用了,很多模块都变成了在游戏各个模块中手写的代码,而最早弃用的原因是为了让游戏存盘的逻辑更加简单一点。

游戏存盘

在魔兽争霸出现之前,我玩过的很多游戏的存盘逻辑都很糟糕。玩家总会觉得每次存盘的时间很长,虽然硬盘的写入速度确实比内存慢了很多数量级,就像是自行车和跑车之前的差别,但是也没有道理不去优化它,我在魔兽争霸中就做了一些改善。

所以在魔兽争霸中,我使用了一些办法,可以将一大块内存直接的写入到磁盘,而不需要一个一个字段进行写入。整个单位的数组(前面提到,最多是600个单位)可以一次性的写入磁盘。所有非指针性质的全局状态数据也可以用同样的方法写入磁盘,例如战争迷雾的状态和游戏当前的地形数据。

虽然事后发现,这样的一次性写入并没有明显的减少存盘的时间,但是这种机制却大幅度的降低了存盘逻辑的复杂度。不过这样的机制必须要求单位数据中必须没有指针。

不幸的是,星际争霸的单位包含了大量的双向链表,而双向链表中包含了大量的指针。数据的复杂度相对于魔偶争霸的单位来说简直就是一个怪物。你必须在存盘时特殊处理那些链表指针(还要特别小心那些使用 union 存放的指针),这样才能将1600个单位的数据一次写入存盘文件,并且在读取之后再把这些指针结构还原,唉~

改回来!

当修正了无数无数个链表问题之后,我在团队中强烈要求,一定要将所有的链表使用改回 Storm.DLL 中的版本,就算这样会让游戏保存的代码复杂度增加。当我提到”强烈要求”的时候,其实或多或少,这是在暴雪内部唯一的讨论方式。和这一群天才、自大、聪明且年轻的人共事时,没有什么争论不是”强烈”的,除非时讨论当天午饭吃啥,因为没人想去做这个决定。

很可惜,这场争论我并没有说服谁,因为我们还有”两个月”就要发布了,对游戏底层进行改动的提议总是不如打一个补丁来修正问题的方案让大家所接受。这种补救的方案导致了连续几个月的苦难。

更多的创口贴:寻路

我想提一下另外一个例子,游戏中单位的寻路模块,也是一个不停的针对问题打补丁的方式,而不去修正模块底层的问题根源。当星际争霸从顶视图修改为斜45度视角的游戏之后,游戏的瓦片渲染引擎是完全没有改进过的,依然用的我在1993年编写的代码。

使用这种方形瓦片渲染引擎去渲染斜45度游戏并不是那么的难,但是当你想要支持地图编辑器时麻烦就来了。因为在底层这些斜对角的图形其实是在正方形的瓦片上渲染的,所以在边缘的显示上有很多的修正。

但是最麻烦的其实是寻路算法的实现。由于底层的地图其实是正方形的瓦片,所以不能直接的设置一个32*32像素的斜对角图形是不是可以行走。我们必须将其分解为16个8*8的正方形瓦片,来作为寻路的最小地图单位。这样不仅仅造成了寻路算法的复杂,还导致了某些大型单位会寻路到一些自己挤不过去的狭窄道路。我计划单独写一篇文章来描述星际争霸寻路这部分所遇到的问题。

总结

我想你目前大概了解了星际争霸制作的种种困难,大部分的困难都是由于公司上上下下每个层次做出的错误选择,包括了产品的方向、技术的选型和游戏设计。

非常幸运的是,我们虽然是一群充满了做出好游戏激情的人,同时也最终认识到了项目的种种问题。在游戏发布之前我们早早的就决定停止新增功能,并仔细打磨现有的代码。感谢编译型的语言,对于玩家来说只会看到一个测试完整的产品,而不是真个开发的恐怖故事。

在后面的文章中,我会更加详细的描述一些技术问题,包括引擎的设计、寻路算法等。同时也会讨论如何实现一个合理的双向链表,多谢耐心的阅读到这里。

comments powered by Disqus