第1章 学海无涯
"Oh, you want some too?!?"
—Hudson, Aliens
Windows 编程就像是一场由来已久并还在进行着的战争。尽管游戏程序员曾经一度拒绝为Windows平台进行开发,但正如《星际迷航》中Borg种族的生物常说的那样:“抵抗是徒劳无功的……”,我觉得这个说法很合适。本章内容将带你快速地巡视一下Windows游戏开发的各个方面:
• 游戏的历史
• 游戏类型
• 游戏编程要素
• 使用工具
• 实例:FreakOut
历史一瞥
一切都可以溯源到第一台大型计算机在60年代问世的时候。现在回想起来,我觉得当时运行在Unix计算机上的Core Wars可列为最早的计算机游戏之一。而当70年代也飞逝而去的时候,全世界的大型和小型计算机上已有了为数不少的冒险游戏。它们大多基于文字和对话,并具有朴素的图形界面。
有趣的是,在那个时代多数游戏都是在线游戏!我指的是,那时90%的游戏是MUD(Multi-User Dungeons,多人的龙与地下城游戏)或者类似的模拟游戏,例如Star Trek(星际迷航)和一些战争模拟游戏。但是,大众还是一直等到一个名叫Pong的计算机游戏出现才开始领略计算机游戏的魅力。Nolan Bushnell设计了Pong,这个单人游戏在一夜之间实实在在地启动了整个视频游戏行业。Atari这个品牌也是那时诞生的。
此后,大约在1976~1978 年间,TRS-80、Apple、Atari 800 等型计算机相继被投入市场,它们是消费者所能买到的第一代计算机。当然,你也可以买到类似Altair 8000 型的组装机,但是又有谁乐意进行它们的组装呢?无论如何,这些计算机有各自的优缺点。在这些计算机中间Atari 800 是当时功能最强大的计算机(我深信可以为其开发Wolfenstein(德军司令部)的一个可运行的版本),TRS-80 最商业化,而Apple电脑的销售情况最好。
渐渐地,这些计算机系统上的游戏开始冲击市场,一夜之间,出现了许多十多岁的百万富翁。在那个时候,只需要一个类似“月球登陆者(Lunar Lander)”或弹球(Pong)类型的好游戏,就可以让它的制作者突然暴富!在那时,计算机游戏开始像真正的计算机游戏,而且只有极少数人知道如何编写游戏。当时绝对没有游戏开发指南一类的书,只是不时有人半地下地出版一些50~100页的小册子,来解答关于游戏开发的谜团。似乎《Byte》杂志上有过一篇文章,但是,大多时候,你必须靠自己。
80 年代是游戏升温的年代。第一代16 位计算机问世,如:IBM PC( 及其兼容机)、Mac、Atari ST、Amiga 500 等等。在此阶段游戏画面开始变得好看了,甚至市面有3D 游戏出现,如:Wing Commander(银河飞将)和Flight Simulator(模拟飞行)。但是那时的PC 机仍然落后于游戏机。截至1985 年,Amiga 500 和Atair ST 作为最强大的游戏机几乎统治了游戏市场。但PC机逐渐以其低廉的价格和广泛的商业用途开始被大众所喜爱。结果就是无论技术或质量如何,PC机终将一统江湖。
在90 年代初期,IBM PC及其兼容机是市场的主流。随着微软的Windows 3.0 的发布,Apple Macintosh寿终正寝。PC是“工作者的计算机”。用户可以用它玩、编写程序、也可以将各种各样的设备连接上去。我想这就可以解释为什么有这么多电脑爱好者迷恋PC而不是长得更漂亮的Mac。一句话,你没法从使用Mac中感受乐趣。
但那时的PC在图像和声音上还依然落后。似乎PC机就是缺乏足够马力,而PC游戏的表现总是和Amiga 或家用游戏机上的游戏的良好表现相去甚远。
上帝说,要有光,于是便有了光……
在1993 年的下半年,Id Software发行了DOOM,作为Wolfenstein 3D(德军司令部3D,最早的3D 共享游戏软件之一,亦由Id 开发)的续作。在家用计算机市场,PC机俨然已成为玩游戏和编程的首选——直到现在也是。DOOM的成功证明了一点,只要足够聪明,人们可以使PC机做任何事。这点非常重要,记住,没有任何东西可以替代想像力和决心。如果你认为一件事是可能的,它就是可能的!
在DOOM热的冲击下,微软公司(Microsoft)才开始重新评价自己在游戏和游戏编程上的地位。它意识到娱乐产业的巨大,并且将只会变得更大,自己若置身这个行业有百利而无一害。于是微软制定了庞大的计划,以使自己得以在游戏业中分一杯羹。
问题在于,即使是Windows 95,实时处理视频音频的能力仍然很差,于是微软制作了一个叫做Win-G的软件,试图解决视频方面的问题。宣传的时候,Win-G被说成是最佳的游戏编程和图形子系统,而事实上它不过只是一堆用于画位图的图形调用而已。更有甚者,Win-G发布大约一年之后,Microsoft竟否认了它的存在,不骗你!
新的囊括图形、声音、输入、网络、3D 系统(95年微软收购了Rendermorphics)的软件套件的开发工作早已开始,DirectX 诞生了。像以往一样,Microsoft 发行人员宣称它将解决世界上PC 机平台上所有游戏编程的问题,还说Windows版游戏将同DOS32游戏运行得一样快,甚至更快。但事实并非如此。
DirectX最初的两个版本作为完整的软件产品来讲具有太多糟糕的缺陷,但这并非指技术而言。Microsoft只是低估了视频游戏编程的复杂性(也低估了视频游戏程序员的能力!)。等到有了DirectX 3.0,DirectX就比DOS工作得更为出色!但是那时(1996~1997),多数游戏公司还是在为DOS32进行开发,直到DirectX 5.0版本发布,人们才转而使用DirectX在Windows上进行实际的开发。
现在DirectX已经升级到9.0版本(本书包含7.0和8.0版本),它是一个难以抗拒的强大的API。没错,你应当换一种方式思考——运用COM(Component Object Model,组件对象模型),在Win32 上编程,同时不再拥有对整个计算机的全面直接控制——但生活就是如此。就像在连续剧《星际迷航》中那样,Geordi(企业号飞船轮机长鹰眼)也无法亲自控制飞船的整个计算机系统。
使用DirectX技术,你可以创建一个有4GB寻址空间(或更多)、内存线性连续的、仿DOS的虚拟机。你完全有理由觉得自己正是在DOS 环境(如果你喜欢的话)下进行编程。更为重要的是,现在你可以很快地为你的程序加入对图像和声音方面新的技术的支持。这都归功于DirectX负有远见的设计和技术。关于DirectX的话题先说到这里,因为很快你就要详细地学习它了。现在先让我们回到历史里。
最早出现的是DOOM游戏,它仅用到软件光栅技术。如图1.1 所示。看一看Rex Blade游戏的屏幕表现,它是DOOM的一个克隆版本。接下来一代3D游戏,如Quake I、Quake II 和Unreal 就有了重大的飞跃。再看看图1.2 中Unreal的游戏画面截图,这个游戏及其类似游戏所表现之好简直令人难以置信。它们都同时包含软件光栅和硬件加速的代码以最大化两者的优势。在这里我忍不住要说一句,如果让一台安装了GeForce 4 TI显卡的奔腾4代2.4GHz电脑运行Unreal II或Quake III,那效果真是棒极了。
图1.1 Rex Blade:第一代DOOM技术的产品
图1.2 Unreal:效果奇佳!
那么这将把我们带向何方?我们的技术发展越来越先进,几乎无可限量。然而,“奇迹”总会涌现。尽管Quake和Unreal这样的好游戏需要花费数年来制作,但是我相信你也能创作出同样迷人的游戏!
历史一瞥暂告一段落,下面让我们转到核心设计上来。
设计游戏
编写视频游戏最难的工作之一就是设计。的确,3D数学很难,但是策划和设计一个有趣的游戏可谓同样困难、重要。如果一款游戏的确好玩,谁又会在意游戏中是否用了最新的容积光子跟踪算法(volumetric photon traces)呢?
其实,想出一个游戏点子并不特别难。关键是细节、最终实现和视觉效果这些东西确定了游戏的归宿,是被扔到垃圾桶里还是出现在《PC Gamer》的封面?下面略述一些基本概念和我的一些经验和教训。
游戏类型
现在,游戏类型多如政治许诺(有些已经销声匿迹了),但可以将它们归入以下几个类型:
类似DOOM的第一视角类——这些游戏大部分是全3D 游戏,玩家以游戏角色的视角进行观察。DOOM、Hexen、Quake、Unreal、Duke Nukem 3D 以及Dark Forces 都是此类游戏中的佼佼者。从技术上讲,它们或许是最难开发的游戏,而且需要用到极好的技术才能在此类游戏中出类拔萃。
运动类——运动类游戏可以是2D 的,也可以是3D 的,但是近来3D 的运动游戏越来越多了。通常运动类游戏可以一个人玩,也可以多人组队一起玩。运动类游戏的图像质量比起早先已经有了很大的改进。虽然运动类游戏也许不像采用第一视角类游戏那样使人印象深刻,但是它们正在迎头赶上。运动类游戏中的人工智能水平位于所有游戏类型中最先进之列。
格斗类——格斗类游戏通常可以一人或两人玩,用侧视角度或通过一个不固定的3D 摄像机观看角色的动作。游戏人物的肖像一般为2D、2.5D(3D模型渲染而成的系列2D位图图像)或全3D 的。运行于索尼PlayStation I(PSX)游戏机上的Tekken(铁拳)将格斗类游戏在家庭游戏机市场上发扬光大。格斗类游戏在PC机上不是十分流行,这或许要归咎于游戏控制器的界面问题以及需要两人玩才更有乐趣。
街机/枪战/横版过关类——此类游戏就是类似于Asteroids、Pac-Man(吃豆)和Jazz Jackrabbit的游戏。它们基本上都是2D的老式游戏,但正在逐渐地被重新制作成3D游戏。3D版本的游戏规则和2D版本大致相同。
机械模拟类——此类游戏包含各种驾驶、飞行、赛艇、赛车、坦克战斗模拟以及读者能够想像到的任何其他种类。绝大多数此类游戏一直是3D的(虽然直到最近游戏的画面表现才算差强人意)。
生态模拟类——这个游戏类型相当新,除了现实世界本身外,再没有其他类似物了。我说的正是Populous(上帝也疯狂)、SimCity(模拟城市)、SimAnt(模拟蚂蚁)等系列游戏。此类游戏允许玩家扮演主宰各个物种的神,或是控制某种人工系统,无论是一个城市、一群蚂蚁、或是像Gazzillonaire(一个很酷的游戏)那样模拟财政金融。
战略或战争类——此类游戏已经被分为许多子类。可是我并不盲目同意那些分类方法,所以我将具有战略元素(有时是回合制的)的划入此类,如Warcraft(魔兽争霸)、Diablo(暗黑破坏神)、Final Fantasy VII(最终幻想7)等皆属此类。这里我可能有点偏颇,可是因为Diablo虽是即时游戏,它仍然含有大量的策略因素和思考。与Diablo相对照,Final Fantasy是回合制的而不是即时的。
交互式故事类——这一类别包括类似Myst(神秘岛)的游戏。基本上,这些游戏的图像都是预先渲染的,或按照“路径”设计的,通过不断地解决谜题来进行游戏。通常,因为缺少更好的定义,这些游戏不允许玩家自由地闲逛,只能和玩互动图书一样。最后,它们并不像通常意义上的游戏程序,因为它们99%都是用Director 或类似Director的工具编写的。我想要是儒勒•凡尔纳一定会批评说这太没劲了。
怀旧经典类——这类游戏总是似乎在一夜之间又冒了出来。无论何时,总是有人又想玩一玩老游戏,但希望游戏中的情节和难度较早先复杂。例如,Atari 制作了大约1000 个Tempest 的版本。我得承认它们的销售情况并不是特别好,但你一定能明白其中的道理。我很幸运地对其中一些诸如Dig Dug(淘金者)、Centipede(贪食蛇)、Frogger(青蛙人)等老游戏进行了重新制作。
纯智力谜题和棋牌类——不需要太多的介绍。此类游戏是2D、3D的或预先渲染的等等。俄罗斯方块(Tetris)、强手棋(又名大富翁,Monopoly)和麻将(Mahjong)是归于此类的众多游戏中的沧海一粟。
集思广益
一旦你决定了想要制作哪一类游戏(这是件简单的事,因为我们知道自己喜欢什么),就到了构思这个游戏的时候了。构思全靠你自己,没有一种方法可以保证你能够源源不断地想到好点子。
首先,必须想好一个你想要制作的游戏点子。你将把这个够酷同时又是可以实现的点子逐步细化,开发成一个为他人所喜欢的游戏。
当然,也可以将其他游戏作为样板或起点来得到启发。不应该依样画葫芦地复制另一个产品,不过大致模仿成功产品是可以接受的。你需要大量阅读科幻书籍和游戏杂志,观察市场上正在卖什么,观看大量的电影来寻找很酷的故事想法、游戏想法,或者只是利用电影的视觉冲击力来激励自己!
我通常所做的是和朋友一起坐坐(或者自己一个人),抛出各种想法,直到出现听上去很酷的想法为止。然后我将这个想法探讨下去,直到振振有词,或者土崩瓦解为止。这有时会让人感到灰心。你可能会对自己的念头想得过多,从而在两三个小时后放弃自己的想法。不要灰心,这是件好事。要知道,如果有个游戏想法在第二天被你想起,而你仍然喜欢它,也许机会就来了。
警告
在这里我想提醒大家一件很重要的事:不要贪多嚼不烂!我收到过上千封游戏编程新手发来的电子邮件。这些朋友一心想在很短的时间内开发出和DOOM或Quake水平相当的游戏来作为他们的处女作,这显然是不可能的。如果一个初学者能够在3~6 个月内完成一个Asteroids 的克隆版本,就是很幸运的了,因此绝不要狂热。要给自己设一个可达到的目标,尝试考虑做一些自己力所能及的事,因为到了最后往往只有你一个人在继续工作,而别人都离你而去了。还有,记得让你的处女作游戏尽量地简单。
下面让我们继续讨论其他细节。
设计文档和情节串联图板
当你已有了游戏想法的时候,就应当将它落实到纸上。现在每当我要开发一个大型游戏产品,我要求自己编写一份像样的设计文档。对于小游戏来讲,几页纸的细节也就够了。基本上设计文档是游戏开发这个冒险活动的地图。应当编入能想到的尽可能多的细节,比如关卡和游戏规则的细节。这样你可以知道正在做什么从而可以按计划工作下去。相反,如果在开发的时候还总是随意修改设计,有一天你开发出的游戏将杂乱无章。
通常,我喜欢从一个简单的故事开始写起,比如用一两页纸描写这个游戏是讲什么的、谁是主角、游戏思路是什么,以及玩家如何进行游戏。然后我决定游戏的核心细节——游戏关卡和规则,列出尽可能多的细节。完成后,我还是可以对内容进行增删,但至少我有一个可行的计划。若是有一天我想出了100 条很酷的新思路,我能够一直将它们加入文档,而不会忘记。
很明显,设计的细节完全由你决定,但是还是要把设计写下来,至少是一个游戏梗概。比方说,有可能你不喜欢连篇累牍的大型设计文档,而更喜欢一些大致的游戏关卡和规则的框图。图1.3 是为一个游戏编写的情节图板的例子。没有复杂的细节,只有方便观察和工作的草图。
图1.3 一个基本的情节串联图板
使游戏具有趣味性
游戏设计的最后部分是实际校验。作为设计者你确信自己的游戏具有趣味性并且人们将会喜欢它吗?真的不是在自欺欺人?这是一个严重的问题。市面上大约有10000 个游戏,9900 个公司在游戏行业,因此要仔细考虑。如果这个游戏令你完全为之着迷并不顾一切地想立刻玩到手,那么几乎已经大功告成。但若是设计者自己对该想法都表示冷淡,想像一下其他人会给于这个游戏什么样的评价吧!
关键之处在于要进行大量的思考和beta测试,增加各种非常酷的特性,因为最后正是这些细节令一个游戏变得生动有趣。这正如同手工制作橡木家具的精湛手艺——人们真的欣赏这些细节。
游戏的构成
现在来看一下是什么使得一个视频游戏程序与众不同。视频游戏是极其复杂的软件,事实上,它们无疑也是最难编写的程序。编写MS Word程序虽然是比编写Asteroids游戏要难,但是编写Unreal游戏则要比编写我所知道的其他任何程序都要难!
这意味着读者应当学习一种新的编程方式,这种方式更有益于实时应用程序和模拟程序,而不是你可能已经习以为常的那些单线的、事件驱动的或顺序逻辑的程序。一个视频游戏基本上是一个连续的循环,它完成逻辑动作并以30帧/秒(或更高)的刷新率在屏幕上绘制图像。这一点和电影的放映原理非常相似,除了导演是你自己。
图1.4是一个简化的游戏循环结构,下面对图中每个部分作些说明。
图1.4 一般的游戏循环结构
第一步:初始化
在这一步中,游戏程序执行标准初始化操作,如内存分配、资源采集、从磁盘载入数据等等。
第二步:进入游戏循环
在这一步中,代码运行到游戏主循环体内部。此时各种操作开始运行,运行持续到用户退出主循环为止。
第三步:获得玩家的输入信息
在这一步里,游戏玩家的输入信息被处理和/或缓存,以备下一步人工智能和游戏逻辑使用。
第四步:执行人工智能和游戏逻辑
这一部分包括游戏代码的主体部分,诸如执行人工智能、物理系统和一般游戏逻辑,其结果用于渲染下一帧图像。
第五步:渲染下一帧图像
在这一步中,玩家的输入和第四步中游戏人工智能和游戏逻辑执行的结果,被用来产生游戏的下一帧动画。这个图像通常放在不可见的缓存区(offscreen buffer area)内,因此玩家不会看到它逐渐被渲染的过程。随后该图像被迅速拷贝到显示存储器中并显示出来。
第六步:同步显示
通常由于游戏复杂程度的不同,游戏在计算机上运行的速度会时快时慢。比如,如果屏幕上有1000 个物体在动作,CPU 的负载就比只有10 个对象时重得多。从而游戏的画面刷新率(帧速率,frame rate)也会时高时低,而这是难以接受的。因此必须把游戏按照某个最大帧速率进行同步,并使用定时功能和/或等待函数来维持同步。一般来讲能达到30 帧/秒的帧速率就非常好了。
第七步:循环
这一步非常简单,只需返回到游戏循环的入口并重新执行上述全部步骤。
第八步:关闭
这一步是游戏的结束,表示将退出主程序或游戏循环,并回到操作系统。然而,在用户进行结束之前,用户必须释放所有的资源并清理系统,这些释放操作对任何其他软件也是同样要做的。
读者可能对实际游戏循环中的众多细节还有疑问。诚然,上面进行的解释有点过于简单化,但是它突出了如何进行游戏编程的重点。在大多数情况下,游戏循环是一个含有大量状态的FSM(Finite State Machine,有限状态自动机)。清单1.1 是更详细的一个版本,基本接近游戏循环的实际C/C++代码了。
程序清单1.1 一个简单的游戏事件循环
// defines for game loop states
#define GAME_INIT // the game is initializing
#define GAME_MENU // the game is in the menu mode
#define GAME_STARTING // the game is about to run
#define GAME_RUN // the game is now running
#define GAME_RESTART // the game is going to restart
#define GAME_EXIT // the game is exiting
// game globals
int game_state = GAME_INIT; // start off in this state
int error = 0; // used to send errors back to OS
// main begins here
void main()
{
// implementation of main game loop
while (game_state!=GAME_EXIT)
{
// what state is game loop in
switch(game_state)
{
case GAME_INIT: // the game is initializing
{
// allocate all memory and resources
Init();
// move to menu state
game_state = GAME_MENU;
} break;
case GAME_MENU: // the game is in the menu mode
{
// call the main menu function and let it switch states
game_state = Menu();
// note: we could force a RUN state here
} break;
case GAME_STARTING: // the game is about to run
{
// this state is optional, but usually used to
// set things up right before the game is run
// you might do a little more housekeeping here
Setup_For_Run();
// switch to run state
game_state = GAME_RUN;
} break;
case GAME_RUN: // the game is now running
{
// this section contains the entire game logic loop
// clear the display
Clear();
// get the input
Get_Input();
// perform logic and ai
Do_Logic();
// display the next frame of animation
Render_Frame();
// synchronize the display
Wait();
// the only way that state can be changed is
// thru user interaction in the
// input section or by maybe losing the game.
} break;
case GAME_RESTART: // the game is restarting
{
// this section is a cleanup state used to
// fix up any loose ends before
// running again
Fixup();
// switch states back to the menu
game_state = GAME_MENU;
} break;
case GAME_EXIT: // the game is exiting
{
// if the game is in this state then
// it's time to bail, kill everything
// and cross your fingers
Release_And_Cleanup();
// set the error word to whatever
error = 0;
// note: we don't have to switch states
// since we are already in this state
// on the next loop iteration the code
// will fall out of the main while and
// exit back to the OS
} break;
default: break;
} // end switch
} // end while
// return error code to operating system
return(error);
} // end main
尽管清单1.1 还没有任何具体功能,但研究其游戏循环有助于理解整个游戏的结构。所有游戏循环或多或少地都是按照这个结构设计的。图1.5 表示了游戏循环逻辑的状态转换图。显然,状态转换是非常连贯的。
图1.5 一个游戏循环的状态转换图
关于游戏循环和有限状态自动机的内容将在本章最后涉及FreakOut 演示游戏的章节中再进行更详细的讨论。
常规游戏编程指导规范
下面讨论一下游戏编程常用的技术,和你应该掌握并运用的基本原理。这会让游戏编程轻松一些。
一句话,视频游戏是超高性能的计算机程序。你不应当在对运行时间或内存要求特别严格的代码段中使用高层API。特别是与游戏内循环有关的代码大都需要手工编写,否则游戏多半会碰到严重的速度和性能问题。当然,这并不意味着就不能信任DirectX 等API,因为DirectX的设计目的就是兼顾高性能和“thin”的原则。但就通常来讲,应当避免频繁调用高层的函数。
除上述情况应多加注意外,在编程时还应留意下面所列的编程技巧。
技巧
不要怕使用全局变量。许多视频游戏不让对时间要求严格的函数使用参数,而是使用一些全局变量来传递参数,例如一个函数的代码如下:
void Plot(int x, int y, int color)
{
// plots a pixel on the screen
video_buffer[x + y*MEMORY_PITCH] = color;
} // end Plot
由于参数要被压栈和出栈,执行这个函数体所需的时间小于调用函数所需的时间。在这种情况下,更好的方法可以是设立一些全局变量,然后在调用前进行赋值以传递参数,如下:
int gx,gy,gz,gcolor; // define some globals
void Plot_G(void)
{
// plot a pixel using globals
video_buffer[gx + gy*MEMORY_PITCH] = gcolor;
} // end Plot_G
技巧
使用内联函数。通过使用inline指示符来完全摆脱函数调用,你甚至能够改进上一条技巧。Inline指示符指示编译器用函数体代码去替换函数调用。这样做无疑会使编译后的程序变得更大,但却有效地提高了运行速度。下面举一个例子:
inline void Plot_I(int x, int y, int color)
{
// plots a pixel on the screen
video_buffer[x + y*MEMORY_PITCH] = color;
} // end Plot_I
注意这里并没有使用全局变量,因为编辑器有效地执行了同类型的数据别名。但是全局变量还是很有用的,尤其是如果函数调用时只有一至两个参数改变了值的情况——其余旧的值无须重新加载就可被使用。
技巧
尽量使用32 位变量而不用8 位变量或16 变量。Pentium以及更新的中央处理器都是全32 位的,这就意味着它们并不喜欢8 位或16 位的数据字。实际上,由于高速缓存和其他相关内存储器的寻址变得较不规则,较小的数据可能会使速度下降。例如,你定义了一个如下所示的结构类型:
struct CPOINT
{
short x,y;
unsigned char c;
} // end CPOINT
注意,定义这个结构看上去不错,但实际并非如此!首先,结构本身是一个5字节长的结构—— (2*sizeof(short) + sizeof(char)) = 5字节。这太糟了,由于没有注意字节对齐,内存寻址的时候会出大问题。更好的结构形式如下:
struct CPOINT
{
int x,y;
int c;
} // end CPOINT
C++
提示:C++中的结构(Struct)非常像类(Class),除了结构默认的可访问性(Visibility)是PUBLIC以外。
这个新结构要好得多。首先,所有结构成员都有相同的大小——sizeof(int) = 4字节。因此,仅用一个指针就可以通过递增DWORD(双字,2字节)的边界访问任一结构成员。这个新结构的大小是(3*sizeof(int)) = 12字节,是4 的倍数,或者在DWORD的边界上。这将明显地提升性能。
实际上,如果读者真想稳妥的话,可以适当地填充一下所有的结构,使其大小都成为32字节的倍数。32字节是Pentium家族中央处理器上标准内部缓存的宽度,因而这是一个最佳长度。可以通过在结构里填入无用的变量或者使用编译器指示符(最简单的方法)来满足这个要求。诚然,进行填充会浪费相当多的内存,但是较之速度的提高来说往往是值得的。
技巧
注释你的代码。游戏程序员不注释代码是出了名的。不要再犯同样的错误,为了得到整洁、有良好注释的代码,一点点额外的打字绝对是值得的。
技巧
以类似RISC(精简指令集计算机)的方式来编程。换句话说,尽量简化你的代码,而不是使它更复杂。Pentium级处理器特别喜欢简单指令,而不是复杂的指令。你的程序可以长些,但应尽量使用简单指令,使程序相对于编译器来说更加简单些。例如,不要编写这样的程序:
if ((x+=(2*buffer[index++]))>10)
{
// do work
} // end if
而应该这样写:
x+=(2*buffer[index]);
index++;
if (x > 10)
{
// do work
} // end if
按照这种方式来编写代码有两个原因。首先,它允许调试程序在代码各部分之间放置断点;第二,这将更易于编译器向Pentium 处理器传送简单指令,这样将使处理器使用更多的执行单元并行地处理更多的代码。复杂的代码在这方面就比较糟糕!
技巧
使用二进制移位运算来进行乘数是2的幂的简单整数乘法。因为所有的数据在计算机中都以二进制存储,把一组位元向左或右移动就分别等价于乘法和除法运算。例如:
int y_pos = 10;
// multiply y_pos by 64
y_pos = (y_pos << 6); // 2^6 = 64
相似的有:
// to divide y_pos by 8
y_pos = (y_pos >> 3); // 1/2^3 = 1/8
在本书关于优化的那章里,你将会发现更多类似的技巧。
技巧
设计高效率的算法。没有任何一种汇编语言能使复杂度为O(n2)的算法运行得更快。更好的做法是使用清楚、高效率的算法而不是蛮力和穷举型的算法。
技巧
不要在编程过程中优化代码。这通常只是浪费时间。建议你等到主要的代码块或整个程序都完成后才开始着手进行繁重的优化工作。这样做最终会节省你的时间,因为你不必对一些含义模糊的代码进行不必要的优化。当游戏基本完成时,才到了性能测试(Profiling)和查找需要优化的问题的时候。另一方面,程序代码要注意错落有致,不要写得杂乱无章。
技巧
不要为简单的对象定义太多复杂的数据结构。链表结构很好用,但这并不意味着当你所需要的其实是大约有256个元素的固定数组的时候,你也该使用链表,只须为其静态地分配内存即可。视频游戏编程中90%的部分都是数据操作。所以数据应尽可能简单、可见,以便能够迅速地存取它、随意操作它。应当确保你的数据结构适合你所真正要解决的问题,不要杀鸡用牛刀。
技巧
使用C++应谨慎。如果你是位经验丰富的C++专家,只管去做你想做的事,但是不要疯狂地写过多的class,也不要把任何东西都重载(overload)。说到底,简单而且直观的代码是最好的程序,也最容易调试。我个人就不想在游戏代码中看到多重继承!
技巧
如果你知道自己的爱车将要开上一条坎坷的石子路,最好的做法是停下来,掉头然后绕路而行。我见过许多游戏程序员沿着一条很差的编程路线走着,直到在糟糕的代码堆中葬送自己。能够意识到自己所犯的错误并重新编写500行代码,比写出一个总是令人不快的代码结构要好得多。因此,如果在工作中发现问题,那就要重新评估并确保你节约的时间是值得的。
技巧
经常备份你的工作。在编写游戏代码时,需要相当频繁地锁定代码库中的代码。重写一个排序算法还比较容易,但是要重写角色AI或者重写碰撞检测则是困难得多了。
技巧
在开始你的游戏项目之前,应当进行一下组织工作。使用合理的文件名和目录名,提出一种一致的变量命名约定,尽量对图形和声音数据使用分开的目录,而不是将所有东西都一股脑儿放在同一个目录中。
使用工具
过去编写视频游戏通常只不过需要一个文本编辑器,或许还有一个自制的绘图程序。但是现在事情就变得复杂一点了。至少,你需要一个C/C++编译器、一个2D 绘图程序和一个声音处理程序。此外,如果你想编写一个3D 游戏的话,可能还需要一个3D造型软件,而如果读者想使用任何MIDI设备的话,还需要准备一个音乐排序程序。
让我们来浏览一下目前比较流行的产品及其功能。
C/C++编译器
对于在Windows 9X/NT平台上的开发来说,简直没有比MS VC++ 6.0+更好的编译器了。它可以做任何事,甚至更多。它能产生最快的.EXE可执行代码。Borland 编译器也可以工作得很好(并且它要便宜得多),但是它的特性设置较少。无论是MS还是Borland,你不一定需要上述任何一种编译器的完整版本,一个能够生成Win32 平台下的.EXE 文件的学生版本就已经足够了。
2D 艺术软件
你可以买到图形软件、制图软件和图像处理软件。你可使用图形软件逐个像素地绘制和处理图片。就我所知,JASC公司的Paint Shop Pro是性价比极佳的图像软件包。ProCreate Painter(就是以前的Fractal Design Painter)也很好,但是它更适合传统艺术家使用,而且它很昂贵。我个人喜欢使用Corel Photo-Paint,但是对于新手的需要来讲,它的功能的确有点太多了。
另一方面,制图软件允许读者创建主要由曲线、直线和基本2D几何形状组成的图像。游戏开发不常用到这类软件,但如果你需要,Adobe Illustrator是一个很好的选择。
2D 艺术软件中的最后一类用于图像处理。这些程序多用于产品的后期制作,而不是前期的艺术创作。Adobe Photoshop 是大多数人喜欢的软件,但是我认为Corel Photo-Paint比较好些。所谓仁者见仁,智者见智吧。
声音处理软件
目前用于游戏的所有的声音效果(SFX)90%都是数码样本,采用这种类型的声音数据来工作,读者应当需要一个数码声音处理软件。这一类中最好的程序是Sound Forge Xp。它有相当复杂的声音处理功能,使用起来也很简单。
3D 造型软件
这可是挑战经济实力的软件。3D造型软件可能动辄标价上万美元。但是最近也有不少低价位的3D造型软件上市,它们的功能也足够强大可用于制作一部影片。我主要使用简单到中等复杂程度的3D造型和动画软件——Caligari TrueSpace。在相应的价位上,这是最好的3D造型软件,只需要几百美金,并且拥有最好的界面。
如果你想要的是功能更为强大并追求画面要像照相般绝对写实,3D Studio Max可以帮到你。但是它的价格大约是2500 美金,因此应当认真考虑一下。然而如果我们使用这些造型软件只为创建3D造型(Mesh),而非画面渲染,那么也就不需要其他高级功能了。这样TureSpace 就足以应付。
音乐和MIDI排序程序
目前的游戏中有两类音乐:纯数字式(像CD 一样)和MIDI(Musical Instrument Digital Interface,乐器数字接口)式,MIDI是一种将音符记录而成的合成音效。如果想制作MIDI信息和曲子,还需要一个排序软件(又名音序器软件)。性价比最佳的一个软件包是Cakewalk,因此,如果你打算录制和制作MIDI 音乐的话,建议最好去了解一下这个软件。在涉及DirectMusic内容的第10章“用DirectSound和DirectMusic演奏乐曲”中,我们将对MIDI 数据再作探讨。
技巧
好消息,一些我在上文中提及的软件厂商准许我在本书附带的CD上放了它们软件的共享版或试用版,建议你一定要试一试!
从准备到完成——使用编译器
学习Windows 游戏编程的过程的一件最令人灰心丧气的事是学习使用编译器。常见的是,初学者对于开始编写游戏程序是如此激动,以至于一下就投入到IDE(集成开发环境)中去并尝试进行编译,然后就出现了成千上万条编译和连接错误!为了避免这个问题,让我们首先回顾一下有关编译器的一些基本概念。
1. 请务必完整地阅读编译器附带的使用说明!
2. 务必在系统中安装DirectX SDK(DirectX Software Development Kit,DirectX软件开发工具包)。你所要做的就是在光盘上找到<DirectX SDK>目录,阅读README.TXT文件,并按说明进行操作(实际上只不过是“双击DirectX SDK里的INSTALL.EXE程序”)。
3. 我们要开发的是Win32 .EXE 程序,而不是.DLL 文件或ActiveX组件等等。因此如果想要顺利编译通过,需要做的第一件事情是使用编译器创建一个新的工程或工作区,然后将目标输出文件设定为Win32环境的.EXE。使用VC++ 6.0 编译器进行这一步的工作如图1.6 所示。
图1.6 使用Visual C++ 6.0 创建一个Win32的 .EXE文件
4. 从主菜单或工程本身使用Add Files命令向工程添加源代码文件。对于使用VC++ 6.0 编译器而言,其操作过程如图1.7 所示。
图1.7 利用VC++ 6.0 向一个工程添加文件
5. 从有关DirectX的章节开始,必须在项目里包含如图1.8所示的大多数DirectX COM接口库文件。
o DDRAW.LIB
o DSOUND.LIB
o DINPUT.LIB
o DINPUT8.LIB
o DSETUP.LIB*
注意:除非你正使用DirectSetup,否则你不需要DSETU.LIB。
图1.8 创建Win32 .EXE 应用程序所需要的资源
o
这些DirectX .LIB 文件位于所安装的DirectX SDK目录下的LIB子目录下。必须将这些.LIB 库文件也添加到读者的工程或工作区中。不可只添加搜索路径,因为搜索引擎可能会在编译器自带的库文件中发现旧的DirectX 3.0的.LIB文件。如果是这样做的话,你将需要将Windows多媒体扩展库——WINMM.LIB 加入到工程中去。该文件位于编译器安装目录下的LIB子目录。
6. 准备完毕,开始编译你的程序吧。
警告
如果你是Borland 用户,DirectX SDK中有一个单独的目录存放Borland库文件。因此要确保将这些.LIB文件而不是目录树中上一级的MS兼容文件添加到工程中。
如果你对此仍有疑问,请不必着急。在本书中讨论Windows编程和首次接触DirectX时还将多次温习这些步骤。