目录 Q[+&n*
p w(eWP
1 游戏程序理论 UZ#Yd|'PD
1.1 技术基础 Jv(9w[
1.2 游戏底层 *;b.x"
1.3 编写规则 xrb %-vT
1.4 程序设计 8G@I e
1.5 制作流程 ;T6{J[
h
1.6 程序调式 [^sv.
1.7 代码优化 ?`T<
sk8c
`D)ay
k=">2!O/
1 游戏程序理论 1|/P[!u
我做游戏的历史只有三年,我所写的内容都只是我在此期间的感觉和经验,还远远谈不上完整和正确,甚至有些内容我们自己也没有完全达到,我只是试图说明我们曾经是怎样做的和在最近的将来打算怎样做。 ]gI>ay"\QA
我读过一些关于如何制作游戏的文章和书籍,有外国的,也有国内的。有的过于难懂(那些专业技术有的我根本看不懂),有的过于简单,而且大都着重于程序编写技术。我在这里希望能够提供其它方面的帮助,比如游戏中的程序设计,项目管理和与我们中国人编写游戏相关联的一些问题。 pt[H5
我希望能够给那些想编写商业游戏和正在编写商业游戏的人们提供一个样本,这不是最好的样本,而是在中国内地游戏制作业的初级阶段中一个普通产品的制作方法和经验。如果是一个初学者,我希望给他一个编写游戏程序的完整印象,让他至少了解编游戏和玩游戏是有区别的。 R(_UR)G0 @
我希望阅读本文的人应该是一个程序员,熟练使用C语言和C++语言,曾经编写的单个程序长度应该在5000行(150k)以上。因为我在这里不会介绍任何有关编程语言的语法,编译等内容,如果你连这些内容都还没有弄清楚,请先学习一下,否则将很难与本文引起共鸣。假如你是一位美术设计,游戏策划人员或游戏项目管理人员,虽然你无法看懂里面的大部分内容,但是我也希望你能够了解里面的有关内容,因为如果你能够了解程序员的工作,我们就能够更好地配合。 =xzDpn>f
我更希望那些不了解游戏的人们能够看到本文,我希望他们能够了解编写游戏程序与编写其它程序是一样艰巨和复杂的,制作游戏并不等同于玩游戏。 -XNjyXm2
.PjJ g^^
1.1 技术基础 c|?0iN
`\!oY;jk
1.1.1 人员 IRy!8A=X
作一个商业游戏所需要的程序员的水平,可以分成三个级别:系统分析员、主模块程序员和外围程序员。 L,G{ t^j
·系统分析员: /HCd52
这里的系统分析员也许比不上程序员资格考试中的系统分析员,因为我们制作的游戏程序到目前为止都属于中小型程序,长度一般在50000到200000行之间(1.5M到6M),经过压缩之后源代码可以轻松放在一张软盘上。工作量一般在20到60人月之间。所以游戏程序的复杂程度比一些大型应用程序要小。但是,游戏系统分析员的工作也一样是复杂繁重的。他的主要工作是需求分析,程序设计和进度管理。 A0Z<1|6r*
在需求分析阶段,主要是根据游戏设计制定的游戏方案确定那些内容是可以实现的,那些是不能实现的;可以实现的部分如何实现,不能实现的部分该如何修改。最后制定一个可行的游戏程序制作方案。这个方案最后被系统分析员写在程序设计文档中。当设计定稿后,制定开发计划,组织人力开发,监督制作成果。 "]*16t%Z%x
·主模块程序员: m2&"}bI{
过去我们的系统分析员和主模块程序员由同一人担任,但是到后来,我们发现,因为程序的管理设计人员也要担任繁重的程序制作任务,很难面面俱到。所以我建议把主模块程序员独立出来。由他来负责对游戏主要运行部分进行编程。这需要主模块程序员具有非常丰富的编程经验,至少应该具有完成一个游戏的经验。但是其实每个人都是从不会到会,从没有经验到经验丰富的,不能满足条件也没有关系,只要程序难度小一些就可以了。 022nn-~
l-|hvv5g
·外围程序员: ia=eFWt.
游戏的程序一般都会被分割程序若干个模块,如果该模块独立性比较大就可以交给其他人员完成。这些人只要有一定编程经验就可以胜任了。 %g1{nGah
AL*P2\8
1.1.2 编程平台 I{>U 7i
5
任何的计算机语言都可以被用来写游戏。游戏可以被运行在任何可能的计算机硬件设备上。但是,因为我的制作经验的局限,我无法向你们介绍用Cobal语言编写AppleII上运行的游戏。同样,我也不会教各位如何使用Visual Basic,Pascal或Java,任何形式的DOS程序也是我不会花时间讲解的。在我后面的讨论中将只涉及在windows95/98下的Visual C++编程。 (Ic{C5'
现在,编写游戏时不能不提到Microsoft公司的DirectX。它是一套基于Windows95/NT的游戏底层接口,在我们的游戏中使用了它作为底层接口。关于它的使用想必大家已经很熟悉了,现在已经有中文的书籍出版,我不必赘述。 Ut"~I)S{LT
有的书上讲在编译和调试程序时最好使用Windows NT或双显示器,尤其因为DirectDraw中独占显示器。可是我试过之后发现都不实用。对于WindowsNT,它的4.0版只支持DirectX 3.0, 听说5.0版才支持DirectX 6.0,但我总不能等到NT5.0出了之后才开始编吧。对于双显示器,我一直耿耿于怀,外国人都很阔,每个人可以配备两台机器,可是,就算我找到两台计算机,程序装入的速度实在让我不耐烦,调试一遍时间太长,等不起。 $r0~&$T&
我们应该重视的倒应该是计算机的兼容性问题。为我们提供测试的计算机是很少的,如何找到更多的条件才测试我们的程序是后期应该注意的问题,千万不要以为在Windows95时代计算机硬件的差别已经没有了,它们出起错来更危险。 N$u;Q(^
0V{a{>+
1.1.3 语言 .1F(-mLd
在任何一本写游戏编程的外文书籍里都会提到编程语言主要有两种,汇编和C。有的书里甚至不提倡使用C++。不使用C++的原因我猜有两个:第一,速度。据说C++代码比C代码慢10%。这个数字我没有测试过,但C++比C慢大概应该是对的。因为用C++比直接用C编写代码要方便和简单,所以速度应该会慢。第二,便于移植,移植这个词可很时髦,什么PC到PS啦,PS到Saturn啦,Saturn到N64啦,N64到Mac啦,Mac到PC啦之类,一个游戏必须要在所有地方出现才过瘾。如果使用C++就难了,因为象次世代游戏机上的开发环境,一般不支持C++,N64虽然说是支持但仍然建议使用C。 Vf?+->-?{
使用汇编的理由也是很简单的,那就是快!因为游戏编程中表现最明显的地方就是游戏的速度。汇编比C快的道理也是很明显的,那就是汇编要比C难写多了,速度自然快些。 "{a-I=s\C
Om
#m":
那么速度就那么重要么?过去上学的时候,用汇编写出来的程序编译出来的一般十几k,后来用C写的小程序大的有几十k,而现在编译windows程序,一上来就有一百多k。过去的游戏就是绿色屏幕上几个字符,而现在都是真彩色的图象和实时三维。过去的游戏一个人就可以“搞定”,而现在必须分工合作。这些给我的感觉就是游戏越做越大,程序越写越长。是不是有一些方面开始变得与游戏速度一样甚至更加重要了呢? o#(z*v@
有!这其实就是大家经常谈论到的一个问题:程序的Bug。一个程序如果越来越复杂,越来越长,而且多个人共同编写,这时候就越容易出现程序中的臭虫。我认为,以我们目前的程序编写水平而言,程序的正确性要远远高于它的效率。实际上,我在后面的主要篇幅都在讲如何提高程序的正确性上,而只有很少的篇幅介绍程序的优化。 <?|v-(E
所以我选择编程语言主要是从其正确性和效率两个方面来考虑的。目前我选择C++。 cH$zDm1
oy+`` W~
这不仅仅因为我对这个语言最熟悉,更因为它的特点,对两个方面都符合得很好。程序是人写出来的,如果程序越容易写,就越容易正确,这方面汇编语言显然不是候选者,Pascal虽然好,但是那些Begin和End写起来也太累。 C语言要省事得多,C++更进一步,而且有了封装,继承,多态的概念。可是编写最方便的首选应该是Visual Basic,Delphi和Java了。程序的框架都已经被准备好,只要在里面填东西就可以了。只是因为它们的效率比较低,而不能成为游戏编写的最佳语言。 8&?kr/_Vr
现在很难想象一个游戏整个都是用汇编写的,因为一个10000行的C程序改用汇编写至少需要50000行,那其中出错的机会至少要多出5倍,而且汇编代码太过抽象,很难读懂。所以应该只在非常需要效率的地方才需要使用汇编语言。 q:Lw!'Zh
C语言与C++的差别不大,而且我认为C++所带来的封装的概念足以弥补它们的速度差异。关于移植,有消息说Sega最新一代的游戏机DreamCast要使用Windows CE作为操作系统,那么C++应该也会支持的了。大融合的时代总会到来,不同担心。我的C语言代码也主要用于程序的底层。 d)V"tSC,
那么,现在大家对我所使用的语言应该有所了解了。使用C++作为主要语言,同时辅助以C和汇编。 <
H1+qN=]`
s"1:#.u
1.1.4 编译环境 2Eq?^ )s
Microsoft Visual C++2.0/4.0/5.0/6.0是我一直使用的编译器。虽然Borland C++和Watcom C++也可以编译Windows程序,我仍然认为Visual C++是最好的。没有别的理由,就是因为习惯了。如果你已经习惯使用Watcom C或Free C什么的,也没必要改。如果从来没有编过Windows程序,倒是可以先从这里开始。 C'~K am S
我听说谁如果不会使用SoftICE调试程序就没有达到编程的顶峰,我一直以此为遗憾。我不会使用SoftICE。我也远远没有达到编程的顶峰。“竟然这样的人也编起游戏来了”,也许有的人会这样想,我想这没关系,因为本来谁都可以编游戏的嘛。 BI9~%dm
l!Bc0
1.1.5 背景知识 96W!~w2xx
编程序到底需不需要是专业人才,这也是个大家争论的话题。我还是那个观点,这只是一个起点的问题,它只表明入门时候的速度而已。但这并不是说你什么都不需要学。有许多东西需要我们学,甚至需要我们不停地学。 MF+J3)
·硬件知识: m~KGB"
你总不能连自己的计算机是什么CPU都不知道吧。虽然我们不需要知道每个芯片的内部构造,但如果我们知道了Pentium CPU有两条并行流水线同时处理指令的话,我们就会特别注意这方面的优化了。 P}=u8(u
·操作系统: a%3V<
"f
我们的程序不一定需要多任务,多线程,但是了解我们的程序何时被调用是很重要的。 tnx)_f
·数学: z\iz6-\&y
数学是我最头痛的科目了。好在我们在这里所接触的大多是离散数学(我也很头痛!)。最基本的要算是数理逻辑,就是程序中最普通的判断语句中的那些“与”,“或”,“非”。其次是那些图论,概率。知道这些编写二维游戏就足够了。如果编写三维游戏,那就赶快复习一下线性代数吧。 z}XmRc_Ko
·专业课: _Ju@<V$
这是计算机系中学到的有关课程。虽然数据库,网络的概念在游戏编写中都可能会用到,但是因为都是间接的,就不在这里说了。数据结构大概是最重要的课程了,这里所提到的数组,队列,堆栈,链表,表,树,图等概念在我们的程序中几乎都会或多或少地用到。另外,对于三维编程,还有一门叫计算机图形学,也是很重要的。 q:OSQ~U_
·文件: .}KY*y
与游戏密切相关的文件有图象文件,声音文件,视频文件等。这些知识越丰富对我们编写游戏就越有用。可是,这也并不是绝对的,我在刚开始写游戏时连BMP图的格式都不知道。 ce/Z[B+d
·美术: Koh`|]N
大概每个游戏程序员都有过当“助美”(助理美工)的体验吧。当我们做程序测试,或美术师们没有时间完成裁图,切图等“体力”劳动时,自然由我们来做这些工作。但是,请记住,程序员永远是程序员,就算我们画得再好,也比不过他们的。 jVh I`F{n
7MX nt5qUh
1.2 游戏底层 Xy_ <Yqx}
在我们编写每一个游戏时,都希望能有一部分代码可以重用,这样我们才能有更多的时间做更多的事情。这些重用的代码就慢慢形成了游戏底层。游戏底层多了,自然就形成了一些类别。 B o@B9/ABv
显示底层: U}9B
wr^
用于基本显示的函数。把常用的功能制作成函数,统一出错管理。对于Windows95/98下的游戏编程,我们一般使用DirectX作为大部分底层的底层。因为这部分是我们可以用于编程的最底层函数。对于二维显示,我们使用的是DirectDraw部分。为什么还需要在DirectDraw外面再包一层函数呢?原因有三:第一,DirectDraw提供的函数扩充性很强,函数参数较多,而我们使用时参数比较固定,包裹成类库后,参数更加可以封装起来;第二,DirectDraw提供的函数比较分散,而有许多操作是需要多个函数按照一定顺序执行的,包装起来后编写程序比较简单;第三,DirectDraw函数的返回值比较复杂,而且没有对错误进行系统的处理,我们需要自己制作一套错误处理函数,对错误代码进行解释。 6U{&`8C
显示底层有许多种做法,可以完全自己开发,也可以直接使用别人完成的函数库,比如Internet上比较流行的CDX类库就是对DirectDraw进行了很好封装的底层。 Z#8O)GK
显示底层主要分成下面几个部分: vj?v7
窗口处理:窗口的创建、管理、删除。 :H}a/ x*ur
图形接口处理:图形设备的初始化、退出、显示模式的设定、主显示面、后缓冲区的建立。 4]\f}
二维图形处理:游戏使用的二维图象的装入、显示、剪裁、逻辑操作。 B~p` 3rC
外围工具:游戏使用的鼠标,刷新率计算和显示,显示内存斟测,面丢失(Surface Lost)处理(在DirectX 6.0后据说可以不用),错误处理,调试函数。 &[]0yNG
关于显示底层还有两个问题需要说明: eUiJl6^x
第一,显示部分的调试。因为我们所做的游戏一般为独占模式,在游戏运行时不能跟踪(除非使用双显示器),所以我们只能另想办法,一个比较常用的办法是使用窗口模式和全屏模式切换。不一定在游戏运行中随时切换显示,在调试时事先指定也可以实现该功能。 5)=XzO0
第二,多媒体播放底层: Vf
Jpiv1
播放多种声音,音乐,视频等多媒体效果。比较古老的一种办法是使用Windows 3.1就开始使用的MCI调用。它是一种通用接口,使用不同的参数调用同一个函数就可以实现基本相同的功能。用MCI可以实现的主要媒体类型有MIDI,WAVE,CDAUDIO,和VIDEO。它的优点是使用简单。缺点则是缺乏交互性,而且同种类型的媒体不能同时播放。这样就很难实现多种音效的混合。 $@8$_g|Wz
一个基于DirectX的调用是使用DirectSound。它是基于WAVE文件的一套播放函数。非常适合混音。缺点就是使用较复杂,而且如果想实现分段读取较长的WAVE文件,编程有些难度。 eBZ^YY<*g
另外一个针对MIDI的DirectX是DirectMusic。它是DirectX系列中的一个新成员。 B?}ZAw>
对于播放高质量的VIDEO(视频)文件,则有另外一套底层。比如Microsoft的ActiveMovie和Intel的Indeo Video。有了它们我们就可以放心地全屏播放AVI文件了。 caA>; +aBH
对于这些底层,我们所做的只是简单地封装上一层函数,使我们在使用该功能时达到最简便。 \Qp #utC0s
但是假如你对这些现成的播放程序不满意,也可以完全自己开发。只要你知道这些文件的格式,再把它们存储成你需要的格式,用自己更加优化的算法将它显示出来就可以了。用这种方式你可以自己播放AVI VIDEO, MPEG, MPEG2或FLI/FLC。 JN5<=x5r
yn;h.m [):
文件封装底层: .SdHFWx
对使用的文本,图象等数据进行封装。我们也常用这种方式把多媒体文件封装起来,防止别人事先查看。这种工作往往是循序渐进的,我们会根据自己的时间和实力,封装相应的部分。比如先封装比较简单的文本,图象。然后是较为复杂的声音文件,最后是最复杂的视频和动画文件。 WdXi
有的时候封装也是为了压缩。不仅再安装时少占用硬盘,装入时也可以节省时间,有时在运行时也会减少内存占用而不会对速度有太大的影响。 xRZ9.Agv_
根据文件类型的不同,我们可以这样分类:文本、图象、声音、视频、动画等。对于每种类型,我们还必须有打包函数和解包函数之分。 y@&Cn
O0?.$f9 s
界面底层: 8"2
Y$*)(
自己编制的类Windows风格的界面类库。我总有这样的习惯,希望在我的游戏里没有一点Windows的风格,于是我不会使用任何一点的有Windows风格的界面(游戏工具除外)。所以游戏的界面必须我们自己做。然而我又使用过MFC那一套界面类库函数,很佩服Microsoft的本事。也希望我们的游戏中有一套自己的界面系统。这套系统将会包含Windows用到的主要界面元素:位图(Bitmap)、按钮(Button)、检查框(Checkbox)、滚动条(Scrollbar)、滑块(Slider)等等。 cO$
PK
虽然Windows允许我们在使用标准界面时自己画图,但是因为它只支持256色,是基于消息响应机制的显示,而且无法支持位图的封装,所以我们使用一套完全由自己制作的界面底层。这套底层建立在显示底层和文件封装底层之上,形成了一套比较完整的系统。 ]6wo]nV[P
}m6zu'CV
上面这些只是一般游戏所需要的底层,对于某些特殊类型的游戏,可能还会有战略游戏的显示,操作底层,RPG游戏的事件处理底层,战棋游戏的显示,智能底层,动作游戏的场景底层等。如果编写三维程序,则还会有三维显示和操作底层。这些底层都不需要也不可能在一开始就全部完成,而必然是在不断的修改和检验中完善的。它们也不可能一成不变,必然随着我们编程经验的积累和开发环境的改变而改变和完善。 (h8M
每个编写游戏的程序员都会慢慢积累出符合自己游戏特点的底层函数库,这些函数库如果得到不断的发展和扩展将会越来越大,越来越好,这对于每个游戏制作公司和集体来讲都是一笔不可忽视的财富。 '\[o>n2
而这套函数库越来越复杂,再加上辅助的一整套工具,就可以被称之为游戏的引擎。有了引擎在手之后,不仅意味着我们有了一套低Bug的现成代码,程序员的工作量将会大幅度下降,而且他们可以把更多的精力放在游戏的优化、游戏的人工智能、游戏的操作、以及游戏性上面了。到了这个层次,我才可以想象一个好的游戏将会是什么样子。 .]_Ye.}
a<CN2e_Z
1.3 编写规则 5l"EQ9
现在,只需要一个程序员编写的程序已经很少了,大部分的程序都需要多个程序员的协作才能完成。这期间必然需要程序员之间互相阅读代码。而代码的编写规则就是非常重要的环节。一段代码如果写的浅显易懂,不但在程序交流时非常方便,而且对于寻找程序错误也时很有帮助的。程序员也是人,是人就会犯错误,不管他多么地不愿意犯这个错误。所以我们也希望能制定下一个规则,使他们在一开始想犯错误也不行。于是就出现了程序的编写规则。 B1]5% B
如果不按照这些规则编写代码编译程序是不会报出错误的。但是如果按照规则编写,则对我们的思路的整理、笔误的减少、错误的查找和代码的阅读都是极有好处的。 j~+<~2%c
下面我只把规则中几个重要的部分加以简单的描述,在后面附录中会有一份我们已经使用了一段时间的完整规则。 E\U6n ""]
l V[d`%(
命名规则: "tuBfA+f
程序中对变量和函数的命名看似简单,可是名字起得好与不好有很大的不同。对于变量我们一般采用匈牙利命名法和微软命名法的结合。对于函数我们一般要把模块名写在函数名的开始。对于类库我们一般要求类的成员和成员函数要紧密对应。 2t h\%
L4th 7#
变量使用: ]lj,GD)c
对于程序中所使用的所有变量,我们都需要其有明显的定义域。对于全局的变量要尽量封装。对于类库,所有成员必须封装。 y=!"++T]B<
_I;+p eq
函数使用: F<8Rr#Z
我们对函数的参数和返回值做了要求。对于函数的使用范围也做了规定,要求范围越小越好。 1( V>8}zn
esCm`?qCP
注释: xpo<1Sr>S
对于程序注释,我们虽然不需要很多,但是在函数声明、变量声明、主要算法部分、源程序文件的开始都需要有注释。 klC;fm2C
5@3[t`n'
调试: imcq
H
我们的底层和Visual C++都提供了一些供调试用的代码,在我们的代码中需要适时地使用这些代码,以提高程序的纠错机会。 R/)cEvB-0
: `D[0
这里有几个问题需要特别说明: Y,O)"6ev
第一,关于链表的使用。 CJA5w[m
我在编程中所遇到的所有死机错误中,有90%来自于指针,9%来自于字符串(其实也是指针的一种),1%来自死循环。而链表这种把指针的优势发挥到极限的方式我是不提倡使用的。有人讲如果不会使用链表就不是C程序员,那么现在我连个合格的程序员的资格都够不上了。的确,对许多人来讲,编写没有Bug的链表是很容易的事情,甚至对某些人而言,一个没有链表存在的程序几乎不能称得上是商业程序。我承认有许多天才的程序员在 -!E ))|A
自己的链表程序中表现得十分出色,他的程序效率既高又没有Bug,但是我也知道还有更多的程序员并不是天才。就算是天才的程序员如果在他的十万行代码中有五万行于某些链表有关,这其中的某个指针出了一个错误,而这个错误又不是每运行一次都出现的时候,他又有多大的把握很迅速地找到这个错误呢?链表的复杂性在于它的地址的随机性,这使我们在调试跟踪时非常困难,我们很难随时观察每一个节点的信息,尤其当节点多的时候。所以我把链表的使用完全限制在算法的需要和该代码被某个独立模块完全封装的情况下。 @WIcH:_w-
实际上我在设计程序时,要求尽量减少指针在主要模块之间的传递,而使用效率较低但是不易出错的句柄ID的方式。 K-Bf=7F,
!,}W|(P)
第二,关于类库的成员。 -uZ bVd
既然我们使用了类库,其中一个主要目的就是要把其中的成员完全封装起来。首先把它设置为私有,然后定义标准的一套Set和Get函数。这虽然使我们在写代码时比较烦琐,但是只有这样我们才可以严格规定该成员的只读属性和可写属性,减少对某些重要变量的无意修改。 y74Ph:^k
QG\lXY,
第三,关于变量和操作符之间的间隔 "kC uCc
许多人对于变量和操作符之间的距离并不在意,认为多个空格少个空格只是看起来好不好看而已。而实际上,这对我们是很重要的。因为如果我们把间隔固定下来,在查找某项操作时就可以使用带空格的字符串,而大大增加找到的机会。 et|P5%G
` aTkIo:ms
1.4 程序设计 Jd_w:H.
我们在编写自己第一个游戏时,往往是凭借自己的兴趣,想到哪就编到哪,有时连自己都没想到会编出这么大的程序来。那时侯我们对代码的修改往往是很随意和彻底的,因为本来都是自己的东西,而且更希望它更好。可是,程序越写越大,有一个问题就会越来越明显,那就是程序的错误。为了减少错误的产生,我们必须采取一些办法,所以也就产生了程序设计。程序设计的必要性还表现在另一个方面,那就是程序员间的合作。为了让每个程序员对我们所要做的事和我们将如何做这件事有个明确的了解,也为了让管理人员能够从量的角度了解任务的完成状况和进度,我们也必须有程序设计。否则我们就只能象一群乌合之众一般,鬼才知道这游戏要做到什么时侯。 5>.)7D%
一般,我个人认为,当一个程序需要写到10000行以上时就应该有比较独立的程序设计文档了。可是程序设计文档应该是什么样子呢?很抱歉,我也一直在寻找这个问题的答案。在我刚开始做游戏的时侯,没有人教,于是我翻出上学用的课本,打算来个生搬硬套,写来写去,发现,不行。这样的写法太累了。比如在软件工程课上所讲的数据流图,我好不容易写满了一张八开的纸,马上就发现自己已经糊涂了。就算不马上糊涂,过一段时间我也会认不出那些条条框框是什么了。就算我能记得这些图表的意义,我能够保证所写的代码和图表上的一样么?我写完后,还需要和这些表对一下。就算程序和文档中的内容一样,那如果我的设计方案改了呢?又需要先改文档,再改程序,再对程序。这样对我来讲可太累了。 K5U=%z
后来,我才发现,原来书上所写确实有道理,可是它并不是面向我们这些人的,它所面向的是写大型程序的人。这些程序大到需要专门有这样的人来写文档。这些人只要把文档写好再确认完成的程序是否符合要求就可以了,根本不用去写一句的代码。是啊,如果编游戏也有这样的工作就好了。可是很遗憾,目前我尚未遇到可以聘有专职程序设计人员的公司。所以程序设计者就是程序编写者的这个事实起码在我们的目前阶段是很难避免的了。 @fR^":.h
那么怎么设计文档对我们用处又大又比较节省时间呢?每个公司和每位程序设计人员的习惯都不一样,我只介绍一下我们曾经使用过的一些方法和文档的类型。 I(fq4$
G%N/]]ll
程序设计总纲: ~YO-GX(
无论我们做什么,最先要清楚的一个问题就是:我们做什么?很弱智是不是?但是我发现有些人在很多时侯都“不知道”自己在做什么。程序设计总纲就是要告诉自己和自己的伙伴们,我们要做的是什么样的游戏程序。它包括: XCU.tWR:
游戏概述:游戏的背景、类型、操作方法、特点等; D`]Lm 24_]
程序概述:游戏程序的编译平台、硬件运行需求、语言、编程重点和难点、技术能力分析等;
Sbub|
程序员概述:参与编程的人的能力和在整个程序制做中的作用和地位; @-dM'R6C
程序模块划分概述:有本游戏所需要的所有程序,和程序内部的功能模块的划分。这部分只要有比较概括的说明就可以了。 tjZ.p.IlG
这篇文档的作用就是让所有人都能够了解本游戏的程序将会是什么样子,它的特点是什么,完成它的难点又是什么。以及我们为什么要这样做,有谁来做等这样基本的问题。它所面向的对象除了有自己和本组内的所有程序员以外,还应该有本游戏的游戏策划,美术设计、项目负责人、项目经理、制片人等。如果有可能也应该和本公司的市场、销售、广告公关、出版、甚至公司总经理充分接触,听取各个方面的意见,也充分表达自己的意见。如果能做到制做本游戏项目的所有人员从上到下对本游戏的程序制做都能有一个基本的了解,理解和信心那是最好的。 (uG.s %I
$Xf1|!W%a%
程序模块划分: nOxCni~T
这是对程序设计总纲中最后一个部分的详细说明。说明的内容主要有:该模块的功能、接口、技术要点、所用到的软件底层等。它所面向的对象一般是所有的程序员,但是如果让与程序合作的人员(美术、策划、经理)也能了解则更好,这样可以加强我们之间的勾通,让不懂编程的人也能了解编程人员的苦衷。 S -j<O&h~C
一般而言,一个游戏按功能划分(我们很少用其它的分类方法)可以有下面几个部分: #~um F%#
&0*l=!:G^
工具部分:在游戏的制做过程之中,从游戏策划的文档,游戏美工的图片到游戏完成,中间必须经过游戏程序的转换和加工。这些数据在进入程序之前必须经过规整,排序等操作。这部分工作过去一般都是由程序员来做的。在人手紧张和工作量比较小的时侯这样做还是可行的。但是如果这样的工作过于复杂,工作量太大,就不行了,我们需要一种方法来提高工作效率。这种方法就是工具。其实我们在计算机所做的任何工作都是在使用工具,唯一的区别只是这些工具都不是我们自己做的。因为游戏本身的特殊性,我们不可能总能找到我们所需要的工具,自己制做就是十分必要的工作。 '0g1v7Gx
这些工具也可以分成三个级别:别人的工具,我们自己的可以重用的工具,我们自己的只在本游戏使用的工具。别人的工具是最好的工具。原因很简单,因为它不用我们再去编了,不仅程序Bug较少,操作界面友好和简单,而且开发时间是零。所以如果可能一定要使用别人做好的工具。如果实在没有满足要求的工具,或者我们还需要该工具所没有的功能,我们只能自己编。那么如果这个工具可以被以后用到也不错。所以我们在编写任何工具的时侯,其通用性是我们考虑的首要因素。 %V-\ |cw
工具与游戏其它部分的程序有一个最大的区别,那就是工具程序是独立的可执行文件,它与游戏的唯一交互方式就是存在磁盘上的文件。所以它比较适合交给其他的程序员制做。另外一个特点就是它一般都是针对非程序员的,所以如果工具做好了,也可以很大程度上帮助他们提高工作效率。 [Af&K22M(X
q0Fq7rWP
底层部分:这部分程序代码的最大特点是可重用性。它是游戏本身与计算机通用系统之间的桥梁,它与游戏的具体细节关系并不大,可以被使用到多种游戏类型和多个游戏之上。这对我们是非常重要的。实际上,我们编写程序所积累的最直接的财富就是可重用的工具和游戏底层。 g]|K@sm
游戏底层从与游戏的相关性可以有大概三个层次: mIVnc`3s
最底层:它们向下直接调用系统函数和功能,向上与游戏几乎完全无关,是游戏程序与计算机系统的直接桥梁。它包括: @/}{Trmg/
·显示底层:提供游戏显示所需要的所有函数; M0`nr}g
·文件底层:提供对游戏所需的各种文件处理的接口; }^uUw&
·多媒体底层:提供游戏所需的多媒体效果,如声音,视频等; E@\e37e
·对于三维游戏有模型管理底层:专门保存,处理和显示三维数据; @xR7>-$0p
中间层:它们向下调用最底层的游戏底层,向上为游戏提供函数。与游戏无关。它可能: WrhC
q6
·界面底层:使用显示和文件底层,提供界面类库和操作函数; 6'y+Ev$9
最高层:它们向下调用中间层和最底层,向上为某种类型的游戏提供特殊的逻辑支持。它可能有: zAEq)9Y"l'
·战略游戏的命令执行底层,显示底层,地图底层。 %Kd&A*
·RPG游戏的事件处理底层,场景底层。 dzDh V{
i:`ur
这三个层次的制做难度是越来越难的。它往往需要我们在制作过几个游戏之后提炼和总结出来(实际上我们到现在也还没有发展出第三个层次的底层)。但同时,这三个层次是越来越重要的,因为它距离一个成品游戏更进一步。我们在写游戏时可重用的部分越多,重用的层次越高,我们的工作量就越少。我们也就越可能有时间研究和开发新的技术,好的算法,我们的游戏也就会越做越精。 lcgT9m#
相比之下,前面的两个底层都与计算机系统有关,与游戏本身无关;第三个底层与计算机系统无关,与某种类型的游戏有关。 MdK!Y
底层部分与工具部分也是密切相关的。一般每一套底层都需要一些工具来与之对应。比如,我的显示底层需要某种格式的图片,我们就要制做一个格式转换和图片观察的工具。把工具和底层结合起来分类,然后把它较给独立的程序员去完成,是一种比较常见的模块划分方法。 .+3= H@8h
当然,我们在做游戏以前也不一定非要什么工具和底层不可,这些东西完全都可以“自然”产生。假如你能够坚持制做几个游戏,把可以反复使用的程序归纳起来,修改一下,就是一套底层了。如果我们在编写程序时,每时每刻都在想:这段代码是否可以在以后使用,怎样的写法可以使我在以后使用,那么我们在后来的修改工作中就会轻松很多。而假如我们在编写程序以前多花一些时间,把想要做的东西设计清楚,在一开始就可以把它写成独立的底层代码。这样积少成多,我们的财富就会越来越丰富。 GF5WR e(E
w)-@?jN
游戏本身:这才是我们每次都要做的游戏。所有体现游戏特色的内容全在这里。当然也有的人认为如果我们的经验足够丰富,想得足够全面,连这部分的程序也可以重用起来。以后我们编游戏只要改一下图片,换几个名字,编一段故事,一个新游戏就产生了。实事上,也有公司制做了一些类似游戏工具的东西,其中做得最好的应该是象Director、Autherware那样的多媒体编辑工具吧。但是,它们都有其局限。最明显的一点就是:它们必须针对某一个类型的游戏或只能是多媒体程序,不同类型游戏的制做是有很大区别的,几乎很难用一个固定的模式将其完全函盖。其次,它们创新困难,现在游戏几乎成了创新的代名词,哪一个游戏做出来没有它自己的特点呢?而使用一个固定的工具是很难做到这一点的。第三,难以对它们进行优化。程序的优化往往和程序本身的特点有关,优化的程度越高,它的特殊性也会越大,所以,通用工具只能牺牲一些性能。我到目前还没有看到一个游戏制做工具可以达到商业效果,而多媒体工具之所以流行正因为制作多媒体产品所需的模式比较单一,对速度要求也不高。 03?TT,y$
所以,制作一个完全可以通用的游戏工具到目前仍然还只是一个梦想。但是,我想这是一个很美的梦,如果真的到那个时侯,我们制做游戏将会和拍电影一样简单(我指的只是制做的过程),只需要一个程序员根据策划的意思把图片和声音放在一起就可以了。 Q\G8R^9j p
每个游戏都是不一样的,但它们都有一些相似的地方,比如: bF %#KSVw
游戏的操作:游戏者如何操作和控制游戏中的一个或多个主角; }OO(uC2
游戏的显示:不是游戏的底层,而是与具体游戏有关的显示部分;
&T?>Kx
游戏的运动:游戏中有的内容是要不断变化的,包括位置变化,图片变化和形式变化; ]T\K-;i
游戏的智能;能够与人对抗的思考; \a+F/I$hwa
游戏的存储;把所有当前游戏需要的数据保存在磁盘上,然后再重新装入游戏; LLv~yS O
游戏状态的转换:从界面到游戏,从游戏到动画,在不同的时侯有不同的操作和显示管理。 <mlQn?u
我们通常也会按照这样的规律对游戏进行模块划分。 PT4Xr=z =
!!&H'XEJV
游戏数据结构和算法: xkR--/f
有人讲程序就等于数据结构加算法。这句话很有道理。我们所编的程序其实就是把某种格式的数据(图片,主角的参数)经过一系列的转换,成为计算机屏幕上的数据(游戏本身)。所有的数据都需要以某种方式存储,这就是数据结构。而算法因为是与数据结构密切相关的,所以虽然它们不属于同一个概念,我把它们放在一起设计。 LXj2gsURu%
我们通常所使用的数据一般可以分成两个部分:数据库结构和当前结构。 s~Wj h7'
这里的数据库不是什么商业数据库软件,而是游戏中所使用到的所有数据的集合。我还没有见到有哪个游戏在存储这些数据时使用商业数据库软件(比如FoxBase)。也许它们都被隐藏起来了吧,我猜想。但不管怎样,其意义是一样的,我们需要把所有在游戏中用到的数据都以一定的形式储存起来。同商业数据库一样,数据库的组成也是由字段组成,即数据元素。每个数据元素一般是一个类库或结构,有若干的成员。数据元素所组成的数组就是数据库结构中最主要的组成部分。 !cnun Lc`
数据库的内容很多,而且按照一定的规则排列,而真正需要显示和当前需要计算的数据却很少,有些数据还是计算的中间结果,比如主角的位置,主角的动作等,这时侯就需要我们另外存储一些数据。它往往是数据库中的某个字段(数据元素),或一些数据的组合。 _3<J!$]&p
如果把数据结构按照功能分类,我们还可以把它分成主数据结构,模块数据结构。它们的定义域是不一样的。 "UVqkw,vt
在我们的游戏中,会有许多数据结构的类库,它们之间也必然有数据结构之间的数据传递和保存。如何处理好这些数据之间的关系是编写这部分内容最重要的考虑。 )t={+^Xe
首先是数据结构定义域的问题。在无数的教学课本上都在不断提醒大家,要尽量避免使用全局量。为什么要这样做呢?第一个原因显然是因为这样做的人太多了,我们使用全局量直接,方便,快捷,有什么理由不用呢?而答案也很简单,因为它暗含了错误,因为它不易于阅读和修改。于是我们用两个规则来限止全局量的使用:第一,尽量使用局部全局量,使该全局量的定义域最小,比如基于源程序文件的全局量,只有整个游戏的核心结构是全局范围内有效。第二,对于所有的全局量都对其内容进行全面的封装,如果是类库则使用成员函数,如果是结构,则定义专门的函数对其内容进行处理。 ,c"_X8Fkx$
其次是数据的传递。我们经常遇到数据结构之间的互相调用和嵌套,很多人都喜欢使用指针(又是指针)做这项工作,因为它象全局量一样:直接、方便、快捷。但是我因为个人水平的问题在使用指针时总遇到一些不便,比如,易于出错,而且一旦出错就是很严重的当机,程序非法退出。要知道现在每启动一次计算机仍然是很耗费时间的。还有另外一个问题,那就是指针不能存盘,我必须另外制作一套程序对结构的存盘做特殊处理。所以我一般使用索引ID号来代替指针。具体的使用是这样的:数据库结构是以数据元素为单位的数组,数据元素的索引(ID)就是它在数据库数组中的下标。所有其它结构对该数据元素的保存都只保存了该元素的ID号。指针只在从数据库根据索引得到元素时使用。这样的好处很明显,ID号是整型数据,可以通过该值的范围判断出是否有效,只要在其有效范围内,再通过指针取值肯定是有效的,几乎跟本不会出现非法指针的现象。 j &)Xi^^
uyP)5,
程序开发计划 f XxdOn.
有了要做什么和怎么做,下面也就是需要什么时侯做和由谁来做了。 ]d?`3{h9LD
一般我们都会制定一系列的里程碑。比如演示版,原型版(体验版),测试版和正式版等。在这之间也可能会有其它什么版本。然后制定完成每一个版本所需要的时间,主要内容和验收标准。除此之外,我们还有: :~loy'
程序中每个模块的制作时间的表格, Z!=/[,b
每个程序员分工的说明,工作量说明。 a$Eqe_
编程过程中可能出现的风险和问题的说明。 xdp!'1n."g
L>$yslH;b
在写完总计划以后,我们还会有更加详细的月计划,阶段性计划,我们还会写程序进度表。我们所做的一切都是为了游戏程序可以按时完成。 [oOZ6\?HB
影响开发时间的主要因素有很多: S!8eY `C.
·资金因素:这些钱至少应该支持到我们可以把游戏做完,而且能够把我们的游戏向市场推出; i_ws*7B<
·市场因素:是否是销售旺季,是否有同类产品产生竞争,本类型,题材的游戏是否还受欢迎; zR
h1
·策划因素:游戏的复杂性有多大,内容有多少,策划的进度是怎样的; [P)'LY6F
·美术因素:美术的制作进度是怎样的,何时可以提供需要的内容; 9F)v=
·技术因素:用户的计算机硬件是否已经准备好。我们的技术是否会过时,开发新技术的风险会有多大。还需要制作哪些外围工具; !q~s-~d^
·人员因素:程序员水平如何,配合状况如何。 %j=dKd>
*A2J[,?c
程序设计人员需要仔细考虑这些因素,才能根据具体情况制定比较完善的程序制作计划。 ~PaD _W#xP
但是,很不幸,我从没有按时完成一个游戏,所以我目前尚不知道妨碍我们按时完成进度的到底是计划不够好还是执行的人不够努力。总之,在做计划时一定要留有余地,而且我的经验是我们完成游戏的时间一般比计划的要多20%到30%。可是如果按照这样的计划去做游戏的话,一定会把投资人吓跑的。于是在现实生活中这种状况就变成了“先斩后奏”,写报告的时侯说时间很短,而到时侯我们就因为某些原因不得不拖期,反正钱也花了这么多了,必须再增加投资而把它做完。所以无论是直接制作游戏的人或者投资制作游戏的人,我都希望他们可以对这件事有个比较客观的了解。也许在将来制作游戏可以不必再加班,制作计划也可以按时完成,但是至少在我们这里,还必须随时面对它们,依靠我们顽强的毅力和坚忍不拔的精神把游戏做出来。 NylN-X7[#
Woa5Ov!n0
1.5 制作流程 {U(-cdU{e`
等到游戏计划写完,并得到通过之后,我们便开始进入游戏制作的时间。其实游戏的制作可能从很早就开始了,比如早期的技术探讨和准备。而我在这里想再说得稍微远一些,远离现在的题目:程序,而转向整个游戏的制作。 _Hi;Y
整个游戏的制作过程一般可以分成三个阶段。策划阶段,制作阶段和调试阶段。它们之间的关系一般应该是在策划阶段后期,开始程序和美术的制作,在制作后期开始游戏的调试。它们三者的时间比应该是3:4:3。我认为这是一个理想状态,但是我们往往达不到理想状态,典型的情况是游戏策划迟迟不能定稿,我们不得不在一边设计游戏时一边制作,而到了后期,制作又很难按时完成,必然压缩调试时间,最后只好仓促地发售游戏。它们三者的时间变成了4:6:0。我所说的这种状态当然只是一个极端,但是我想在我们目前所开发的游戏中或多或少都有这方面的问题。 ]]@jvU_?kS
造成这个问题的原因也有很多: a*hOT_;#
·游戏立项不规范:人们不知道为什么要做这个游戏; i`7{q~d=
·策划设计工作准备不足:人们不清楚该做什么、什么时侯做、谁来做; 6FG h=~{3,
·制作人员水平有限,缺乏经验:自己做不好,或做不快,甚至都不知道该怎么做; )hK5_]"lmj
·资金:没有钱谁会做呢? A/RHb^N
·项目缺乏管理:各部门缺乏协调,大家群龙无首,缺乏沟通,最后大大降低士气和工作热情。 kCxmC<34
Z `O.JE
但是无论这些因素都是些什么,以及它将如何严重地影响我们,我都觉得只要有一样东西就足以支持我们把这个游戏做完。这就是我们的坚持不懈的努力。现在,我们在市场上的游戏质量都不算很好,但是我要说,只要它被做出来了,就是一个成功的作品。我不管游戏玩家们对我这句话有何看法,我只是认为,在那些作品的背后一定有一个或一些人在努力着。也只有他们才能真正体会制作游戏的艰辛。而且,只要他们还能继续做下去,经验会逐渐增加,配合会更加默契,游戏也一定会越做越好。 4q[C'
J
(:2:_FL
游戏程序的制作过程与游戏的模块划分有关,大概可以分成三个阶段:工具制作阶段,底层制作阶段,游戏制作阶段。 8lI#D)}
底层制作可能是最开始进行的阶段,有可能与策划阶段同时,甚至更早。因为这个部分与具体游戏无关,而可能只与游戏所要运行的平台和我们所使用的开发工具有关。我认为,开始的时间应该越早越好。游戏程序员在技术上的准备如果越充分,制作起游戏来就会越顺利。 H,txbJ
在策划大纲基本上完成后,也就是游戏的类型,模式基本上固定之后,就可以开始游戏工具的制作了。游戏工具一般是提供给美术人员,策划人员使用的(当然也有自己使用的),所以在用户界面上应该多多听取他们的意见。这些天天与我们在一起的用户,如果我们都不能好好对待,那就更别提我们游戏的用户了。 {YWj`K
游戏本身程序的制作是最耗费时间和精力的地方,如果说底层程序和工具都可以把重点放在结构化和优化上,这部分程序正好相反,我们通常做不到这些。因为我们在写这部分程序的时侯,总是已经精疲力尽了,而且我们的代码已经很长很长,一旦发现了程序错误,能找到就很不错了,更别提把它改得漂亮一点。再加上游戏策划随时提出的一点小改动,就可能要改变我们整体的数据结构,做大的改动几乎是不可能的。所以到了游戏制作后期,把程序员叫作铁匠更合适一些,他们在到处打补丁(注意,千万别在写游戏底层的时侯也干这种事)。 Qi2yaEB
<ro0}%-z>M
在游戏制作的中后期,每当程序员一睁开眼,进入眼帘的就是满是Bug和缺乏功能的游戏,这样的一个程序如何才能变成游戏中真正需要得程序呢?无论当初程序计划制定得多么好,在这时候都显得有些不中用。但这并不说明计划没用,而是需要我们把计划变成我们真正需要的东西,这就是每日工作计划。名字听起来很正式,其实并没有什么特殊的格式,仅仅是把我还没有做出来的游戏的内容一条条地写出来,不必区分什么模块和内容的多少,只要手写而且自己看得懂,然后贴在机箱上,要保证随时看到。这些内容我可能在一个星期后也没做,但是它会时时提醒我还有什么没有做。随着时间的推移,有些项目已经完成了,又会有新的内容写进来,而等到你最后积累出一大叠这种计划单时,你会发现原来游戏已经做完了。 1i#uKKwE
这种办法我一直在使用,有时侯真觉得自己没有长进,做事一点计划也没有,可是它就是这么有效,你不必有什么经验,也不必整天面对着程序发呆,每天只要考虑如何把下面的内容做好就可以了。 NUiZ!&
~\4l*$3(^
不过我还是希望以后的程序员不必这样写程序,如果所有的人都能够每天按照预先制定好计划完成工作,也不需要加班,最终做出的游戏又很不错就好了。希望这不是永远的梦想。 LtbL[z>]
ZgF-.(GV
1.6 程序调式 E4[
|=<
我在前面曾经说过,我不会使用SoftICE来调试程序。这证明我并不是一个很聪明的程序员。这也证明我在调试程序时会遇到更多的困难。让我每天在汇编代码和死机中漫游是件很痛苦的事,所以我总在想如何才能摆脱它们。 ,kuJWaUC@
如果我不能很快地找到这些难缠的Bug,我能否在一开始就避免它们呢,甚至减少它们出现的机会呢?答案是肯定的。 tY !fO>Fn~
死机(在Windows系统中有时是“非法指令”错)错误很主要的来源来自指针,但这并不是说我们就不能使用指针。恰恰相反,我们在很多地方都使用指针,而且出错的机会很少。原因很简单,那是因为我们在使用被保护的指针。如果你能证明你所使用的指针永远都指在合法的位置上,它还会出错么?所以我们在分配空间时大多使用静态空间和连续空间。所谓静态空间就是数组,连续空间就是指针数组。我们通过数组的下标来限制指针的读取,这样非常方便也有效。 @=`Dw/13
封装也是减少错误的很好办法。我们把对内存的分配和释放封装起来,把对全局量的读取封装起来。在查找问题时就可以很快地缩小可疑对象。 m9Gyjr'L
对函数的返回值和错误代码的认真对待也可以让我们飞快地找到问题所在。很多人刚开始编程时都认为程序很短小,没必要写那么复杂的错误处理信息,也没必要判定函数的返回值。而我则恰恰相反,无论对待多么简单的程序我都会做非常详尽的错误处理。我甚至把显示错误的对话框写在底层程序中,以供随时调用。假如出现了错误,我就可以迅速知道错误发生的位置以及发生错误的大概原因,我的程序还可以正常地退出,不是继续执行而造成死机。 v 2k/tT$t
使用调试信息可以帮助我在Debug版中获得更多的有用信息。比较常用的有Assert()和OutputDebugString()。在错误刚刚发生时就拦截它要比它造成了更严重的影响后要好。 Z"'rc.>a
代码写得好看一些也有助于查错。假如你在一行中写进好几句代码,你将如何逐行跟踪呢?我们如果把函数,成员函数写得规范对称,虽然麻烦一些,但别人在使用时就会轻松多了。 ?{%P9I
b>;>*'e
这些办法都是在我们编写代码时需要注意的内容,我想如果你在准备开始编程时就对此有所准备,那么到你编程结束时应该能够节省不少调试错误的时间。但是Bug是一定会有的,无论你在当初如何注意,因为我们毕竟是人,会犯错误。我想任何人都有过把恒等号(双等于号)写成赋值号(单等于号)的经历吧(好在在Visual C++ 6.0中有这方面的警告了)。没有别的办法,我们仍然需要面对死机。一般的指针错误只会导致“非法指令”或 TYWajcch
“未知的意外”错误,除非你向一个不知名的地方写了大量的内容(比如几百K以上),一般还是可以安全地退到系统中的。这得非常感谢微软,Windows95/98还是要比DOS好。可是如果你所访问的是系统区域,或申请了与硬件有关的调用,比如在DirectDraw的GetDC()中设置了断点,程序就会直接退回系统或干脆死机。 A?|KA<&m#u
这个时侯就全凭我们自己了。但是也不要害怕,因为,我们所面对的仅仅是一些代码而已,我们有一个最为严格和伟大的力量:逻辑。这可能是程序员唯一能够全心全意依靠的力量了。所以请大胆地使用它。 iN+Dmq5
如果程序出错,一定有它的道理。哪怕运行一千次只出现一次也是如此。所以我们改正错误的过程实际上也就是找到错误的过程。如何找到错误呢?可以试着用下面的方法: QKc3Q5)@j
6@g2v^ %
屏蔽法:把一些代码去掉,再看错误是否消失了,如果已经消失,那么错误可能在刚才屏蔽掉的代码中,再屏蔽掉另外的代码,如果错误又出现了,则错误肯定在这段代码中。要注意的是,这样做不一定能够找到错误,因为,有时侯错误是由两段代码或多段代码相互影响造成的。而且有的时侯我们找到的地方可能并不是真正出错的地方,你只找到了错误被引发的地方,而引发错误的地方还需要我们再仔细找。比如,一个指针指错了地方,那么我们会发现出错的地方是引用指针的地方而不是写入指针的地方。这时侯就体现出封装变量的用处来了,我们可以很快查出这个指针到底在哪被修改了。 x68J [; jm
如果上面的方法不能奏效,而且错误是在我们刚刚修改了代码造成的,那么可以使用比较法,找到原来版本的文件进行比较。所以我们需要经常保存备份程序,一方面是为了保证程序不丢失,另一方面也会增加找到错误的机会。尤其是当我们到了编程的后期,我们的程序大都编完,正处于修改的状态,而时间又特别紧迫。如果一旦出现了错误,我们只要比较一下新旧版本,就很容易找到错误。 2,puu2F
如果你跟踪了半天也没有结果,几网下去一条鱼也没捞上来,而且必须在全屏状态运行,不能单步跟踪,每执行一遍程序就死机,又需要重新启动,这又该怎么办呢?那也不要担心,我们还有最后一招:输出Log文件。在每一个需要监视的地方,增加一段程序,向磁盘文件输出一段文字,如果程序运行到这里,该文件就会多出一段文字,如果程序运行到这里之前就死机或退出了,那么出错的地方就在这段文字和上段文字之间。虽然这样做比较耗费时间和精力,但是基本上能够找到出错的地方。找到之后,我们再去找出错的原因吧。 ]Gow
但是万一你现在还是没有找到错误该怎么办呢?那也不要着急,我所遇到的找到错误的最长时间是整整一天,有的人会长达3天,但是我目前尚未遇到查找单个错误的时间超过一个星期的。如果你寻找错误的时间还没有达到这个长度,就不要急于认为这个错误你是永远找不到的。 hJ.XG<?]$
?;>s<
其实最难找的错误并不是这些错误,因为这些错误是确实存在的,所以我们改正它只是时间问题。而比较难以调试的错误并不是每次运行都出现的,有的错误需要特定的环境和条件,比如只在某台计算机上出现,有的错误则需要特定的操作,比如打开某文件,再关上,有的错误则干脆需要运行一定的时间才可能出现,比如运行一个小时。这时侯我们的工作才真正艰巨起来。 >4}+\ Q`S
这时侯,也往往是开发的最后关头,最紧张的时侯,如果出现了错误,将使我们直接面临一个最严峻的问题,到底是发行时间重要还是修改程序错误重要。我姑且不谈到底谁重要。关键是如何解决这些错误。这里你可以看到,把错误消灭于未然是多么重要。我们工作的重点现在转向了重现错误。使用出错的计算机,频繁重复执行某些有关的操作,随时存盘以保存最近时的信息,都是我们常用的办法。 w7p%6m
有的时侯重新启动一次计算机,重新编译所有代码,更换一些驱动程序,都可以解决问题。但是有些问题我必须说明: \D?6_
,O
第一,要以负责任的态度来解决问题。如果你发现有一个地方很明显不会造成错误,但是修改一下之后错误确实消失了,这时侯千万要小心,因为这个错误可能这样被你又隐藏起来了,在以后的某个时侯(可能是最后的调试期)再次出现,这时侯它将会更加隐蔽。 #Bj{
4OeV
第二,不要动不动就认为是编译器,驱动程序,操作系统或计算机硬件有问题,而不下功夫去寻找错误。这些东西确实会有问题,但它们出错的机会比起我们自己要小得多,我觉得初级程序员还很难发现它们的错误。有问题还是先从自己那里着手吧。 U`K5 DZ~
&WN4/=QW-J
1.7 代码优化 O^G/(
我对代码优化的研究并不深,归根结底是因为我并不是个很聪明的程序员。我曾经见到过一本非常好的书,名叫<图形程序开发人员指南>,英文名是Michael Abrash's Graphics Programming Black Book Special Edition。说它好有两个原因:第一,这本书从头到尾讲得都是程序的优化;第二,我看不懂。我想对于那些高级程序员来讲,他们可能就根本写不出有错误的代码,或者至少他们对于防止程序错误很有一套,总之,程序 -'BJhi\Y]~
的调试对他们来讲根本就不是问题,所以他们有的是时间来研究程序的优化。而我所能讲的显然不能与他们相比,仅仅是非常初步的内容而已。 <8 Nh dCO6
最优化的代码就是没有代码。 )XMSQ ="m
我忘了这是谁曾经说过的话。但是我觉得很有道理。有时侯我们把那些外国人想象得多么了不得,其实他们大多只是把这句话应用了一下而已。想一个巧妙的办法有时要比节约几个指令周期有效得多。但是这与我们具体所写的代码和人的经验有关,我们很难只用几句话就把规则说清楚。要知道,游戏的速度有时比游戏的效果重要,如果你对当前我们最需要什么样的游戏有所了解,做起决定就会容易得多。 5[`!\vCiZ
#6fp"
如果我们真的需要对某些代码进行实质性的优化,那么首先我们要搞清楚哪里最浪费时间。我们常用Profile,VTune或自制的时间计数器来测量时间。而往往最浪费时间的代码大都很少,多是大量的循环最占用时间。 flIdL,
优化的级别也有区别:算法级,语言级和指令级。 T
nAd!
体现一个程序员水平最重要的地方就是算法。一个好的算法可以使用非常少的代码就实现原来很复杂的操作。而它也是很难做到的。尤其这些算法经常与负载的状况有关,所以需要比较和测试才能有好的效果。 \:4WbM:B
语言级优化就是我们采用较少的C语言代码来代替冗长的。比如使用临时的指针来代替多级的成员读取。把某些赋值语句放到多重循环的外面,使用inline函数,使用指针或引用代替结构赋值,使用指针的移动代替内存拷贝,把初始化操作放在一开始而不是循环之中。它所遵循的原则就是“无代码”原则,减少需要执行的语句是提高速度的最直接的做法。一般的程序员只要做到这一层就应该可以实现比较明显的优化效果了,这样的程序比较简捷,运行效果也比较稳定。 jC
,foqL
指令级优化则要深入得多,我对这项工作也并不十分擅长。这里所要用的语言一般是汇编语言,调试和测试也比较复杂,程序不太容易看懂,也更容易出错,有时与硬件有关。但是它所能实现的效果可能是一般人所不能实现的。所以这种方法一般被高级程序员所使用,所针对的代码数量应该比较少,正是刀刃上的部分。这样的优化是以指令周期做为单位的,所以千万要注意,不要让我们费尽心机所做的优化效果,被另外一些很低效的C代码给抵销了。 dMw7Lp&
优化的内容一般有: +`kfcA#pi
代码替换:使用周期少的指令代替周期长的指令。比如使用左移指令代替乘数是2的倍数的乘法。使用倒数指令(如果有的话)代替除法指令。这要求我们对80x86的每一条指令都有很熟悉了解; 5!}xl9D
减少分支预测:这是pentium 以上CPU特有的功能,它会在执行该指令前预读一些指令,但是如果有分支就会造成预读的失效; 5X\3y4
并行指令:这是pentium 以上CPU特有的多流水线的优势。两条(或多条,在pentium pro以上)参数无关的指令可以被并行执行; ye KzI~
MMX指令:在处理大量字节型数据时我们可以用到它,一次可以处理8个字节的数据; k6ERGQ9|I
指令的预读:在读取大量数据时,如果该数据不在缓存里,将会浪费很多时间,我们需要提前把数据放在缓存中。这个功能大概要在pentium II的下一代CPU Katmai中才会出现; _ot4HmD
Katmai指令:想一次处理4个单精读浮点数么?那就使用Katmai CPU 中的有关指令吧。 6AqHzeh
YZ:YYcr
在优化时要注意的问题有: `j(\9j ok
第一,不要本末倒置。先优化大的内容再优化小的部分。这样我们才总能找到最耗费时间的地方而优化它; 第二,要经常比较。需要对每一种可能的方法进行比较,而不能只听信书上写的。奇迹经常出现在这里; yU\&\fD>j
第三,要在效率和可读性上掌握好平衡,不要光要求速度而不管结构如何,最后造成隐藏的错误;