第一章 无尽之旅
Windows编程是一场由来已久并还在进行着的战争。开始时,游戏程序拒绝Windows平
台,但正如Borg所言:“反对无效......”,我也赞同这一观点。本章就Windows的发展进
行一下回顾。
·游戏的历史
·游戏类型
·游戏编程的基本要素
·使用工具
·一个游戏的例子:FreakOut
历史一瞥
在60年代的某个时候,第一台台式计算机问世,当时运行在Unix机器上的Core Wars,
是最早的计算机游戏之一。当70年代的黄金岁月到来时,在全世界的台式计算机和小型计
算机上流行着文本游戏和粗糙的图片游戏。有趣的是从那以后,大多数游戏开始网络化。
我是说,90%的游戏程序MUD(Multi-User Dungeons)或类似的模拟游戏,如Star Trek和战
争模拟游戏。但是,直到精彩的Pong游戏问世,大众才真正品尝到计算机游戏的乐趣。由
Nolan Busnell设计的Atari计算机的问世,使单人的视觉游戏一夜之间充斥市场。
而后,在1976~1978年间,TRS-80、Apple、Atari 800等型计算机开始冲击市场,这
些是消费者买得起的第一代计算机。当然,读者也可以买类似Altair 8000型的组装机,但
是又有谁乐意进行它们的组装呢?无论如何,这些计算机有各自的优缺点,Atari 800是当
时功能最强大的计算机(我也保证,我可以写一个Wolfenstein程序在它上面运行)。TRS-8
0最商业化,而Apple机的市场最好。
慢慢地,游戏开始冲击计算机相关产业方面的市场,一夜之间,产生了许多年轻的百
万富翁。制作者只需要一个类似“月亮领地”或Pong类型的好游戏,就可以突然致富!从
那时,游戏开始像真正的计算机游戏,而且只有百十人知道如何编写游戏,当时绝对没有
这类的指导书,只是有人不时半地下地出版一些50~100页的有许多令人费解之处的小册子
,另外也曾在Byte杂志上有过文章,但是,大多时候,读者必须靠自己。
80年代是游戏升温的年代。第一代16位计算机问世,如:IBM PC及其兼容机、Mac、A
tari ST、Amiga 500等等。这是游戏变得好看的时候,甚至有些3D游戏如Wing Commander
、Fight Simulator等开始出现在市场上。但是,PC机肯定仍然落后于游戏机。在1985年以
前,Amiga 500和Atair ST作为最终游戏机占有绝对的支配地位。但是PC机由于低廉的价格
和其商业用途开始大众化,并且最终必须将是具有最大市场基础的计算机——不考虑它的
技术和质量——一统江湖。
在90年代初期,IMB PC及其兼容机是主流。随着微软的Windows 3.0的发行,Apple M
acintosh寿终正寝。PC是“工作者的计算机”。读者可以真正在上面进行游戏、编程、开
机并连接其他东西。我想这就是为什么会有这么多人沉醉于PC而不是Mac的原因。一句话,
读者不可能从Mac中寻求乐趣。
但是PC在图像或声音上还依然落后,PC机看起来像没有足够的马力使游戏如同在Amig
a或者游戏控制台上表现得那样好。
而后曙光终于来临......
在1993年后期,Id Softwere发行了DOOM作为Wolfenstein 3D(是由Id编写的第一批3D
游戏之一)的换代产品。PC机开始成为家用微机市场玩游戏或编程的选择,直到现在也是。
DOOM表明,只要读者有足够的才智,就可以在微机上面作任何事情。这点非常重要,记住
,没有任何东西可以替代想像力和决心,只要读者认为可能,它就能实现。
在DOOM的疯狂冲击下,Microsoft开始重新估计它在游戏和游戏编程上的地位,它意识
到娱乐产业的巨大,并且只会更大。它开始制定巨大的计划涉足游戏,以使它得以在这个
产业中分一杯羹。
问题是即使Windows 95的实时视听能力也表现得很恶劣,于是微软发行了Win-G以解决
视觉方面的问题,Win-G被宣布为游戏编程和图形子系统的最终解决方案,而实际上它不过
只是用位图画的一些图像而已。一年之后Microsoft却又否认它的存在。——这可实有其事
!
但是,新的图形、声音、输入、网络、3D系统的工作已经开始,DirectX诞生了。像以
往一样,Microsoft发行人员宣称它将解决世界上PC机平台上所有游戏编程的问题,并且在
Windows上运行的游戏将同DOS32上的游戏一样快,甚至更快。但事实上并非如此。
DirectX开始的两个版本在实际应用中是失败的,但这并非指技术而言,Microsoft低
估了视觉游戏编程的复杂性及视觉游戏程序员能力。但是到了DirectX3.0,DirectX就开始
工作得比DOS出色!但是那时(1996~1997)许多公司仍然采用DOS32平台进行游戏编程,直
到DirectX版本5.0发行,人们才转而使用DirectX。
现在DirectX已经升级到8.0版本(本书包含7.0版本),它是一类极强大的API。的确,
读者应当改变一下想法——用COM(Component Object Model)在Win32上编程,不再对整个
计算机进行全部控制——但生活就是如此。
利用DirectX技术,读者可以创建一个4GB地址(或更多)、内存线性连续的、仿DOS的
虚拟机,读者可以像在DOS环境(如果读者喜欢的话)下那样来编程。更为重要的是,现在
读者可以即时操纵图像和声音技术的每一个新的片段。这都归功于DirectX远见卓识的设计
和技术。总之,关于DirectX的话题已经足够多了,不久读者就会接触到整个处理所带来的
效果。
首先出现的是DOOM游戏,它仅用到软件光栅技术,如图1.1所示。看一看Rex Blade游
戏的屏幕表现,它是DOOM游戏的一个克隆版本。下一代的3D游戏,如QuakeⅠ、QuakeⅡ和
Unreal就有了巨大的飞跃。再看一看图1.2中Unreal游戏的屏幕表现。这个游戏及其类似的
游戏,其表现简直令人难以置信。所有这类游戏都采用软件光栅和硬件加速代码来获得最
好的游戏表现。提请读者注意的是,Unreal和QuakeⅡ游戏至少要运行在配置VoodooⅡ加速
卡的PentiunⅡ400MHz机器上才能够达到这么眩目的效果。那么这将把我们带向何方?技术
的发展越来越先进,以至空间已成为制约因素。然而,“奇迹”总会涌现。即使诸如Quak
e和Unreal这类的游戏需要花费数年才能制作完成,我们仍然相信读者也能够创作出有如此
吸引力的游戏。
历史的回顾到此为止,下面让我们转到游戏软件设计的核心上来。
设计游戏
编写视频游戏最难的一个工作是设计。的确,3D运算很难,但是策划一个有趣的游戏
并且进行设计可谓同样困难、重要。谁会关心游戏中是否采用最新的光子跟踪技术呢?
现在,构思一个游戏并不难。难的是它的细节体现、最终实现和相似物体在视觉形象
上的差别表现。因此下面概述一下一些基本概念和本人在游戏设计工作中的一些心得、经
验。
游戏类型
现在,游戏类型多如政治许诺(只说不做),但可以将它们归入以下几个类型:
类似DOOM的单人游戏——这些游戏大部分是全3D游戏,你将以角色的身份置身游戏中
。DOOM、Hexen、Quake、Unreal、Duke Nukem 3D以及Dark Forces都属于这一类型的游戏
。
从技术上讲,它们或许是最难开发的游戏 ,这类游戏要求采用边缘切换技术。
运行游戏——运行游戏可以是2D的,也可以是3D的,但是近来3D的运行游戏越来越多
了。运动游戏可以一个人玩。也可以多人玩。运动游戏的图像已经有了很大的改进。也许
运动游戏不像单人游戏给人印象深刻,但是它们也很吸引人。当然运行游戏中的人工智能
水平却是所有游戏类别中最先进的。
格斗游戏——
街道/枪战/平面游戏——
机械模拟游戏——
生态模拟游戏——
攻略或战争游戏——
交互式故事——
重新流行的游戏——
纯智力和棋牌游戏——
集思广益
一旦读者决定了制作哪一种游戏(这是件简单的事,因为我们知道自己喜欢什么),
就到了构思这个游戏的时候了。这完全由读者自己决定,策划一个好游戏并没有一成不变
的思维定式。
首先,必须想出一个读者喜欢制作并将它开发成为听上去很酷的、可以实现的,并且
其他人能够喜欢的游戏。
当然,读者可以将其他游戏作为模板或出发点来得到启发。不要简单地复制其他产品
,但是大致模仿已成功的产品也是可以的。并且,大量阅读科幻书籍和游戏杂志,观察正
在卖什么观看大量的电影将有助于产生很酷的故事想法、游戏想法或视频动作。
我通常所做的是和朋友一起坐坐(或者是自己),抛出各种想法,直到出现听上去很
酷的想法。然后,开发这个想法,直到似是而非,或者土崩瓦解为止。这令人非常心灰意
冷。读者可能会对所有的想法厌烦,然后在两三个小时后放弃。不要灰心,这是件好事,
如果一个游戏想法能存活一夜,第二天想起来还喜欢它的话,也许运气就来了。
警告:在这里我想给读者一个告诫:不要贪多嚼不烂!我已经收到上千封游戏编程新
手的电子邮件。这些读者一心想在很短的时间内开发出诸如DOOM或Quake这类高水平的游戏
来作为他们的处女作,显然这是不可能的。如果读者在3~6个月内能够完成一个Asteroid
s的复制品,就是很幸运的了,因此不要狂热。设定一个可行的目标,尝试考虑一些自己能
做的事,因为最后可能只有自己在继续工作,别人都离你而去了。另外让处女作游戏的想
法简单些。
下面我们进行一些细节工作。
设计文档和情节图板
一旦读者有了一个游戏想法,就应当将它落实到纸上。现在,当我进行一个大游戏产
品时,需要自己编写一个实际的设计文档,但是对于少数游戏来讲,应当做几页的细节。
首先一个设计文档是一个游戏的路标或框架。应当编写能想到的、尽可能多的游戏、游戏
等级和游戏规则的细节。这样能知道正在做什么以及能按计划做下去。相反,如果想法一
直在变化,那么所做的游戏不连贯。
通常,我喜欢将一个简单的故事写下,开始的一或两页可能描写这个游戏是什么游戏
,谁是主角、游戏思路是什么、最后游戏如何攻关。然后我决定游戏的核心细节——游戏
等级和规则,列出尽可能多的细节。完成后,就一直进行添加或删除,但至少我有一个工
作计划。如果想出了100条很酷的新思路,我能够一直补充它们,而不会忘记。
很明显,细节的数量由读者决定,但是要写下一些东西,至少是一个游戏梗概。例如
,可能读者不会连续地考虑一个完整的设计文档,而是一些大致的游戏等级和规则的框图
。图1.3是为一个游戏编写的情节图板的例子。没有复杂的细节,只有方便观察和工作的草
图。
使游戏具有趣味性
游戏设计的最后部分是实际校验。读者确信游戏具有趣味性并且人们喜欢它吗?还是
在自欺欺人?这是一个严重的问题。大约有10000个游戏被搁置,9900个公司歇业,因此要
仔细考虑。如果读者完全为之着迷并不顾一切的想玩这个游戏的话,那么已经大功告成。
但是如果读者作为一个设计者对该想法冷淡的话,想像一下如何令其他人来对它感兴趣呢
!
这里的关键是进行大量的游戏策划和beta测试,增加各种类型的非常酷的特性,因为
这才是令游戏生动有趣的关键所在。这就如同橡木家具上的精美的手工艺品。人们喜欢这
些游戏细节。
游戏的构成
现在来看一下一个视频游戏程序和其他各种程序的区别。视频游戏是一种极其复杂的
软件,毫无疑问它们也是最难编写的程序。显然,编写MS Word程序要比Asteroids游戏难
一点,但是编写Unreal游戏则要比我所能想像得到的其他任何程序都要难。
这就表示读者应当学习一种新的编程方式,这种方式更有益于实时应用和模拟,而不
是读者经常使用的单行的、事件驱动的或顺序逻辑的程序,一个视频游戏基本上是一个连
续的循环,它完成逻辑动作,并在屏幕上产生一个图像,通常是每秒钟30幅图或更多,这
和电影的放映非常相似。只是读者要按自己的思路创建这个电影。
下面让我们从观察如图1.4所示的简化游戏循环开始,下面对图中每个部分作些说明。
第一步:初始化
在这一步中,游戏程序运行的初始化操作和其他程序一样,如内存单元配置、资源采
集、从磁盘装载数据等等。
第二步:进行游戏循环
在这一步中,代码运行进入游戏主循环,此时各种游戏动作和情节开始运行,直至用
户退出游戏主循环。
第三步:获得玩家的输入信息
在这一步中,处理游戏玩家的输入信息并将其储存到缓存以备下一步人工智能和游戏
逻辑使用。
第四步:执行人工智能和游戏逻辑
这部分包括游戏代码的主体部分,执行人工智能、物理系统和常规的游戏逻辑,其结
果用于产生下一帧屏幕图像。
第五步:渲染下一帧图像
本步中,玩家输入和游戏人工智能和逻辑执行的结果,用来产生游戏的下一帧动画。
这个图像通常放在后备缓存区内,因此无法看到它被渲染的过程。随后该图像被迅速拷贝
到显示区中。
第六步:同步显示
许多计算机会因为游戏复杂程度的不同,游戏的速度会加快或减慢。例如,如果屏幕
上有1000个对象在运行,CPU的负载就比只有10个对象时重得多,因而游戏画面刷新速度也
会有所改变,这是不允许的。因此必须确保游戏和最大帧速同步并使用定时器和/或等待函
数来维持同步。一般认为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 operation system
return(error);
}//end main
尽管清单1.1是一个没有任何功能的程序,但通过学习其游戏循环的结构可以获得很好
的思路。所有的游戏循环大都按照这个结构的一种形式或另一种形式进行设计。图1.5表示
了游戏循环逻辑的状态转换图。如读者所见,状态转换是非常连贯的。
关于游戏循环和有限态计算机(FSM)的内容将在本章最后涉及FreakOut演示游戏的章节
中再进行更详细的讨论。
常规游戏编程指导
下面讨论一下读者所关心,也是游戏编程常用的技术和基本原理,这有利于简化游戏
编程的复杂程度。
首先,视频游戏是运行于超高性能计算机上的游戏程序。对于时间或内存要求特别严
格的代码部分不能使用高级API来编程,和游戏代码内部盾环有关的部分,大都需自已手工
编写,否则游戏将会碰到严重的速度和性能问题。当然,这并不意味着就不能信任Direct
X等API编程工具,因为DirectX以高性能和心可能“瘦”的方式编写。但在通常情况下,要
避免高级的函数调用。
除上述情况应多加注意外,在编程时还应留意下面的编程技巧。
技巧:不要怕使用全局变量,许多视频游戏不使用大量的带有形参的、与时间相关的
函数,而是使用一个全局变量来代替,例如一个函数的代码如下:
void Plot(int x,int y,int color)
{
//在屏幕上画一个点像素
video_buffer[x + y * MEMORY_PITCH] = color;
}//结束Plot
函数体运行的时间小于函数调用所需的时间。这是由于参数压入和弹出堆栈造成的。
在这种情况下,更好的方法可能是创建一个全局变量,然后在调用前进行赋值,像下面一
样:
int gx,gy,gz,gcolor;//定义一些全局变量
void Plot_G(void)
{
//使用全局变量来画一个点像素
video_buffer(gx + gy * MEMORY_PITCH] = gcolor;
}//结束Plot_G
技巧:使用内联功能。通过使用内联指令来完全摆脱调用功能甚至能够改善上面的技
巧。内联指令不调用函数,而指示编译器将被调用函数代码放在需要调用该函数的最佳位
置,这样做会使程序变得更大,但却提高了运行速度。下面是一个实例。
inline void Plot_I (int x,int y,int color)
{
//在屏幕上画一个点像素
video_buffer[x + y * MEMORY_PITCH] = color;
}//结束Plot_I
注意:这里并没有使用全局变量,因为编辑器有效运行了相同类型数据的别名,但是
全局变量迟早会派上用场,如果在函数调用时,一个或两个形参已改变,由于没有重新加
载,所以旧的参数值有可能仍被使用。
技巧:尽量使用32位变量而不用8位变量或16位变量,Pentium和之后的处理器全部都
是32位的,这就意味着它们并不喜欢8位或16位的数据字符,实际上,更小的数据可能会由
于超高速缓存和其他相关的内存寻址异常而使速度下降,例如,读者可能创建一个如下所
示的结构:
struct CPOINT
{
short x,y;
unsigned char c;
}//结束CPINT
尽管这个结构看上去很好,但实际并非如此!首先,结构本身目前是一个5字符长的结
构——2个short+1个char=5。这实际上是一个很差的设计,这将导致内存地址崩溃。更好
的结构形式如下:
struct CPOINT
{
int x,y;
int c;
}//结束CPINT
C++ 提示:除了默认的公共可见性外,C++中的结构更像是“类”。
这种新结构要更好一些。首先,所有的成员都是相同尺寸——也就是说整数的大小为
4字节。因此,单个指针可以通过递增DWORD(双字节)的边界访问任何单元。当然,这种新
结构的大小 就是3个整数长,即12字节,至少是4的倍数,或者在DWORD边界上。这样将明
显地提升性能。
实际上,如果读者真想稳妥地话,应当将所有的结构都变为32字节的倍数。由于Pent
ium家庭处理器芯片上标准缓存线长度是32倍数,因而这是一个最佳长度。可以通过人工虚
设单元或者使用编辑指令(最简单的方法)来满足这个要求。当然,这可能会浪费大量的内
存,但是为了提高速度这是值得的。
技巧:注释你的代码,游戏程序员不注释代码是出了名的,不要犯相同的错误。用额
外的输入换取整洁,注释良好的代码是值得的。
技巧:程序应以类似RISC(精简指令系统计算机)的形式来编写。换句话说,尽量简化
你的代码,而不是使它更复杂。Pentium和PentiumⅡ处理器特别喜欢简单指令,而不是复
杂的指令,你的程序可以长些,但应尽量使用简单指令,使程序相对于编辑器来说更加简
单些。例如,不要编写类似下面的程序:
if((x+=(2*buffer[index++])>10)
{
//进行工作
}//结束
应这样做
x+=(2*buffer[index]);
index++
if(x>10)
{
//进行工作
}//结束if
技巧:按照这种方式来编写代码有两个原因,首先,它允许调试程序在代码各部分之
间放置断点;第二,这将更易于编译器向Pentium处理器传送简单指令,这样将使处理器使
用更多的执行单元并行地处理代码。复杂的代码就比较糟糕。
对于简单的、是2的倍数的整数乘法运算,应使用二进制移位运算。因为所有的数据在
计算机中都以二进制存储,位组合向左或右移动就等同于乘法和除法运算。例如:
int y_pos = 10
//将y_pos乘以64
y_pos = (y_pos << 6);//2^6=64
相似的有
{
//将y_pos除以8
y_pos = (y_pos >> 3);//1/2^3=1/8
当读者接触到优化那一章时,将会发现更多的、类似的技巧。哈哈,酷吧!
技巧:编写高效的算法。世界上所有的汇编语言都不会将n^2算法运行得更快些,更好
的方法是使用整齐、高效的算法而不是蛮干。
技巧:不要在编程过程中优化代码。这通常会浪费时间。等到完成主要的代码块或整
个程序后才开始进行繁重的的优化工作。这样可以节省你的时间,因为你必须处理一些模
糊的代码或不必要的优化。当游戏编程完成时,才到了剖析代码、查找问题以优化程序的
时间。另一方面,程序要注意错落有致,不要杂乱无章。
技巧:不要为简单的对象编写大量的复杂的数据结构,仅仅因为连接的清单非常酷并
不意味着必须使用它们,对于静态数组而言,其元素一般为256个,的以只需为之静态分配
内存并进行相应的处理即可。视频游戏编程90%都是数据操作,游戏程序的数据应尽可能简
单、可见,以便能够迅速地存取它、随意操作它或进行其他处理,确保你的数据结构按照
这一原则进行处理。
技巧:使用C++应谨慎。如果读者是位老练的高手,继续前进去做你喜欢的事,但是不
要去疯狂追求类,或使游戏程序过于复杂以至于超出一般计算机的承受能力,简单、直观
的代码是最好的程序,也最容易调试。我从来都不想看多重的隶属关系。
技巧:如果感到前路荆棘丛生,那就停下来,回头然后绕路而行,我见过许多游戏程
序员开始于一条很差的编程路线,然后葬送自己。能够意识到自己所犯的错误,然后重新
编写500行的代码要比得到一个不是期望的代码结构要好得多,因此,如果在工作中发现问
题,那就要重新评估并确保它是值得花时间补救的。
技巧:经常备份你的工作。在编写游戏代码时,需要相当频繁地锁定系统。重新做一
个排序算法比较容易,但是要为一个新角色或碰撞检测重新编写AI则是另一回事。
技巧:在开始你的游戏项目之前,进行一下组织工作,使用合理的文件名和目录名,
提出一种一致的变量命名约定,尽量对图形和声音数据使用分开的目录,而不是将其全部
都放置在一个目录中。
使用工具
过去编写视频游戏通常只不过需要一个文本编辑器和一个简略的自制图形程序。但是
现在事情就变得复杂一点了,读者至少需要一个C/C++编译器、一个2D的图形程序和一个声
音处理程序。此外,如果读者想编写一个3D游戏的话,读者可能还需要一个3D的模型,而
如果读者想使用任何MIDI设备的话,还要有一个音乐排序程序。
让我们来浏览一下目前流行的产品及其功用。
C/C++编译器
对于Windows 9X/NT的研制来讲,简直没有比MS VC++5.0+更好的编译器了。它可以做
任何读者想做的事,甚至更多。所产生的.EXE文件是最快的有效代码。Borland编译器也可
以工作得很好(并且它要便宜得多),但是它的特性设置较少。在任何情况下,读者不一定
需要上述任何一种编译器增强版本,一个能够产生Win32平台下的.EXE文件的学生版本就已
经足够了。
2D艺术软件
这儿读者可以得到图形程序、画图程序和图像处理程序。图形程序主要允许读者以原
色一个像素、一个像素地绘制和变换图形。直到现在为止,JASC公司的Paint Shop Pro5.
0+还是性价比最佳的软件包。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网络,而不是渲染,那么所有的其他修饰也就不需要了。这样
tureSpace就足以应付。
音乐和MIDI排序程序
目前的游戏中有两类音乐:纯数字式(像CD一样)和MIDI(乐器数字界面)式,MIDI是一
种基于人工记录数据的合成音效。如果想制作MIDI信息和歌曲,读者还需要一个排序软件
包。一种性价比最佳的软件包是Cakewalk,因此,如果读者打算记录和制作MIDI音乐的话
,建议最好去了解一下这个软件。在涉及DirectMusic内容的第10章“用DirectSound和Di
rectMusic演奏乐曲中”中,我们将对MIDI数据再作探讨。
技巧:现在是最酷的部分......许多软件制造商将许我在CD上列出了它们软件的共享
版或评测版,赶快去体验这些软件吧!
从准备到完成——使用编译器
学习Windows游戏编程的一件最容易令人灰心丧气的事是学习如何使用编译器。大多数
情况下,读者对开始编写游戏程序如此激动,以至于全身心地投入到IDC(集成开发环境)
中去尝试进行编译,然后就出现了一百万条编译和软件错误!为了有助于解决这个问题,
让我们首先回顾一下有关编译器的一些基本概念。
0. 请读者务必阅读全部编译器指令!
1. 必须在你的系统上安装DirectX SDK(软件开发工具包)。你所要做的就是在CD上查
找到上当,阅读README.TXT文件,并按说明进行操作(实际上只不过是“单
击DiretC SDK INSTALL.EXE程序”)。
2. 我们要制作的是Win32.EXE程序,而不是.DLL文件和ActiveX组件等等。因此如果
读者想编译的话,需要做的第一件事情是使用编译器创建一个新的工程或工作区,然后将
目标输出文件设定为Win32.EXE。使用VC++5.0编译器进行这一步的工作如图1.6所示。
3. 应用添加文件(ADD Files)命令从主菜单或工程节点本身向工程添加源文件。对于
使用VC++5.0编译器而言。其操作过程如图1.7所示。
4. 从接触到DirectX一章时起,就必须要包含下面列出的和图1.8所表示的DirectX
COM界面库文件。
·DDRAW.LIB
·DSOUND.LIB
·DSOUND3D.LIB
·DINPUT.LIB
·DMUSIC.LIB
·DSETUP.LIB
这些DirectX.LIB文件位于所安装的DirectX SDK根目录下的目录下。必须将这些
.LIB库文件添加到读者的工程或工作区中。读者不可能只添加搜索路径,因为搜索引擎会
发现编译器本身安装的库文件和旧的DirectX3.0的.LIB文件。如果是这样做的话,读者可
能不得不将Windows多媒体扩展库文件——WINMM.LIB加入到工程中去。该文件位于读者的
编译器安装目录下的目录下。
5. 准备编译你的程序
警告:如果读者是Borland用户,在DirectX软件开发工具包中有一个单独的Borland库
文件目录,因此要确保将这些.LIB文件而不目录树中上一级的MS兼容文件添加到工程中。
如果读者仍然对此有疑问的话,请不必着急。在本书中,当讨论Windows编程和首次接
触DirectX时还要多次重复这些步骤。
实例:FreakOut
在沉溺于所讨论的有关Windows、DirectX和3D图形之前,应当轶一下,先显示一个完
整的游戏——虽然简单了一点,但毫无疑问是一个完整的游戏。读者可以看到一个起初的
游戏循环和一些图形的调用以及一些编码工作。怎么样?跟我来吧!
问题是我们现在仅仅在第一章。我并不喜欢使用后面章节中的内容......这听起来欺
骗读者,对吧?因此,我决定要做是使用以前常常使用的黑匣子API来进行游戏编程。基于
这个要求,我要提一个问题“创建一个类似Freakout的2D游戏,其最低要求是什么?”我
们真正所需要的是下面的功能:
·转换为任何图形模式
·在屏幕上画各种颜色的矩形
·采用键盘输入
·使用定时函数同步游戏循环
·在屏幕上画一串带颜色的文本
因此我创建一个名字为BLACKBOX。CPP|H的目录。该程序带有一套DirectX函数集,并
且包含实现所需要功能的支持代码。最妙的是,读者根本不需要去查看这些代码,只要在
这些函数原型的基础上使用这些函数就可以了,并确保连接上BLACKBOX.CPP|H文件来产生
一个.EXE文件。
以BLACKBOX库为基础,我编写了一个名字为FreakOut的游戏,这个游戏演示了本章中
所讨论的许多概念。FreakOut游戏含有实际游戏的全部主要组成部分,包括:一个游戏循
环、计分、等级,甚至还带有一个球的小型物理模型。我所说的是小型的模型。图1.9是一
幅游戏运行中的屏幕画面。当然它远远不及Arkanoid,但4个小时的工作有此成果也不赖!
在编写游戏代码之前,我希望读者能看一下工程和其各种构件是如何协调一致的。参
见图1.10
从图中可以看到,游戏主要由下面文件构成:
FREAKOUT.CPP——主要的游戏逻辑,使用BLACKBOX.CPP,创建一个最小的Win32应用程
序。
BLACKBOX.CPP——游戏库(请不要偷看)
BLACKBOX.H——游戏库头文件。
DDRAW.LIB——用来建立应用程序的DriectDraw输入库。其中并不含有真正的DirectX
代码。主要用来作为一个函数调用的中间库,可以轮流调用进行实际工作DDRAW.DLL动态链
接库。它可以在DirectX SDK安装程序目录下找到。
DDRAW.DLL——运行过程中的DirectDraw库,实际上含有通过DDRAW.LIB输入库调用Di
rectDraw界面函数的COM执行程序。对此读者不必担心:只要确认已经安装了DirectX文件
即可。
现在,我们对此已有了了解,让我们看一下BLACKOUT.H头文件,看看它包含了哪些函
数。
程序清单1.2 BLOCKOUT.H头文件
-------------------------
// BLACKBOX.H - Header file for demo game engine library
// watch for multiple inclusions
#ifndef BLACKBOX
#define BLACKBOX
// DEFINES ////////////////////////////////////////////////////
// default screen size
#define SCREEN_WIDTH 640 // size of screen
#define SCREEN_HEIGHT 480
#define SCREEN_BPP 8 // bits per pixel
#define MAX_COLORS 256 // maximum colors
// MACROS /////////////////////////////////////////////////////
// these read the keyboard asynchronously
#define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)
#define KEY_UP(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1)
// initializes a direct draw struct
#define DD_INIT_STRUCT(ddstruct) { memset(&ddstruct,0,sizeof(ddstruct)); ddstr
uct.dwSize=sizeof(ddstruct); }
// TYPES //////////////////////////////////////////////////////
// basic unsigned types
typedef unsigned short USHORT;
typedef unsigned short WORD;
typedef unsigned char UCHAR;
typedef unsigned char BYTE;
// EXTERNALS //////////////////////////////////////////////////
extern LPDIRECTDRAW4 lpdd; // dd object
extern LPDIRECTDRAWSURFACE4 lpddsprimary; // dd primary surface
extern LPDIRECTDRAWSURFACE4 lpddsback; // dd back surface
extern LPDIRECTDRAWPALETTE lpddpal; // a pointer to the created dd palette
extern LPDIRECTDRAWCLIPPER lpddclipper; // dd clipper
extern PALETTEENTRY palette[256]; // color palette
extern PALETTEENTRY save_palette[256]; // used to save palettes
extern DDSURFACEDESC2 ddsd; // a direct draw surface description struct
extern DDBLTFX ddbltfx; // used to fill
extern DDSCAPS2 ddscaps; // a direct draw surface capabilities struct
extern HRESULT ddrval; // result back from dd calls
extern DWORD start_clock_count; // used for timing
// these defined the general clipping rectangle
extern int min_clip_x, // clipping rectangle
max_clip_x,
min_clip_y,
max_clip_y;
// these are overwritten globally by DD_Init()
extern int screen_width, // width of screen
screen_height, // height of screen
screen_bpp; // bits per pixel
// PROTOTYPES /////////////////////////////////////////////////
// DirectDraw functions
int DD_Init(int width, int height, int bpp);
int DD_Shutdown(void);
LPDIRECTDRAWCLIPPER DD_Attach_Clipper(LPDIRECTDRAWSURFACE4 lpdds, int num_rect
s, LPRECT clip_list);
int DD_Flip(void);
int DD_Fill_Surface(LPDIRECTDRAWSURFACE4 lpdds,int color);
// general utility functions
DWORD Start_Clock(void);
DWORD Get_Clock(void);
DWORD Wait_Clock(DWORD count);
// graphics functions
int Draw_Rectangle(int x1, int y1, int x2, int y2, int color,LPDIRECTDRAWSURFA
CE4 lpdds=lpddsback);
// gdi functions
int Draw_Text_GDI(char *text, int x,int y,COLORREF color, LPDIRECTDRAWSURFACE4
lpdds=lpddsback);
int Draw_Text_GDI(char *text, int x,int y,int color, LPDIRECTDRAWSURFACE4 lpdd
s=lpddsback);
#endif
-------------------------
现在,就不要将你的精力都浪费在程序代码和那些神秘的全局变量是什么的问题上,
让我们来看一看这些函数。如读者所看到一样,这个简单的图形界面需要一些函数来完成
。在这个图形和小型的Win32应用程序(我们要做的Windows编程工作越少越好)的基础上
,我创建了游戏FREAKOUT.CPP如实例1.1所示。请认真地看一看,特别是主游戏循环和对游
戏处理功能的访问。
程序清单1.3 FREAKOUT.CPP源文件
----------------------------
// FREAKOUT.CPP - break game demo
// INCLUDES ///////////////////////////////////////////////////
#define WIN32_LEAN_AND_MEAN // include all macros
#define INITGUID // include all GUIDs
#include // include important windows stuff
#include
#include
#include // include important C/C++ stuff
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include // directX includes
#include "blackbox.h" // game library includes
// DEFINES ////////////////////////////////////////////////////
// defines for windows
#define WINDOW_CLASS_NAME "WIN3DCLASS" // class name
#define WINDOW_WIDTH 640 // size of window
#define WINDOW_HEIGHT 480
// states for game loop
#define GAME_STATE_INIT 0
#define GAME_STATE_START_LEVEL 1
#define GAME_STATE_RUN 2
#define GAME_STATE_SHUTDOWN 3
#define GAME_STATE_EXIT 4
// block defines
#define NUM_BLOCK_ROWS 6
#define NUM_BLOCK_COLUMNS 8
#define BLOCK_WIDTH 64
#define BLOCK_HEIGHT 16
#define BLOCK_ORIGIN_X 8
#define BLOCK_ORIGIN_Y 8
#define BLOCK_X_GAP 80
#define BLOCK_Y_GAP 32
// paddle defines
#define PADDLE_START_X (SCREEN_WIDTH/2 - 16)
#define PADDLE_START_Y (SCREEN_HEIGHT - 32);
#define PADDLE_WIDTH 32
#define PADDLE_HEIGHT 8
#define PADDLE_COLOR 191
// ball defines
#define BALL_START_Y (SCREEN_HEIGHT/2)
#define BALL_SIZE 4
// PROTOTYPES /////////////////////////////////////////////////
// game console
int Game_Init(void *parms=NULL);
int Game_Shutdown(void *parms=NULL);
int Game_Main(void *parms=NULL);
// GLOBALS ////////////////////////////////////////////////////
HWND main_window_handle = NULL; // save the window handle
HINSTANCE main_instance = NULL; // save the instance
int game_state = GAME_STATE_INIT; // starting state
int paddle_x = 0, paddle_y = 0; // tracks position of paddle
int ball_x = 0, ball_y = 0; // tracks position of ball
int ball_dx = 0, ball_dy = 0; // velocity of ball
int score = 0; // the score
int level = 1; // the current level
int blocks_hit = 0; // tracks number of blocks hit
// this contains the game grid data
UCHAR blocks[NUM_BLOCK_ROWS][NUM_BLOCK_COLUMNS];
// FUNCTIONS //////////////////////////////////////////////////
LRESULT CALLBACK WindowProc(HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM lparam)
{
// this is the main message handler of the system
PAINTSTRUCT ps; // used in WM_PAINT
HDC hdc; // handle to a device context
// what is the message
switch(msg)
{
case WM_CREATE:
{
// do initialization stuff here
return(0);
} break;
case WM_PAINT:
{
// start painting
hdc = BeginPaint(hwnd,&ps);
// the window is now validated
// end painting
EndPaint(hwnd,&ps);
return(0);
} break;
case WM_DESTROY:
{
// kill the application
PostQuitMessage(0);
return(0);
} break;
default:break;
} // end switch
// process any messages that we didn't take care of
return (DefWindowProc(hwnd, msg, wparam, lparam));
} // end WinProc
// WINMAIN ////////////////////////////////////////////////////
int WINAPI WinMain( HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
{
// this is the winmain function
WNDCLASS winclass; // this will hold the class we create
HWND hwnd; // generic window handle
MSG msg; // generic message
HDC hdc; // generic dc
PAINTSTRUCT ps; // generic paintstruct
// first fill in the window class stucture
winclass.style = CS_DBLCLKS | CS_OWNDC |
CS_HREDRAW | CS_VREDRAW;
winclass.lpfnWndProc = WindowProc;
winclass.cbClsExtra = 0;
winclass.cbWndExtra = 0;
winclass.hInstance = hinstance;
winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
winclass.hCursor = LoadCursor(NULL, IDC_ARROW);
winclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
winclass.lpszMenuName = NULL;
winclass.lpszClassName = WINDOW_CLASS_NAME;
// register the window class
if (!RegisterClass(&winclass))
return(0);
// create the window, note the use of WS_POPUP
if (!(hwnd = CreateWindow(WINDOW_CLASS_NAME, // class
"WIN3D Game Console", // title
WS_POPUP | WS_VISIBLE,
0,0, // initial x,y
GetSystemMetrics(SM_CXSCREEN), // intial width
GetSystemMetrics(SM_CYSCREEN), // initial height
NULL, // handle to parent
NULL, // handle to menu
hinstance,// instance
NULL))) // creation parms
return(0);
// hide mouse
ShowCursor(FALSE);
// save the window handle and instance in a global
main_window_handle = hwnd;
main_instance = hinstance;
// perform all game console specific initialization
Game_Init();
// enter main event loop
while(1)
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
// test if this is a quit
if (msg.message == WM_QUIT)
break;
// translate any accelerator keys
TranslateMessage(&msg);
// send the message to the window proc
DispatchMessage(&msg);
} // end if
// main game processing goes here
Game_Main();
} // end while
// shutdown game and release all resources
Game_Shutdown();
// show mouse
ShowCursor(TRUE);
// return to Windows like this
return(msg.wParam);
} // end WinMain
// T3DX GAME PROGRAMMING CONSOLE FUNCTIONS ////////////////////
int Game_Init(void *parms)
{
// this function is where you do all the initialization
// for your game
// return success
return(1);
} // end Game_Init
///////////////////////////////////////////////////////////////
int Game_Shutdown(void *parms)
{
// this function is where you shutdown your game and
// release all resources that you allocated
// return success
return(1);
} // end Game_Shutdown
///////////////////////////////////////////////////////////////
void Init_Blocks(void)
{
// initialize the block field
for (int row=0; row < NUM_BLOCK_ROWS; row++)
for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
blocks[row][col] = row*16+col*3+16;
} // end Init_Blocks
///////////////////////////////////////////////////////////////
void Draw_Blocks(void)
{
// this function draws all the blocks in row major form
int x1 = BLOCK_ORIGIN_X, // used to track current position
y1 = BLOCK_ORIGIN_Y;
// draw all the blocks
for (int row=0; row < NUM_BLOCK_ROWS; row++)
{
// reset column position
x1 = BLOCK_ORIGIN_X;
// draw this row of blocks
for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
{
// draw next block (if there is one)
if (blocks[row][col]!=0)
{
// draw block
Draw_Rectangle(x1-4,y1+4,
x1+BLOCK_WIDTH-4,y1+BLOCK_HEIGHT+4,0);
Draw_Rectangle(x1,y1,x1+BLOCK_WIDTH,
y1+BLOCK_HEIGHT,blocks[row][col]);
} // end if
// advance column position
x1+=BLOCK_X_GAP;
} // end for col
// advance to next row position
y1+=BLOCK_Y_GAP;
} // end for row
} // end Draw_Blocks
///////////////////////////////////////////////////////////////
void Process_Ball(void)
{
// this function tests if the ball has hit a block or the paddle
// if so, the ball is bounced and the block is removed from
// the playfield note: very cheesy collision algorithm :)
// first test for ball block collisions
// the algorithm basically tests the ball against each
// block's bounding box this is inefficient, but easy to
// implement, later we'll see a better way
int x1 = BLOCK_ORIGIN_X, // current rendering position
y1 = BLOCK_ORIGIN_Y;
int ball_cx = ball_x+(BALL_SIZE/2), // computer center of ball
ball_cy = ball_y+(BALL_SIZE/2);
// test of the ball has hit the paddle
if (ball_y > (SCREEN_HEIGHT/2) && ball_dy > 0)
{
// extract leading edge of ball
int x = ball_x+(BALL_SIZE/2);
int y = ball_y+(BALL_SIZE/2);
// test for collision with paddle
if ((x >= paddle_x && x <= paddle_x+PADDLE_WIDTH) &&
(y >= paddle_y && y <= paddle_y+PADDLE_HEIGHT))
{
// reflect ball
ball_dy=-ball_dy;
// push ball out of paddle since it made contact
ball_y+=ball_dy;
// add a little english to ball based on motion of paddle
if (KEY_DOWN(VK_RIGHT))
ball_dx-=(rand()%3);
else
if (KEY_DOWN(VK_LEFT))
ball_dx+=(rand()%3);
else
ball_dx+=(-1+rand()%3);
// test if there are no blocks, if so send a message
// to game loop to start another level
if (blocks_hit >= (NUM_BLOCK_ROWS*NUM_BLOCK_COLUMNS))
{
game_state = GAME_STATE_START_LEVEL;
level++;
} // end if
// make a little noise
MessageBeep(MB_OK);
// return
return;
} // end if
} // end if
// now scan thru all the blocks and see of ball hit blocks
for (int row=0; row < NUM_BLOCK_ROWS; row++)
{
// reset column position
x1 = BLOCK_ORIGIN_X;
// scan this row of blocks
for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
{
// if there is a block here then test it against ball
if (blocks[row][col]!=0)
{
// test ball against bounding box of block
if ((ball_cx > x1) && (ball_cx < x1+BLOCK_WIDTH) &&
(ball_cy > y1) && (ball_cy < y1+BLOCK_HEIGHT))
{
// remove the block
blocks[row][col] = 0;
// increment global block counter, so we know
// when to start another level up
blocks_hit++;
// bounce the ball
ball_dy=-ball_dy;
// add a little english
ball_dx+=(-1+rand()%3);
// make a little noise
MessageBeep(MB_OK);
// add some points
score+=5*(level+(abs(ball_dx)));
// that's it -- no more block
return;
} // end if
} // end if
// advance column position
x1+=BLOCK_X_GAP;
} // end for col
// advance to next row position
y1+=BLOCK_Y_GAP;
} // end for row
} // end Process_Ball
///////////////////////////////////////////////////////////////
int Game_Main(void *parms)
{
// this is the workhorse of your game it will be called
// continuously in real-time this is like main() in C
// all the calls for you game go here!
char buffer[80]; // used to print text
// what state is the game in?
if (game_state == GAME_STATE_INIT)
{
// initialize everything here graphics
DD_Init(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP);
// seed the random number generator
// so game is different each play
srand(Start_Clock());
// set the paddle position here to the middle bottom
paddle_x = PADDLE_START_X;
paddle_y = PADDLE_START_Y;
// set ball position and velocity
ball_x = 8+rand()%(SCREEN_WIDTH-16);
ball_y = BALL_START_Y;
ball_dx = -4 + rand()%(8+1);
ball_dy = 6 + rand()%2;
// transition to start level state
game_state = GAME_STATE_START_LEVEL;
} // end if
////////////////////////////////////////////////////////////////
else
if (game_state == GAME_STATE_START_LEVEL)
{
// get a new level ready to run
// initialize the blocks
Init_Blocks();
// reset block counter
blocks_hit = 0;
// transition to run state
game_state = GAME_STATE_RUN;
} // end if
///////////////////////////////////////////////////////////////
else
if (game_state == GAME_STATE_RUN)
{
// start the timing clock
Start_Clock();
// clear drawing surface for the next frame of animation
Draw_Rectangle(0,0,SCREEN_WIDTH-1, SCREEN_HEIGHT-1,200);
// move the paddle
if (KEY_DOWN(VK_RIGHT))
{
// move paddle to right
paddle_x+=8;
// make sure paddle doesn't go off screen
if (paddle_x > (SCREEN_WIDTH-PADDLE_WIDTH))
paddle_x = SCREEN_WIDTH-PADDLE_WIDTH;
} // end if
else
if (KEY_DOWN(VK_LEFT))
{
// move paddle to right
paddle_x-=8;
// make sure paddle doesn't go off screen
if (paddle_x < 0)
paddle_x = 0;
} // end if
// draw blocks
Draw_Blocks();
// move the ball
ball_x+=ball_dx;
ball_y+=ball_dy;
// keep ball on screen, if the ball hits the edge of
// screen then bounce it by reflecting its velocity
if (ball_x > (SCREEN_WIDTH - BALL_SIZE) || ball_x < 0)
{
// reflect x-axis velocity
ball_dx=-ball_dx;
// update position
ball_x+=ball_dx;
} // end if
// now y-axis
if (ball_y < 0)
{
// reflect y-axis velocity
ball_dy=-ball_dy;
// update position
ball_y+=ball_dy;
} // end if
else
// penalize player for missing the ball
if (ball_y > (SCREEN_HEIGHT - BALL_SIZE))
{
// reflect y-axis velocity
ball_dy=-ball_dy;
// update position
ball_y+=ball_dy;
// minus the score
score-=100;
} // end if
// next watch out for ball velocity getting out of hand
if (ball_dx > 8) ball_dx = 8;
else
if (ball_dx < -8) ball_dx = -8;
// test if ball hit any blocks or the paddle
Process_Ball();
// draw the paddle and shadow
Draw_Rectangle(paddle_x-8, paddle_y+8,
paddle_x+PADDLE_WIDTH-8,
paddle_y+PADDLE_HEIGHT+8,0);
Draw_Rectangle(paddle_x, paddle_y,
paddle_x+PADDLE_WIDTH,
paddle_y+PADDLE_HEIGHT,PADDLE_COLOR);
// draw the ball
Draw_Rectangle(ball_x-4, ball_y+4, ball_x+BALL_SIZE-4,
ball_y+BALL_SIZE+4, 0);
Draw_Rectangle(ball_x, ball_y, ball_x+BALL_SIZE,
ball_y+BALL_SIZE, 255);
// draw the info
sprintf(buffer,"F R E A K O U T Score %d Level %d",score,level);
Draw_Text_GDI(buffer, 8,SCREEN_HEIGHT-16, 127);
// flip the surfaces
DD_Flip();
// sync to 33ish fps
Wait_Clock(30);
// check of user is trying to exit
if (KEY_DOWN(VK_ESCAPE))
{
// send message to windows to exit
PostMessage(main_window_handle, WM_DESTROY,0,0);
// set exit state
game_state = GAME_STATE_SHUTDOWN;
} // end if
} // end if
///////////////////////////////////////////////////////////////
else
if (game_state == GAME_STATE_SHUTDOWN)
{
// in this state shut everything down and release resources
DD_Shutdown();
// switch to exit state
game_state = GAME_STATE_EXIT;
} // end if
// return success
return(1);
} // end Game_Main
///////////////////////////////////////////////////////////////
----------------------------
哈哈,酷吧?这是一个完整的Wind32/DirectX游戏,表现接近上佳水平了。BLACKOUT
.CPP源文件中有几百行代码,但是我们只能将之视为类DirectX游戏而且是由某个人编写的
(我!)。不管怎样说,还是让我们迅速浏览一下实例1.3的内容吧。
首先,Windows需要一个事件循环。这就是Windows编程的标准,因为Windows在大部分
情况下都是事件驱动的。但是游戏却不是事件驱动的。无论用户在干什么,它们都在一直
运行。因此,我们至少需要支持小型事件循环以取悦于Windows。执行这项功能的代码位于
WinMain()中——呀,令人惊奇!不是吗?
WinMain()是所有Windows程序的主要入口点,就像是Main()是所有DOS/UNIX程序中的
入口点一样。任何情况下,FREAOUT的WinMain()创建一个窗口,正确进入到事件循环中。
如果Windows需要工作时,就按照这样做。当所有的基本事件处理都结束时,访问Game_Ma
in()。这就是我们的游戏程序实际运行的状态。
如果需要的话,读者可以一直在Game_Main()中循环。而不释放回到WinMain()主事件
循环体中。但这样做不是件好事,因为Windows会得不到任何信息,从而缺乏资源。哎,我
们想要做的是运行一个动画和逻辑的画面,然后返回到WinMain()。这样的话,Windows可
以继续工作和处理信息。如果所有这些听起来像是幻术的话,请不要担心——在下一章中
情况还会更糟。
一旦进入Game_Main(),就执行FreakOut逻辑。游戏图像提交到显示缓冲区,最后通过
DD_FLIP()访问在循环结束时在显示屏上显示出来。因此我希望读者要做的是浏览一下全部
的游戏状态,跟随一遍游戏循环的每一部分,了解一下工作原理。要想玩游戏的话,只要
单击FREAKOUT.EXE文件,程序就会立即启动。控制部分包括:
右箭头键——向右移动操作杆。
左箭头键——向左移动操作杆。
Esc键——结束并返回到Windows。
还有,如果读者错过一个球的话,将被罚掉100点,可要仔细盯紧啊!
如果读者对游戏代码和玩法已适应,试着修改一下游戏。可以增加不同的背景颜色(0
~255是各种有效的颜色)、更多的球、可以改变操作杆的大小以及更多的声音效果(那是
我用Win32API功能MessageBeep()函数做出的)。
总结
这大概是我所介绍的关于游戏编程快速入门课程中最快的一次了!我们谈论了大量的
基础内容,但是只能把它看作本书的基础版本。我只想让读者对本书中我们所讨论的和学
习的内容有一个感性认识。另外,阅读一个完整的游戏是有益的,因为这可以让读者思考
若干问题。
现在,在进行关于Windows编程的第二章之前,读者对编译FreakOut游戏应该能够轻松
驾驭。如果还没有这种感觉的话,就请立即打开编译器的书和RTFM。我等着你们。