目录 FE\E%_K'n7
Zu.hcDw1
2 游戏实践讨论 h p|v?3(
2.1 制作概况 QEs$9a5TE
2.2 模块划分 rJ Jx8)M
2.3 游戏引擎 #gQn3.PX+y
2.4 关键讨论 ByY2KJ7
2.5 希望 RqTO3Kf
8TFQ%jv
wnokP
2 游戏实践讨论 9,'m,2%W
Qb^G1#r@C
现在有很多人对国产游戏事业是又爱又恨的。爱的是希望能够出现一批真正属于我们自己的好游戏,恨的是这些游戏制作者们太不争气,到现在也没有做出来。我非常感谢他们,因为假如没有他们,也就没有了我们。我也感到非常抱歉,因为我们的作品目前还不能让人们满意。我想多说什么都是无用的,只有实事才能说话。前面我所说的大多是理论上的内容,是我们在几年的游戏制作过程中总结出的经验。这些内容有些可能是错误的,因为我们还没有从正面证明它,有些对我们至今仍然只是个美好的愿望,我们自己还没有真正做到。所以请大家在阅读时针对自己的情况进行取舍。 _/LGGt4&%
下面我利用我们曾经做过的一个游戏,具体分析它的制作过程和制作方法。希望借此为那些关心游戏制作的人提供尽可能多的材料,让他们了解得多一些。也为那些有志于游戏制作的人提供一些经验和教训,让他们少走一些弯路。要说明的是,我们在处理某些问题的时候,所使用的方法很可能是非常普通的,甚至是笨拙的,别人看来可能有更好的办法。但是我不想与各位争论,假如您有什么更好的想法和办法,就把它用到你的游戏中去吧,我希望每一个喜爱游戏制作的人都能够制作出更好的游戏来。 5`.CzQVb
~o!-[
2.1 制作概况 Vx $;wU Y
<赤壁>作为瞬间工作室成立以来的第一个作品,是1997年7月上市的,这个版本称作标准版。其后增加了网络功能,修改了一些Bug,增加了一些游戏事件,被称作增强版,增强版于1997年底上市。在此期间,我们又制作了日文版,韩文版和繁体版,又为国内的OEM厂商制作了相应的版本。据说<赤壁>全部的销售量超过了十万份,其中零售量超过一万五千份。这是与前导公司所有员工的努力分不开的。 %Xd*2q4*
'Tm1Mh0Fso
<赤壁>的策划工作开始于<官渡>制作的后期,1996年6月。而程序开始的时间要晚一些,在1996年9月。那时侯我们碰巧见到了另一部国产游戏<生死之间>的早期版本。这给我们的震动非常大,因为这两个游戏的类型比较接近,而我们才刚刚开始这个游戏的制作而已。从程序开始动工,到第一个版本发行,一共9个月,先后参与程序编写的程序员有5人,总的工作量大约40个人月。程序代码的总量约为90000行,2.6MB。大部分用C++编写,少部分由C和汇编编写。我们使用Microsoft Visual C++ 4.0编译<赤壁>标准版,Microsoft Visual C++ 5.0编译<赤壁>增强版。可执行文件大小约为500多KB。 gsyOf*Q$
n{;Q"\*Sg
<赤壁>的工作进度如下: 0 #8
程序设计期:1996年9月初至1996年11月底。 i\6CE|
底层制作期:1996年12月初至1997年2月底。 DEZww9T2Qs
游戏编写期:1997年3月初至1997年5月底。 \EfX3ghPI
游戏测试期:1997年6月初至1997年6月底。 49MEGl;K0\
F"]P|
日文版: 1997年8月。 - Z,Qj"V
OEM版: 1997年9月。 L[Vk 6e
韩文版: 1997年11月。 *SNdU^!
繁体版: 1997年12月。 )$n%4 :
增强版: 1997年12月。 /A7( `l;6
r!Aj5
需要说明一点:那就是我们的所有程序全部都是自己完成的,没有使用任何其他人其他公司提供给我们的代码。要知道,我们公司当时还没有能力去购买国外的游戏引擎,而我个人连SoftICE都不会使用,更不要说跟踪研究别人的代码了。我们对别人的学习方式非常简单和直接,就是观察。通过观察猜测它所使用的方法,然后考虑自己如何把它实现。这可能是我想到的最笨的一种方法,如果一个程序员能力强,在制作游戏以前,详细分析了解别的游戏的算法我想一定是非常有用的,他在制作游戏时一定可以节约不少走弯路的时间。 ~</FF'Xz
另外,我们对<赤壁>的测试时间也是很短的,在程序基本稳定之后,我们大概只剩下两个星期左右的时间。所以有很多Bug。 !1)aie+p6
",b:rgpRp
2.2 模块划分 Dx-P]j)4x
m8fj\,X
赤壁的程序分成五个大的部分和19个模块: g,+e3f
显示模块 X`D2w:
战场显示模块 h-P|O6@Ki
分为通用显示底层和游戏战场显示。 <c}@lj-j
通用显示底层是基于DirectDraw的一套函数。 KyyRHf5
有关内容请详见DDApi.h, DDCompo.h。 Y*c]C;%=
2l)"I
游戏战场显示是根据游戏单元的类型,位置,状态,动画帧等数据将单元位图以适当形式显示在战场的适当的位置上。要显示的内容有:地形,单元(士兵/建筑/将领),攻击效果,魔法效果,远程武器物体,阴影等。 .H)H9cmf
主要功能有:对图素的压缩和读取,图像的显示,单元归属颜色的转换,边界剪裁,遮挡关系,缩略图显示和响应,屏幕移动,阴影遮挡判断,攻击效果,魔法效果,远程武器物体的显示和移动。 dTg`z,^F
有关内容请详见CBDraw.h, 显示单元的位图 /]`@.mZ9:
CBDrawM.h, 显示特殊效果,比如水。 3NpB1lgh&:
CBDrawOT.h, 显示特殊物体。 q}P@}TE
CBMini.h, 显示缩略图。 %l7[eZ{Y
CBMap.h, 图素的操作。 QXkA%'@'
CBShadow.h, 阴影的计算和显示。 j*DPW)RkKX
CBOther.h, 特殊效果的显示和计算。 LlX)xJ
sC-o'13
界面显示模块 ^#:;6^Su
根据游戏设计需要,在显示器相应的位置上显示游戏的各层界面。 6j6CA?|
它分为两个部分:界面底层部分和游戏界面部分。 }:#WjH^
界面底层是属于底层部分的通用函数库。它包括按钮,对话框,滑块,检查框等界面元素的实现。 LL( xi )
有关内容请详见DDBitmap.h, 显示位图的基类。 8S1@,O,
DDButton.h, 按钮。 NpH8=H9
DDCheck.h, 检查框。 0zr27ko
DDList.h, 列表框。 A"JdG%t>.h
DDMenu.h, 菜单。 fa/S!%}fO
DDScroll.h, 滚动条。 EsGu#lD2
DDText.h, 文字。 O@Aazc5K
q|D5
A|)
游戏界面主要有:游戏主菜单,新游戏菜单,读取进度菜单,保存进度菜单,网络选择菜单,系统设置菜单,任务提示菜单,结束菜单,错误处理对话框等。 aS [[
AL
有关内容请详见Interfac.h, 提供基本的游戏界面接口函数。 L)JB^cxf
Interfa1.h, 处理所有的按钮Button信息。 K{ P-+(
Interfa2.h, 处理所有鼠标操作发出的消息。 ,clbD4
net_face.h, 网络部分界面。 #kC~qux^
Marco.h, 所有按钮的消息ID。 4eHSAN"$
CBprompt.h, 游戏战场中屏幕右方信息的显示。 ,sL'T[tuiU
CBAarray.h, 对游戏元素的查询。 Ce}`z
L
DDComUn.h, 针对游戏中下达命令时的命令组。 8Rj5~+5
^@^8iZ
过场动画模块 ;\RVC7
显示公司标志,制作群,历时回顾,片头,片尾和过场动画。 40kAGs>_
它有两个部分,第一是播放视频图像,第二是调用其它进程。 i6if\B
有关内容请详见CBAvi.h, 视频图像的播放。 G)7U&B
Mciapi.h, 播放AVI文件的底层函数。 60+ zoL'
VCMApi.h, 高效率的播放AVI的底层函数。 6^b)Q(Edut
CBGame.h, 播放结尾。 64/ZfXD
Interface.h, 程序状态的转换。 XJ<"S
p
\L*%?~
单挑显示模块 _w\9
\<%
武将单挑时出现的专门画面。本部分与原始设计有出入,原始设计中可以对单挑进行操作,后来删减称为只播放一段Video。有关内容请详见CBAvi.h。 6 eSo.@*l
0<m7:D
Gd
控制模块 uFWA] ":is
鼠标控制模块 d1D
f`
根据鼠标的位置设置鼠标的形状,对鼠标的操作对响应单元发出命令。
DN2 ]Y'
主要内容有:鼠标点击检测,目的地模式,命令构造,可建造区域判定,鼠标形状转换。 s>>&3jfM
鼠标点击检测,主要判断鼠标点击的位置是否在某个单元上或地形上。详见CBMouse.h。 (e7!p=D
目的地模式,主要控制鼠标选择了单元(命令主体)后,可能对单元下达的命令的模式,根据不同的命令可能需要不同的目的地类型和命令参数。详见CBCtrl.h。 d {!P
c<
命令构造,通过鼠标的点击选择或按下某个,命令按钮,构造出具有命令主体,命令ID和命令客体的命令,放到命令队列中,供执行。详见CBCtrl.h。 , /.@([C
可建造区域判定,在建造建筑时需要判断哪里可以建造哪里不可以, 并且显示出来。详见CBBuild.h。 T~]~'+<Pi
鼠标形状转换,鼠标移动到某个界面或某个单元上时,或处于命令构造阶段时,需要对鼠标的形状做一定的改变,以显示当前的操作状态。详见CBMouse.h。 {xTq5`&gT
%>
XsKXj
命令处理模块 |*{*tW C1
根据鼠标和人工智能发出的命令,传送给对象单元,并将其转化成为单元的相应状态序列。 $nE{%?n-#
根据单元状态,判断单元状况,更改单元的状态。 =0cTct6\
详见CBCtrl.h, 命令的构造保存和传递。 OR@
67Y
CBRun.h, 命令的执行。 9kD#'BxC
CBRDelay.h, 命令执行时需要的一些变量。 agUdI_'~@9
CPJ<A,V
攻击计算模块 S\:^#Yi`
根据敌我双方的攻防力量,计谋的实施和阵型计算每一次打击敌人生命的损失。 [K4cxqlfk
详见CBRun.h, 攻击计算。 bgzd($)u
CBZhenFa.h, 阵法计算。 y<Koc>8
KtQs uL%
行军控制模块 IO\1nB$0nb
根据单元的位置,速度,目的地和地形数据,计算行军路线,设置单元的下一步。详见March_n.h。 KTm^}')C8
Cv,WG]E7(
网络控制模块 >eGg 1
游戏数据在网络上的传递,纠错。模拟机的建立和管理。详见Network.h。 bbC@
1TZ[i
策略模块 zb0NqIN:
君主策略模块 u2#q7}
计算机一方根据战场双方的力量对比和战斗模式计算对单元的生产,对敌人的攻击,产生命令。 ud/!@WG
详见TEmperor.h。 v<1@"9EH
84(Jo_9
将领策略模块 .V;,6Vq
每一支部队根据将领的属性,士兵的状况调整战斗的方式或判断撤退。 HkD.W6A3
详见TGeneral.h。 MRpMmu
+
f6LG 0q
本能策略模块 9~UR(Ts}l
士兵单元面对周围的情况产生固定的基本反应。 j
e\!0{
详见Tai.h。 pf8'xdExH)
Tbnbase.h, 人工智能中需要的数据结构。 [E9iuym
CBEyes.h, 人工智能与游戏主体结构之间的接口。 _`?0w#>0
:qo[@ x{
文件模块 tiZH;t';<
资料数据文件模块 =IL\T8y09
地形和单元图素的图像文件,相应控制数据文件。单元的各项属性数据文件,操作用数据文件,界面位置数据文件。 L0![SE>
详见CBData.h, 游戏中使用的单元属性全局数据结构。 [Hx}#Kds
CBMap.h, 游戏中使用的地图图素,单元图素全局数据结构。 !RKuEg4hQ
CBGame.h, 游戏中使用的单元全局数据结构。 3/RwCtc
gT8(LDJ
存储数据文件模块 )q<VZ|V
存盘用数据,记录战场上的所有单元的状态和思考数据。 WM+8<|)n
详见CBGame.h, 存盘。 s\d3u`G
<f7 O3 >
其它模块 I=L["]
0ca0-vY
文件封装模块 mlByE,S2E
为减少程序使用的文件的数量,增加程序的安全性,将大量的图像文件和数据文件封装起来,供程序调用。 $oW=N
详见L_Allbmp.h, L_Image.h, L_Save.h, L_Scan.h, L_Text.h, Tools.h。 *B&P[n
:%gc Sm
声音模块 ':4ny]F
背景音乐和音效。有混音和音量控制。 4u5j
7`O
详见DsWave.h, 播放WAV文件。 ]O|>nTa
aqSOC(jU
文字模块 oRbWqN`F.
在非中文系统下显示汉字,日文,繁体汉字和韩文。详见puthz.h。 g]f<k2
29:2Xu i
地图编辑器 sPK ]:iC
为使美术人员方便快捷规范地制做战场地图,提供专门的地图编辑器。同时为整个游戏的文件系统,显示系统做技术上的准备。 1sXCu|\q
这是一个单独运行的程序,详见Mapedit。exe。 U.TZd"
f,ro1Nke
安装程序 I[%IW4jJ
将游戏安装和卸载。详见Setup。exe。 EP38Ho=[
O8Mypv/C
2.3 游戏引擎
m}yu4
说到游戏的引擎,很多人都不知道它是什么,以为制作它有多么困难。引擎的概念也很混乱,至少现在我还不知道它的确切定义。但我想如果一个东西要被称作引擎,它应该具有这样一些特点: QbdXt%gZe
它应该是由函数组成的。 dg|+?M^9`
它应该实现某项具体功能。 +Ug &
它应该是完整的。 x;[)#>.'
它应该可以被重新使用。 :3M,]W]
|co#X8J
从上面的要求可以看出,其实这就是作为底层程序的要求。我想没有必要把引擎认为是游戏的现成编写工具,只要2改一下美术就是另一个游戏了。只要这些程序代码将会被我们应用在以后的游戏中,我们就已经节约了很多的时间和精力。 %/2
` u
下面我会说一下在<赤壁>的代码里,哪些将被看作我们的引擎。实际上,这些部分经过一些修改后正在被我们应用到新的产品中。 `*U@d%a
e,OXn gC
显示底层: gNr4oOR{
这是一套包裹在DirectDraw外面的函数。为了简化在调用DirectDraw函数时的复杂度我们使用了一些缺省参数和内部错误处理函数。建立了一个CDDSurface类库,使得对位图的使用更加简单。详见DDApi.h Jz''UJY/O
在DDCompo.h中我们有关于游戏鼠标的一套操作。在屏幕独占模式中,Windows标准鼠标有时显示会不正常。于是我们自己制作了鼠标的显示方法。方法很简单,在每帧读取鼠标的位置,然后在该位置上显示一张位图。
7T[L5-g
在赤壁里面,我们没有使用双缓冲区的模式,而是只更新某个特定的区域。它的优点是当需要更新哪里的时侯就更新哪里,对于哪些在每帧中都只有小面积图像需要更新时是非常高效的。比如在486上,<赤壁>的主游戏界面里的鼠标移动仍然是十分流畅的。可惜的是,在<赤壁>的战场部分,它并没有优势,因为基本上是需要全屏刷新的。 OXLB{|hH80
在未来的游戏制作中,因为计算机的速度越来越快,所以我们当时所使用的模式恐怕变得不太适用,双缓冲区模式应该是主流方向。 2]fTDKh
t M5(&cQ!d
多媒体底层: z
4}"oQk:r
主要包括声音和视频。我们使用了MCI设备来播放AVI,WAV,MIDI,CD AUDIO等内容。那曾经是我们在做上一个游戏的时侯完成的部分。但是它有很多缺点,比如不能同时播放多个WAV文件,这对于我们制作游戏音效是很重要的内容。 *$7^.eHfdd
所以我们又使用DirectSound来播放声音。这里的难点在于当我们需要播放很长的文件时,不能一次读入,而需要建立新的线程按时装入声音。好在现在大部分游戏都使用CD Audio作为背景音乐,不需要WAV。 %ZRv+}z
Z*Ffdh>*:&
界面底层: :+YHj)mN
基于显示底层之上的界面元素其实并不好做。因为我们总希望它的响应方式与Windows95中相同。而大家在<赤壁>里看到的内容就与Windows95有些不一样。比如滚动条(ScrollBar)对鼠标的响应就非常简单,按钮(Button)的反应也有所不同。但是好在它比较简单,易于使用。 TD\TVK3P
-,
+o*BP
在每做完一个游戏之后,我们都习惯要把某些东西整理一下,看看它是否可以在以后被使用起来。而往往这些东西也都是需要不断修改的。因为程序运行的平台不一样了,它的用途也不一样了,而我们的编程水平也不一样了。但总之这些代码被较为完整地保留了下来,它必将是我们今后编程的基石。 Yh]a4l0
bAt!S
2.4 关键讨论 ta&z lZt
iB0r+IbR
我刚开始编写游戏的时侯总有一个想法,只要游戏的主要部分写完了,游戏也就差不多了。我也遇到过一些游戏制作组的成员,他们也大都是这样的想法,认为只要把游戏的演示版拿给别人一看,然后只要再投资让美工画一些画,游戏就可以做完了。其实事情并不想想象中那样简单。 U,b80%k:
在我看来,把游戏的大概样子做完了,顶多占整个游戏的三分之一。另外的两个三分之一分别是整个游戏的制作和测试。 vT5GUO{5
举个简单的例子,比如我们在演示版中通常只有一个兵种和一个战场。游戏的显示效果可能很不错。但是,真正在游戏中不会只有一个兵种的,每方都会有大概十种兵,又会有三四方的敌人,这时侯你的显示底层是否能够胜任呢?内存是否会占用太多呢?这时侯还需要我们对其进行优化和修改。连游戏的底层显示部分都可能需要修改,更何况游戏中还有更多的内容呢? b$2=w^*
z\v
下面我举一些<赤壁>中的例子,这些都可能是极小的问题,但都是我们需要仔细考虑的问题。在你准备开始制作一个即时战略游戏之前,你是否曾经考虑过这些问题呢?假如你对这些问题有所了解,那么你就应该可以非常有把握地马上开始制作游戏。如果没有,也没有关系,因为这些问题我也没有全都事先考虑过。 xDe^>(,"
假如你有时间,可以对你自己的游戏多多考虑一下,这个游戏距离一个真正的产品,到底还缺什么?还有哪些模块和部分没有做完?当你对两者之间的差距有了一个明确的认识后,也就不会担心了。任何东西都是一点一点做出来的,只要按照你想做的内容去做就可以了。 dtuCA"D
y6am(ugE
程序状态的转换 <w&'E6mU
我们在写DirectX程序的时侯,总有一种偏见,那就是不希望Windows界面出现在我们的游戏里。于是什么都需要我们自己做。比如说窗口。因为窗口的刷新需要我们自己管理,就觉得没有必要生成多个窗口了。这样所有的窗口消息就必须在唯一的一个函数里实现。可是我们的游戏里有很多种不同的操作,比如界面,系统菜单,播放视频等,这些内容就都必须在这个地方处理。所以我们就引入了程序状态这个概念。我们定义了一系列的状态,在每个状态里,有固定的操作和响应,状态之间的转换也在特定的时侯进行。这样我们就很容易把一些关系不大的内容独立开来,降低程序的复杂性。 O4-#)#-)S~
其实我们在实现这一部分的时侯是很混乱的。你很难在代码中找到所有状态转换的地方。但是它的实现很简单,一般的规则就是程序的对称性。有专门的装入函数和释放函数,然后有显示函数,计算函数,鼠标消息响应函数,热键响应函数。在内部,需要结束本状态时就发出一个状态转换的消息。在外部,只要在主程序的主循环和消息响应函数处针对不同的状态执行它们不同的函数就可以了。 EW]DzL3
这里的关键在于状态的转换。因为状态在转换中一定会释放和申请大量内存,如果有的内存没有释放,转换次数一多就会出现问题。如果我们把状态转换的地方写好了,程序看起来也非常干净整齐,Bug也会比较少。 =)y$&Y