目录 CKU)wJ5t
+>%AG&Pc
1 游戏程序理论 s:tWEgZk?
1.1 技术基础 vgr5j
1.2 游戏底层 !__^M3S,k
1.3 编写规则 P rv=f@
1.4 程序设计 }MM:q R
1.5 制作流程 4k6:
1.6 程序调式 UK[+I]I
p
1.7 代码优化 +]Z*_?j9{
8yuTT^
owO&[D/
1 游戏程序理论 mt-t8~A
我做游戏的历史只有三年,我所写的内容都只是我在此期间的感觉和经验,还远远谈不上完整和正确,甚至有些内容我们自己也没有完全达到,我只是试图说明我们曾经是怎样做的和在最近的将来打算怎样做。 mVT[:a3
我读过一些关于如何制作游戏的文章和书籍,有外国的,也有国内的。有的过于难懂(那些专业技术有的我根本看不懂),有的过于简单,而且大都着重于程序编写技术。我在这里希望能够提供其它方面的帮助,比如游戏中的程序设计,项目管理和与我们中国人编写游戏相关联的一些问题。 P>|sCF
我希望能够给那些想编写商业游戏和正在编写商业游戏的人们提供一个样本,这不是最好的样本,而是在中国内地游戏制作业的初级阶段中一个普通产品的制作方法和经验。如果是一个初学者,我希望给他一个编写游戏程序的完整印象,让他至少了解编游戏和玩游戏是有区别的。 O@LUM{\
我希望阅读本文的人应该是一个程序员,熟练使用C语言和C++语言,曾经编写的单个程序长度应该在5000行(150k)以上。因为我在这里不会介绍任何有关编程语言的语法,编译等内容,如果你连这些内容都还没有弄清楚,请先学习一下,否则将很难与本文引起共鸣。假如你是一位美术设计,游戏策划人员或游戏项目管理人员,虽然你无法看懂里面的大部分内容,但是我也希望你能够了解里面的有关内容,因为如果你能够了解程序员的工作,我们就能够更好地配合。 K-@cn*6
我更希望那些不了解游戏的人们能够看到本文,我希望他们能够了解编写游戏程序与编写其它程序是一样艰巨和复杂的,制作游戏并不等同于玩游戏。 )_Iu7b
gO]8hLT
1.1 技术基础
3BB/u%N}
L 1q]
1.1.1 人员 >R\!Qk
作一个商业游戏所需要的程序员的水平,可以分成三个级别:系统分析员、主模块程序员和外围程序员。 !zfV(&
·系统分析员: 7TZ,bD_
这里的系统分析员也许比不上程序员资格考试中的系统分析员,因为我们制作的游戏程序到目前为止都属于中小型程序,长度一般在50000到200000行之间(1.5M到6M),经过压缩之后源代码可以轻松放在一张软盘上。工作量一般在20到60人月之间。所以游戏程序的复杂程度比一些大型应用程序要小。但是,游戏系统分析员的工作也一样是复杂繁重的。他的主要工作是需求分析,程序设计和进度管理。 o;pJjC]
在需求分析阶段,主要是根据游戏设计制定的游戏方案确定那些内容是可以实现的,那些是不能实现的;可以实现的部分如何实现,不能实现的部分该如何修改。最后制定一个可行的游戏程序制作方案。这个方案最后被系统分析员写在程序设计文档中。当设计定稿后,制定开发计划,组织人力开发,监督制作成果。 #}UI
·主模块程序员: `3dGn.M
过去我们的系统分析员和主模块程序员由同一人担任,但是到后来,我们发现,因为程序的管理设计人员也要担任繁重的程序制作任务,很难面面俱到。所以我建议把主模块程序员独立出来。由他来负责对游戏主要运行部分进行编程。这需要主模块程序员具有非常丰富的编程经验,至少应该具有完成一个游戏的经验。但是其实每个人都是从不会到会,从没有经验到经验丰富的,不能满足条件也没有关系,只要程序难度小一些就可以了。 }9+Vf'u|l
ZP.~Y;Ch;-
·外围程序员: \T;(k?28HN
游戏的程序一般都会被分割程序若干个模块,如果该模块独立性比较大就可以交给其他人员完成。这些人只要有一定编程经验就可以胜任了。 j7VaaA
!1bATO:x
1.1.2 编程平台 W{kTM4
任何的计算机语言都可以被用来写游戏。游戏可以被运行在任何可能的计算机硬件设备上。但是,因为我的制作经验的局限,我无法向你们介绍用Cobal语言编写AppleII上运行的游戏。同样,我也不会教各位如何使用Visual Basic,Pascal或Java,任何形式的DOS程序也是我不会花时间讲解的。在我后面的讨论中将只涉及在windows95/98下的Visual C++编程。 X>MDX.Z
现在,编写游戏时不能不提到Microsoft公司的DirectX。它是一套基于Windows95/NT的游戏底层接口,在我们的游戏中使用了它作为底层接口。关于它的使用想必大家已经很熟悉了,现在已经有中文的书籍出版,我不必赘述。 _wZr`E)
有的书上讲在编译和调试程序时最好使用Windows NT或双显示器,尤其因为DirectDraw中独占显示器。可是我试过之后发现都不实用。对于WindowsNT,它的4.0版只支持DirectX 3.0, 听说5.0版才支持DirectX 6.0,但我总不能等到NT5.0出了之后才开始编吧。对于双显示器,我一直耿耿于怀,外国人都很阔,每个人可以配备两台机器,可是,就算我找到两台计算机,程序装入的速度实在让我不耐烦,调试一遍时间太长,等不起。 O+~@S~
我们应该重视的倒应该是计算机的兼容性问题。为我们提供测试的计算机是很少的,如何找到更多的条件才测试我们的程序是后期应该注意的问题,千万不要以为在Windows95时代计算机硬件的差别已经没有了,它们出起错来更危险。 ;^8X(R
jZqCM{
1.1.3 语言 Ja%isIdh
在任何一本写游戏编程的外文书籍里都会提到编程语言主要有两种,汇编和C。有的书里甚至不提倡使用C++。不使用C++的原因我猜有两个:第一,速度。据说C++代码比C代码慢10%。这个数字我没有测试过,但C++比C慢大概应该是对的。因为用C++比直接用C编写代码要方便和简单,所以速度应该会慢。第二,便于移植,移植这个词可很时髦,什么PC到PS啦,PS到Saturn啦,Saturn到N64啦,N64到Mac啦,Mac到PC啦之类,一个游戏必须要在所有地方出现才过瘾。如果使用C++就难了,因为象次世代游戏机上的开发环境,一般不支持C++,N64虽然说是支持但仍然建议使用C。 _^F%$K6
使用汇编的理由也是很简单的,那就是快!因为游戏编程中表现最明显的地方就是游戏的速度。汇编比C快的道理也是很明显的,那就是汇编要比C难写多了,速度自然快些。 v0%FG9Gk
$ Vsf?ID
那么速度就那么重要么?过去上学的时候,用汇编写出来的程序编译出来的一般十几k,后来用C写的小程序大的有几十k,而现在编译windows程序,一上来就有一百多k。过去的游戏就是绿色屏幕上几个字符,而现在都是真彩色的图象和实时三维。过去的游戏一个人就可以“搞定”,而现在必须分工合作。这些给我的感觉就是游戏越做越大,程序越写越长。是不是有一些方面开始变得与游戏速度一样甚至更加重要了呢? p4Xhs@.k
有!这其实就是大家经常谈论到的一个问题:程序的Bug。一个程序如果越来越复杂,越来越长,而且多个人共同编写,这时候就越容易出现程序中的臭虫。我认为,以我们目前的程序编写水平而言,程序的正确性要远远高于它的效率。实际上,我在后面的主要篇幅都在讲如何提高程序的正确性上,而只有很少的篇幅介绍程序的优化。 "s\himoa
所以我选择编程语言主要是从其正确性和效率两个方面来考虑的。目前我选择C++。 XeAH.i<
Qgl5Jr.
这不仅仅因为我对这个语言最熟悉,更因为它的特点,对两个方面都符合得很好。程序是人写出来的,如果程序越容易写,就越容易正确,这方面汇编语言显然不是候选者,Pascal虽然好,但是那些Begin和End写起来也太累。 C语言要省事得多,C++更进一步,而且有了封装,继承,多态的概念。可是编写最方便的首选应该是Visual Basic,Delphi和Java了。程序的框架都已经被准备好,只要在里面填东西就可以了。只是因为它们的效率比较低,而不能成为游戏编写的最佳语言。 I"WmDC`1
现在很难想象一个游戏整个都是用汇编写的,因为一个10000行的C程序改用汇编写至少需要50000行,那其中出错的机会至少要多出5倍,而且汇编代码太过抽象,很难读懂。所以应该只在非常需要效率的地方才需要使用汇编语言。 ban;HGGNG{
C语言与C++的差别不大,而且我认为C++所带来的封装的概念足以弥补它们的速度差异。关于移植,有消息说Sega最新一代的游戏机DreamCast要使用Windows CE作为操作系统,那么C++应该也会支持的了。大融合的时代总会到来,不同担心。我的C语言代码也主要用于程序的底层。 sg_%=;
那么,现在大家对我所使用的语言应该有所了解了。使用C++作为主要语言,同时辅助以C和汇编。 #-g2p?+i&
m$$sNPnT
1.1.4 编译环境 v+d? #^
Microsoft Visual C++2.0/4.0/5.0/6.0是我一直使用的编译器。虽然Borland C++和Watcom C++也可以编译Windows程序,我仍然认为Visual C++是最好的。没有别的理由,就是因为习惯了。如果你已经习惯使用Watcom C或Free C什么的,也没必要改。如果从来没有编过Windows程序,倒是可以先从这里开始。 zY&/lWW._
我听说谁如果不会使用SoftICE调试程序就没有达到编程的顶峰,我一直以此为遗憾。我不会使用SoftICE。我也远远没有达到编程的顶峰。“竟然这样的人也编起游戏来了”,也许有的人会这样想,我想这没关系,因为本来谁都可以编游戏的嘛。 TnPx.mwK\
} DkdF
1.1.5 背景知识 ^<Sy{KY
编程序到底需不需要是专业人才,这也是个大家争论的话题。我还是那个观点,这只是一个起点的问题,它只表明入门时候的速度而已。但这并不是说你什么都不需要学。有许多东西需要我们学,甚至需要我们不停地学。 +`.,6TNVlY
·硬件知识: E, |OMK#
你总不能连自己的计算机是什么CPU都不知道吧。虽然我们不需要知道每个芯片的内部构造,但如果我们知道了Pentium CPU有两条并行流水线同时处理指令的话,我们就会特别注意这方面的优化了。 )oMMDHw\
·操作系统: VCUsvhI
我们的程序不一定需要多任务,多线程,但是了解我们的程序何时被调用是很重要的。 q>VvXUyK,
·数学: PtOYlZTe?
数学是我最头痛的科目了。好在我们在这里所接触的大多是离散数学(我也很头痛!)。最基本的要算是数理逻辑,就是程序中最普通的判断语句中的那些“与”,“或”,“非”。其次是那些图论,概率。知道这些编写二维游戏就足够了。如果编写三维游戏,那就赶快复习一下线性代数吧。 eg>]{`WQ
·专业课: C33=<r[;N<
这是计算机系中学到的有关课程。虽然数据库,网络的概念在游戏编写中都可能会用到,但是因为都是间接的,就不在这里说了。数据结构大概是最重要的课程了,这里所提到的数组,队列,堆栈,链表,表,树,图等概念在我们的程序中几乎都会或多或少地用到。另外,对于三维编程,还有一门叫计算机图形学,也是很重要的。 DA/\[w?J
·文件: U~<~>^[
与游戏密切相关的文件有图象文件,声音文件,视频文件等。这些知识越丰富对我们编写游戏就越有用。可是,这也并不是绝对的,我在刚开始写游戏时连BMP图的格式都不知道。 >]?!9@#IH
·美术: ?"T *{8
大概每个游戏程序员都有过当“助美”(助理美工)的体验吧。当我们做程序测试,或美术师们没有时间完成裁图,切图等“体力”劳动时,自然由我们来做这些工作。但是,请记住,程序员永远是程序员,就算我们画得再好,也比不过他们的。 S6c>D&Q
IjRUL/\=
1.2 游戏底层 N-Sjd%Z
在我们编写每一个游戏时,都希望能有一部分代码可以重用,这样我们才能有更多的时间做更多的事情。这些重用的代码就慢慢形成了游戏底层。游戏底层多了,自然就形成了一些类别。 1K{hj%
显示底层: 6b h.5|
用于基本显示的函数。把常用的功能制作成函数,统一出错管理。对于Windows95/98下的游戏编程,我们一般使用DirectX作为大部分底层的底层。因为这部分是我们可以用于编程的最底层函数。对于二维显示,我们使用的是DirectDraw部分。为什么还需要在DirectDraw外面再包一层函数呢?原因有三:第一,DirectDraw提供的函数扩充性很强,函数参数较多,而我们使用时参数比较固定,包裹成类库后,参数更加可以封装起来;第二,DirectDraw提供的函数比较分散,而有许多操作是需要多个函数按照一定顺序执行的,包装起来后编写程序比较简单;第三,DirectDraw函数的返回值比较复杂,而且没有对错误进行系统的处理,我们需要自己制作一套错误处理函数,对错误代码进行解释。 >F;yfv;
显示底层有许多种做法,可以完全自己开发,也可以直接使用别人完成的函数库,比如Internet上比较流行的CDX类库就是对DirectDraw进行了很好封装的底层。 v`~egE17
显示底层主要分成下面几个部分: qk!,:T
窗口处理:窗口的创建、管理、删除。 -W)8Z.
图形接口处理:图形设备的初始化、退出、显示模式的设定、主显示面、后缓冲区的建立。 Hr=?_Un"
二维图形处理:游戏使用的二维图象的装入、显示、剪裁、逻辑操作。 ZrDr/Q~
外围工具:游戏使用的鼠标,刷新率计算和显示,显示内存斟测,面丢失(Surface Lost)处理(在DirectX 6.0后据说可以不用),错误处理,调试函数。 kC0^2./p
关于显示底层还有两个问题需要说明: ^xF-IA#ZeB
第一,显示部分的调试。因为我们所做的游戏一般为独占模式,在游戏运行时不能跟踪(除非使用双显示器),所以我们只能另想办法,一个比较常用的办法是使用窗口模式和全屏模式切换。不一定在游戏运行中随时切换显示,在调试时事先指定也可以实现该功能。 ?xzDz
第二,多媒体播放底层: ;C+
_K S
播放多种声音,音乐,视频等多媒体效果。比较古老的一种办法是使用Windows 3.1就开始使用的MCI调用。它是一种通用接口,使用不同的参数调用同一个函数就可以实现基本相同的功能。用MCI可以实现的主要媒体类型有MIDI,WAVE,CDAUDIO,和VIDEO。它的优点是使用简单。缺点则是缺乏交互性,而且同种类型的媒体不能同时播放。这样就很难实现多种音效的混合。 :74G5U8%
一个基于DirectX的调用是使用DirectSound。它是基于WAVE文件的一套播放函数。非常适合混音。缺点就是使用较复杂,而且如果想实现分段读取较长的WAVE文件,编程有些难度。 Wvwjj~HP2}
另外一个针对MIDI的DirectX是DirectMusic。它是DirectX系列中的一个新成员。 biAa&
对于播放高质量的VIDEO(视频)文件,则有另外一套底层。比如Microsoft的ActiveMovie和Intel的Indeo Video。有了它们我们就可以放心地全屏播放AVI文件了。 qUG)+~g`
对于这些底层,我们所做的只是简单地封装上一层函数,使我们在使用该功能时达到最简便。 6G?7>M
但是假如你对这些现成的播放程序不满意,也可以完全自己开发。只要你知道这些文件的格式,再把它们存储成你需要的格式,用自己更加优化的算法将它显示出来就可以了。用这种方式你可以自己播放AVI VIDEO, MPEG, MPEG2或FLI/FLC。 QZ_8r#2x
\.{ZgL5"
文件封装底层: + :k"{I
对使用的文本,图象等数据进行封装。我们也常用这种方式把多媒体文件封装起来,防止别人事先查看。这种工作往往是循序渐进的,我们会根据自己的时间和实力,封装相应的部分。比如先封装比较简单的文本,图象。然后是较为复杂的声音文件,最后是最复杂的视频和动画文件。 Xa&0j&AH
有的时候封装也是为了压缩。不仅再安装时少占用硬盘,装入时也可以节省时间,有时在运行时也会减少内存占用而不会对速度有太大的影响。 ]0myoWpi3
根据文件类型的不同,我们可以这样分类:文本、图象、声音、视频、动画等。对于每种类型,我们还必须有打包函数和解包函数之分。 &^])iG,Ew
ddvtBAX
界面底层: 4XArpKA
自己编制的类Windows风格的界面类库。我总有这样的习惯,希望在我的游戏里没有一点Windows的风格,于是我不会使用任何一点的有Windows风格的界面(游戏工具除外)。所以游戏的界面必须我们自己做。然而我又使用过MFC那一套界面类库函数,很佩服Microsoft的本事。也希望我们的游戏中有一套自己的界面系统。这套系统将会包含Windows用到的主要界面元素:位图(Bitmap)、按钮(Button)、检查框(Checkbox)、滚动条(Scrollbar)、滑块(Slider)等等。 F!ra$5u
虽然Windows允许我们在使用标准界面时自己画图,但是因为它只支持256色,是基于消息响应机制的显示,而且无法支持位图的封装,所以我们使用一套完全由自己制作的界面底层。这套底层建立在显示底层和文件封装底层之上,形成了一套比较完整的系统。 fBct%M 3
RRR=R]
上面这些只是一般游戏所需要的底层,对于某些特殊类型的游戏,可能还会有战略游戏的显示,操作底层,RPG游戏的事件处理底层,战棋游戏的显示,智能底层,动作游戏的场景底层等。如果编写三维程序,则还会有三维显示和操作底层。这些底层都不需要也不可能在一开始就全部完成,而必然是在不断的修改和检验中完善的。它们也不可能一成不变,必然随着我们编程经验的积累和开发环境的改变而改变和完善。 9I*`~il>{
每个编写游戏的程序员都会慢慢积累出符合自己游戏特点的底层函数库,这些函数库如果得到不断的发展和扩展将会越来越大,越来越好,这对于每个游戏制作公司和集体来讲都是一笔不可忽视的财富。 =}YaV@g<f
而这套函数库越来越复杂,再加上辅助的一整套工具,就可以被称之为游戏的引擎。有了引擎在手之后,不仅意味着我们有了一套低Bug的现成代码,程序员的工作量将会大幅度下降,而且他们可以把更多的精力放在游戏的优化、游戏的人工智能、游戏的操作、以及游戏性上面了。到了这个层次,我才可以想象一个好的游戏将会是什么样子。 t3;QF
lxOUV? m^N
1.3 编写规则 f5hf<R),A
现在,只需要一个程序员编写的程序已经很少了,大部分的程序都需要多个程序员的协作才能完成。这期间必然需要程序员之间互相阅读代码。而代码的编写规则就是非常重要的环节。一段代码如果写的浅显易懂,不但在程序交流时非常方便,而且对于寻找程序错误也时很有帮助的。程序员也是人,是人就会犯错误,不管他多么地不愿意犯这个错误。所以我们也希望能制定下一个规则,使他们在一开始想犯错误也不行。于是就出现了程序的编写规则。 .jbT+hhM
如果不按照这些规则编写代码编译程序是不会报出错误的。但是如果按照规则编写,则对我们的思路的整理、笔误的减少、错误的查找和代码的阅读都是极有好处的。 ~X1<x4P\
下面我只把规则中几个重要的部分加以简单的描述,在后面附录中会有一份我们已经使用了一段时间的完整规则。 %51HJB}C]
8DZ
OPA
命名规则: 2B=+p83<
程序中对变量和函数的命名看似简单,可是名字起得好与不好有很大的不同。对于变量我们一般采用匈牙利命名法和微软命名法的结合。对于函数我们一般要把模块名写在函数名的开始。对于类库我们一般要求类的成员和成员函数要紧密对应。 ?F@X>zR2
@ R;o $n
变量使用: r*W&SU9Z
对于程序中所使用的所有变量,我们都需要其有明显的定义域。对于全局的变量要尽量封装。对于类库,所有成员必须封装。 SI/p8 ^
Y .\<P*iO
函数使用: Pxe7 \e
我们对函数的参数和返回值做了要求。对于函数的使用范围也做了规定,要求范围越小越好。 hZeF? G)L'
%scQP{%aD
注释: >Ms_bfSK
对于程序注释,我们虽然不需要很多,但是在函数声明、变量声明、主要算法部分、源程序文件的开始都需要有注释。 _}:#T8h
~`o%Y"p%rv
调试: aq%i:};
我们的底层和Visual C++都提供了一些供调试用的代码,在我们的代码中需要适时地使用这些代码,以提高程序的纠错机会。 F=~LVaF/_
qre(3,VE5
这里有几个问题需要特别说明: !jyy`q=
第一,关于链表的使用。 k= oCpXq^
我在编程中所遇到的所有死机错误中,有90%来自于指针,9%来自于字符串(其实也是指针的一种),1%来自死循环。而链表这种把指针的优势发挥到极限的方式我是不提倡使用的。有人讲如果不会使用链表就不是C程序员,那么现在我连个合格的程序员的资格都够不上了。的确,对许多人来讲,编写没有Bug的链表是很容易的事情,甚至对某些人而言,一个没有链表存在的程序几乎不能称得上是商业程序。我承认有许多天才的程序员在 $1;@@LSw
自己的链表程序中表现得十分出色,他的程序效率既高又没有Bug,但是我也知道还有更多的程序员并不是天才。就算是天才的程序员如果在他的十万行代码中有五万行于某些链表有关,这其中的某个指针出了一个错误,而这个错误又不是每运行一次都出现的时候,他又有多大的把握很迅速地找到这个错误呢?链表的复杂性在于它的地址的随机性,这使我们在调试跟踪时非常困难,我们很难随时观察每一个节点的信息,尤其当节点多的时候。所以我把链表的使用完全限制在算法的需要和该代码被某个独立模块完全封装的情况下。 u_X(c'aE;
实际上我在设计程序时,要求尽量减少指针在主要模块之间的传递,而使用效率较低但是不易出错的句柄ID的方式。 PgwNE wG
OiMr,
第二,关于类库的成员。 ,(&