目录 ('t kZt%8
QsOhz
2 游戏实践讨论 =Ey`M#t;
2.1 制作概况 n>P!u71
2.2 模块划分 Noh?^@T`Ov
2.3 游戏引擎 IZ 8y}2
2.4 关键讨论 OC_M4{9/
2.5 希望 J3G7zu8
_UkmYZ/
)r9b:c\
2 游戏实践讨论 W/r^ugDV
I]X
现在有很多人对国产游戏事业是又爱又恨的。爱的是希望能够出现一批真正属于我们自己的好游戏,恨的是这些游戏制作者们太不争气,到现在也没有做出来。我非常感谢他们,因为假如没有他们,也就没有了我们。我也感到非常抱歉,因为我们的作品目前还不能让人们满意。我想多说什么都是无用的,只有实事才能说话。前面我所说的大多是理论上的内容,是我们在几年的游戏制作过程中总结出的经验。这些内容有些可能是错误的,因为我们还没有从正面证明它,有些对我们至今仍然只是个美好的愿望,我们自己还没有真正做到。所以请大家在阅读时针对自己的情况进行取舍。 Wr>(#*r7q
下面我利用我们曾经做过的一个游戏,具体分析它的制作过程和制作方法。希望借此为那些关心游戏制作的人提供尽可能多的材料,让他们了解得多一些。也为那些有志于游戏制作的人提供一些经验和教训,让他们少走一些弯路。要说明的是,我们在处理某些问题的时候,所使用的方法很可能是非常普通的,甚至是笨拙的,别人看来可能有更好的办法。但是我不想与各位争论,假如您有什么更好的想法和办法,就把它用到你的游戏中去吧,我希望每一个喜爱游戏制作的人都能够制作出更好的游戏来。 pCC 7(Ouo
9=
V>f)R
2.1 制作概况 x l0DN{PG
<赤壁>作为瞬间工作室成立以来的第一个作品,是1997年7月上市的,这个版本称作标准版。其后增加了网络功能,修改了一些Bug,增加了一些游戏事件,被称作增强版,增强版于1997年底上市。在此期间,我们又制作了日文版,韩文版和繁体版,又为国内的OEM厂商制作了相应的版本。据说<赤壁>全部的销售量超过了十万份,其中零售量超过一万五千份。这是与前导公司所有员工的努力分不开的。 aX^+ O,
Pdw#o^Iq^
<赤壁>的策划工作开始于<官渡>制作的后期,1996年6月。而程序开始的时间要晚一些,在1996年9月。那时侯我们碰巧见到了另一部国产游戏<生死之间>的早期版本。这给我们的震动非常大,因为这两个游戏的类型比较接近,而我们才刚刚开始这个游戏的制作而已。从程序开始动工,到第一个版本发行,一共9个月,先后参与程序编写的程序员有5人,总的工作量大约40个人月。程序代码的总量约为90000行,2.6MB。大部分用C++编写,少部分由C和汇编编写。我们使用Microsoft Visual C++ 4.0编译<赤壁>标准版,Microsoft Visual C++ 5.0编译<赤壁>增强版。可执行文件大小约为500多KB。 4<.O+hS
nLy#|C
<赤壁>的工作进度如下: DZe}y^F
程序设计期:1996年9月初至1996年11月底。 5lTD]d
底层制作期:1996年12月初至1997年2月底。 Q.k
:\m*h
游戏编写期:1997年3月初至1997年5月底。 /s
c.C
游戏测试期:1997年6月初至1997年6月底。 ]>Si0%
i[150g?K
日文版: 1997年8月。 iCTQ]H3
OEM版: 1997年9月。 7yI`e*EOD
韩文版: 1997年11月。 dn,g Z"<
繁体版: 1997年12月。 $D'^t(
增强版: 1997年12月。 cS|VJWgTZ
i-W
需要说明一点:那就是我们的所有程序全部都是自己完成的,没有使用任何其他人其他公司提供给我们的代码。要知道,我们公司当时还没有能力去购买国外的游戏引擎,而我个人连SoftICE都不会使用,更不要说跟踪研究别人的代码了。我们对别人的学习方式非常简单和直接,就是观察。通过观察猜测它所使用的方法,然后考虑自己如何把它实现。这可能是我想到的最笨的一种方法,如果一个程序员能力强,在制作游戏以前,详细分析了解别的游戏的算法我想一定是非常有用的,他在制作游戏时一定可以节约不少走弯路的时间。 '# z]M
另外,我们对<赤壁>的测试时间也是很短的,在程序基本稳定之后,我们大概只剩下两个星期左右的时间。所以有很多Bug。 Q>8pP \ho
[;KmT{I9
2.2 模块划分 st/n"HQ
\dq!q=b\
赤壁的程序分成五个大的部分和19个模块: ug*D52?
显示模块 4DLq}v
战场显示模块 zX kx7d8
分为通用显示底层和游戏战场显示。 Sdd9Dv?!
通用显示底层是基于DirectDraw的一套函数。 3]U]?h
有关内容请详见DDApi.h, DDCompo.h。 !gH9 ay
~O;y?]U
游戏战场显示是根据游戏单元的类型,位置,状态,动画帧等数据将单元位图以适当形式显示在战场的适当的位置上。要显示的内容有:地形,单元(士兵/建筑/将领),攻击效果,魔法效果,远程武器物体,阴影等。 hazq#J!
主要功能有:对图素的压缩和读取,图像的显示,单元归属颜色的转换,边界剪裁,遮挡关系,缩略图显示和响应,屏幕移动,阴影遮挡判断,攻击效果,魔法效果,远程武器物体的显示和移动。 Pl+xH%U+?
有关内容请详见CBDraw.h, 显示单元的位图 hVP
IHQt
CBDrawM.h, 显示特殊效果,比如水。 n#*`!#
CBDrawOT.h, 显示特殊物体。 ~|lIC !q
CBMini.h, 显示缩略图。 kIvvEh<L=
CBMap.h, 图素的操作。 <\@1Zz@ms
CBShadow.h, 阴影的计算和显示。 }B q^3?,#{
CBOther.h, 特殊效果的显示和计算。 47UO*oLS
T&xt`|
界面显示模块 MJ\[Dt
根据游戏设计需要,在显示器相应的位置上显示游戏的各层界面。 *8)2iv4[
它分为两个部分:界面底层部分和游戏界面部分。 &Un6ay
界面底层是属于底层部分的通用函数库。它包括按钮,对话框,滑块,检查框等界面元素的实现。 ~p*1:ij
有关内容请详见DDBitmap.h, 显示位图的基类。 {8@\Ij
DDButton.h, 按钮。 N[Sb#w`[/
DDCheck.h, 检查框。 _3>djF_u
DDList.h, 列表框。 O8|*M "
DDMenu.h, 菜单。 b |7ja_
DDScroll.h, 滚动条。 1;&;5
DDText.h, 文字。 =Q(vni83<
DjHp+TyT
游戏界面主要有:游戏主菜单,新游戏菜单,读取进度菜单,保存进度菜单,网络选择菜单,系统设置菜单,任务提示菜单,结束菜单,错误处理对话框等。 8)xt(~qF
有关内容请详见Interfac.h, 提供基本的游戏界面接口函数。 ~rv})4h
Interfa1.h, 处理所有的按钮Button信息。 $/_qE
Interfa2.h, 处理所有鼠标操作发出的消息。 0a2@b"l
net_face.h, 网络部分界面。 cDV^8 R
Marco.h, 所有按钮的消息ID。 $h28(K%
CBprompt.h, 游戏战场中屏幕右方信息的显示。 "0&N}
CBAarray.h, 对游戏元素的查询。 (/h5zCc/v
DDComUn.h, 针对游戏中下达命令时的命令组。 'v&}(
S>Z|)I
过场动画模块 pOga6'aB)
显示公司标志,制作群,历时回顾,片头,片尾和过场动画。 >UHa
它有两个部分,第一是播放视频图像,第二是调用其它进程。 #S5`Pd!I
有关内容请详见CBAvi.h, 视频图像的播放。 h`5)2n+ P
Mciapi.h, 播放AVI文件的底层函数。 XU-m"_t
VCMApi.h, 高效率的播放AVI的底层函数。 K: r\{#9
CBGame.h, 播放结尾。 *t9eZ!_f?
Interface.h, 程序状态的转换。 H?yE3w
Q:MhjkOr}
单挑显示模块 kzO&24
武将单挑时出现的专门画面。本部分与原始设计有出入,原始设计中可以对单挑进行操作,后来删减称为只播放一段Video。有关内容请详见CBAvi.h。 'Qn~H[$/p
KhaYr)&~
控制模块 F}X0',
鼠标控制模块 7m1KR#j
根据鼠标的位置设置鼠标的形状,对鼠标的操作对响应单元发出命令。 Q\kub_I{@
主要内容有:鼠标点击检测,目的地模式,命令构造,可建造区域判定,鼠标形状转换。 Sm|(
鼠标点击检测,主要判断鼠标点击的位置是否在某个单元上或地形上。详见CBMouse.h。 m)&znLA
目的地模式,主要控制鼠标选择了单元(命令主体)后,可能对单元下达的命令的模式,根据不同的命令可能需要不同的目的地类型和命令参数。详见CBCtrl.h。 SEF6B45}1
命令构造,通过鼠标的点击选择或按下某个,命令按钮,构造出具有命令主体,命令ID和命令客体的命令,放到命令队列中,供执行。详见CBCtrl.h。 \#dl6:"
可建造区域判定,在建造建筑时需要判断哪里可以建造哪里不可以, 并且显示出来。详见CBBuild.h。 .T.5TMiOSq
鼠标形状转换,鼠标移动到某个界面或某个单元上时,或处于命令构造阶段时,需要对鼠标的形状做一定的改变,以显示当前的操作状态。详见CBMouse.h。 $.K?N@(W
Cg!^S(U4
命令处理模块 or_+2aG
根据鼠标和人工智能发出的命令,传送给对象单元,并将其转化成为单元的相应状态序列。 c3xl9S,5
根据单元状态,判断单元状况,更改单元的状态。 H+ZSPHs
详见CBCtrl.h, 命令的构造保存和传递。 =_pwA:z"A
CBRun.h, 命令的执行。 r;qzo.
CBRDelay.h, 命令执行时需要的一些变量。 1n%8j*bJq
3qMNl>>
攻击计算模块 4]XI"-M^D
根据敌我双方的攻防力量,计谋的实施和阵型计算每一次打击敌人生命的损失。 "x*-PFT
详见CBRun.h, 攻击计算。 ,&]MOe4@>
CBZhenFa.h, 阵法计算。 '2^
Yw
3071:W
行军控制模块 #DI$Oc
根据单元的位置,速度,目的地和地形数据,计算行军路线,设置单元的下一步。详见March_n.h。 /-Qv?"
p25Fn`}H
网络控制模块 +,flE=5]s
游戏数据在网络上的传递,纠错。模拟机的建立和管理。详见Network.h。 >+9JD%]x]
fCX*R"
策略模块 ;")A{tX2
君主策略模块 J7&DR^.Sw
计算机一方根据战场双方的力量对比和战斗模式计算对单元的生产,对敌人的攻击,产生命令。 Fhj8lVvk
详见TEmperor.h。 [}o~PN:sT(
k%Vv?{g
将领策略模块 H\G{3.T.9
每一支部队根据将领的属性,士兵的状况调整战斗的方式或判断撤退。 jqcz\n d
详见TGeneral.h。 GJQc!cqk
Yx)o:#2
本能策略模块 I6w~H?ul@*
士兵单元面对周围的情况产生固定的基本反应。 B)=~8wsI:Z
详见Tai.h。 ($!KzxF3
Tbnbase.h, 人工智能中需要的数据结构。 M##';x0
CBEyes.h, 人工智能与游戏主体结构之间的接口。 e!x6bR9EZ
{aj/HFLNY
文件模块 %c/^_.
资料数据文件模块 %:u[MBe ,
地形和单元图素的图像文件,相应控制数据文件。单元的各项属性数据文件,操作用数据文件,界面位置数据文件。 )]Ti>R O7
详见CBData.h, 游戏中使用的单元属性全局数据结构。 s#-eN)1R
CBMap.h, 游戏中使用的地图图素,单元图素全局数据结构。 t#~?{i@m
CBGame.h, 游戏中使用的单元全局数据结构。 F@vbSFv)/
Cmd329AH
存储数据文件模块 Rp.W,)i
存盘用数据,记录战场上的所有单元的状态和思考数据。 eaZQ2
详见CBGame.h, 存盘。 7'w0
Q/^A #l[
其它模块 sic$uT
N:BL=}V
文件封装模块 Dpqt;8"2L
为减少程序使用的文件的数量,增加程序的安全性,将大量的图像文件和数据文件封装起来,供程序调用。 2(#Ks's?
详见L_Allbmp.h, L_Image.h, L_Save.h, L_Scan.h, L_Text.h, Tools.h。 Dy9\O77>
<8o(CA\
声音模块 @LX6hm*}
背景音乐和音效。有混音和音量控制。 lrEj/"M
详见DsWave.h, 播放WAV文件。 tIZ~^*'
WS0JS'
文字模块 0Xl%uF+w
在非中文系统下显示汉字,日文,繁体汉字和韩文。详见puthz.h。 \cySWP[
'fW#7W
地图编辑器 Ka-p& Uv1<
为使美术人员方便快捷规范地制做战场地图,提供专门的地图编辑器。同时为整个游戏的文件系统,显示系统做技术上的准备。 `~F5wh~
这是一个单独运行的程序,详见Mapedit。exe。 Plo ,XU
$aP(|!g
安装程序 4\2V9F{s
将游戏安装和卸载。详见Setup。exe。 |!*Xl)
]
^PqF<d6
2.3 游戏引擎 +V8b
说到游戏的引擎,很多人都不知道它是什么,以为制作它有多么困难。引擎的概念也很混乱,至少现在我还不知道它的确切定义。但我想如果一个东西要被称作引擎,它应该具有这样一些特点: {]/8skov5]
它应该是由函数组成的。 Zz"}Cz:bX
它应该实现某项具体功能。 H7&xLYQ2
它应该是完整的。 =xl~][
它应该可以被重新使用。 zICI_*~
8k!6b\Imz
从上面的要求可以看出,其实这就是作为底层程序的要求。我想没有必要把引擎认为是游戏的现成编写工具,只要2改一下美术就是另一个游戏了。只要这些程序代码将会被我们应用在以后的游戏中,我们就已经节约了很多的时间和精力。 6`e@$(dfA
下面我会说一下在<赤壁>的代码里,哪些将被看作我们的引擎。实际上,这些部分经过一些修改后正在被我们应用到新的产品中。 }vh Za p^
k3hkk:W
显示底层: Ill[]O
这是一套包裹在DirectDraw外面的函数。为了简化在调用DirectDraw函数时的复杂度我们使用了一些缺省参数和内部错误处理函数。建立了一个CDDSurface类库,使得对位图的使用更加简单。详见DDApi.h yp]@^T N
在DDCompo.h中我们有关于游戏鼠标的一套操作。在屏幕独占模式中,Windows标准鼠标有时显示会不正常。于是我们自己制作了鼠标的显示方法。方法很简单,在每帧读取鼠标的位置,然后在该位置上显示一张位图。 }KB[B
在赤壁里面,我们没有使用双缓冲区的模式,而是只更新某个特定的区域。它的优点是当需要更新哪里的时侯就更新哪里,对于哪些在每帧中都只有小面积图像需要更新时是非常高效的。比如在486上,<赤壁>的主游戏界面里的鼠标移动仍然是十分流畅的。可惜的是,在<赤壁>的战场部分,它并没有优势,因为基本上是需要全屏刷新的。 .b>TK
在未来的游戏制作中,因为计算机的速度越来越快,所以我们当时所使用的模式恐怕变得不太适用,双缓冲区模式应该是主流方向。 v[ ,Src
X[hM8G
多媒体底层: w G!u+
主要包括声音和视频。我们使用了MCI设备来播放AVI,WAV,MIDI,CD AUDIO等内容。那曾经是我们在做上一个游戏的时侯完成的部分。但是它有很多缺点,比如不能同时播放多个WAV文件,这对于我们制作游戏音效是很重要的内容。 b-<HXn_Fd
所以我们又使用DirectSound来播放声音。这里的难点在于当我们需要播放很长的文件时,不能一次读入,而需要建立新的线程按时装入声音。好在现在大部分游戏都使用CD Audio作为背景音乐,不需要WAV。 W{Q)-y
pj{\T?(
界面底层: =_d%=m
基于显示底层之上的界面元素其实并不好做。因为我们总希望它的响应方式与Windows95中相同。而大家在<赤壁>里看到的内容就与Windows95有些不一样。比如滚动条(ScrollBar)对鼠标的响应就非常简单,按钮(Button)的反应也有所不同。但是好在它比较简单,易于使用。 ]H[8Z|i""
/9 hR
在每做完一个游戏之后,我们都习惯要把某些东西整理一下,看看它是否可以在以后被使用起来。而往往这些东西也都是需要不断修改的。因为程序运行的平台不一样了,它的用途也不一样了,而我们的编程水平也不一样了。但总之这些代码被较为完整地保留了下来,它必将是我们今后编程的基石。 k
onoI&kV|
Vz:_mKA
2.4 关键讨论 P:!)9/.2
C7qYiSv
我刚开始编写游戏的时侯总有一个想法,只要游戏的主要部分写完了,游戏也就差不多了。我也遇到过一些游戏制作组的成员,他们也大都是这样的想法,认为只要把游戏的演示版拿给别人一看,然后只要再投资让美工画一些画,游戏就可以做完了。其实事情并不想想象中那样简单。 S*t%RZ~a
在我看来,把游戏的大概样子做完了,顶多占整个游戏的三分之一。另外的两个三分之一分别是整个游戏的制作和测试。 h=+$>_&:
举个简单的例子,比如我们在演示版中通常只有一个兵种和一个战场。游戏的显示效果可能很不错。但是,真正在游戏中不会只有一个兵种的,每方都会有大概十种兵,又会有三四方的敌人,这时侯你的显示底层是否能够胜任呢?内存是否会占用太多呢?这时侯还需要我们对其进行优化和修改。连游戏的底层显示部分都可能需要修改,更何况游戏中还有更多的内容呢? ;=;JfNnbm
,0?!ov|
下面我举一些<赤壁>中的例子,这些都可能是极小的问题,但都是我们需要仔细考虑的问题。在你准备开始制作一个即时战略游戏之前,你是否曾经考虑过这些问题呢?假如你对这些问题有所了解,那么你就应该可以非常有把握地马上开始制作游戏。如果没有,也没有关系,因为这些问题我也没有全都事先考虑过。 `/:cfP\
假如你有时间,可以对你自己的游戏多多考虑一下,这个游戏距离一个真正的产品,到底还缺什么?还有哪些模块和部分没有做完?当你对两者之间的差距有了一个明确的认识后,也就不会担心了。任何东西都是一点一点做出来的,只要按照你想做的内容去做就可以了。 Ot9V< D6h
f(:1yl\a
程序状态的转换 3N4.$#>#9@
我们在写DirectX程序的时侯,总有一种偏见,那就是不希望Windows界面出现在我们的游戏里。于是什么都需要我们自己做。比如说窗口。因为窗口的刷新需要我们自己管理,就觉得没有必要生成多个窗口了。这样所有的窗口消息就必须在唯一的一个函数里实现。可是我们的游戏里有很多种不同的操作,比如界面,系统菜单,播放视频等,这些内容就都必须在这个地方处理。所以我们就引入了程序状态这个概念。我们定义了一系列的状态,在每个状态里,有固定的操作和响应,状态之间的转换也在特定的时侯进行。这样我们就很容易把一些关系不大的内容独立开来,降低程序的复杂性。 ([k7hUP
其实我们在实现这一部分的时侯是很混乱的。你很难在代码中找到所有状态转换的地方。但是它的实现很简单,一般的规则就是程序的对称性。有专门的装入函数和释放函数,然后有显示函数,计算函数,鼠标消息响应函数,热键响应函数。在内部,需要结束本状态时就发出一个状态转换的消息。在外部,只要在主程序的主循环和消息响应函数处针对不同的状态执行它们不同的函数就可以了。 3LK%1+)4
这里的关键在于状态的转换。因为状态在转换中一定会释放和申请大量内存,如果有的内存没有释放,转换次数一多就会出现问题。如果我们把状态转换的地方写好了,程序看起来也非常干净整齐,Bug也会比较少。 N6/T#UVns
现在编写游戏,需要装入的图量非常大,很有必要制作一个装入时的画面,并且显示百分比。这是我们可能需要一个装入中的状态。遗憾的是<赤壁>并没有实现这个,它的淡入淡出效果仅仅是效果,程序在执行这部分时停止在这里。 8jnz}aBd
有关内容请详见CBGame.cpp和Interfac.cpp。 !1:@8q
w]!0<
执行任务 R}{GwbF_\
一个士兵在接到我的命令之后,便开始了它的行动。它察看了一下它的命令,这是由两个部分组成的。它先取出第一个部分,是行走。于是根据自己现在的位置和目的地位置先计算了一下路线,把自己当前的状态设置为正在移动,然后根据计算好的方向和自己的速度把自己移动到一个位置。如果这个位置已经到达目的地了,它就停止行走。又察看了一下剩余命令的部分,是攻击。于是它拔出刀砍向附近的一个敌人。敌人死了么?它不停地问自己。如果敌人真的"哇"的一声倒下了,它就得意地站在一旁,微笑着。 0i@:KYP
这就是我们在战场的一个角落所看到的一个士兵的表现。而实事上,在程序里我们也是这样做的。 ><Z'D
我们把每个士兵做为一个单元,独立地处理自己的事物。我们把每个命令划分得更加细致,而称其为状态,执行每个状态时所需要的参数的执行的步骤是最简化的。每个命令都是由一个或多个状态组成的。一个状态满足后就自动转向下一个状态。我们为每个状态都编写专门的开始,运行中,和结束代码。这样每个士兵都在独立地按照顺序处理自己的事务,一个个复杂的单元行动就变得有条有理了。 %xlpB75N4N
有关内容请详见CBRun.cpp。 1y[B[\
HOPqxI(k
阴影 !:
us!s
有人说Warcraft做得好,有人说C&C做得好。我属于前者一派,这不仅仅因为我对Warcraft的观察多一些,而且有一个理由足以说明Warcraft在程序上比C&C更高一筹。那就是阴影。大家都知道Warcraft是双层阴影,而C&C是单层。双层阴影的好处是更加真实。在我们去过一个地方之后,虽然我已经看清楚了地形,但是这里敌人的活动,我们不应该永远知道。可是要把单层阴影变成双层,并不是只要加上一层显示就可以解决问题的。 5K.+CO<
首先,增加一层阴影就增加了需要显示的时间。时间对于即时战略来讲是至关重要的。它直接影响到游戏的表现效果和操作速度。多加一层阴影就意味着减少了我们增加效果的机会。在我们这里,双层阴影占用了大约5-10%的显示时间。 m_lrPY-
其次,增加一层阴影就增加了特殊的计算。一层阴影只需要一个二进制数组记录哪里被打开,哪里被隐藏就可以了。而现在则不同,我们需要把那些已经打开的阴影再关上。我们的做法是生成一个二维数组,每位表示一个图素格子。当有一个士兵的视野可以打开这个格子上的阴影时,就把那里的计数器加一,离开时减一。当为0时则这块地形被重新隐藏。 v'ay.oVzw
第三,阴影下的单元需要特殊的处理。当我的士兵离开敌人阵地的时侯,敌人附近被半透明的阴影重新覆盖了,这时我们需要把正在那里活动的敌人士兵隐藏起来,而建筑不动。 =>LZm+P
这就是双层阴影所带来的。可是为了效果,我们不得不如此。看起来还是达到了效果。 %+tV/7|F
除了双层阴影以外,阴影的边界也是很重要的问题。我们不可能把阴影做得和刀切的一样,而让它必须和打开的地面相结合。于是我们必须要有一套贴图,用于阴影边界的各个方向。好在我们在这里利用了一个偷懒的办法(当然是很巧的办法,是Onefish想出来的)。我们采用了一个椭圆的贴图,让相邻的椭圆相切,从而造成边界。因为椭圆是没有方向之分的,我们也就节省了一些贴图的内存和对使用哪张贴图的复杂计算。 &RY