一个C++写成的DirectDraw 游戏
——M$新型快速图形API的实用观点

原文:PC Magazine: A DirectDraw Game in C++ (08/96)

作者:G y o r g y G r e l l and P a u l Modzelewski

网址: http://www.pcmag.com/issues/1514/pcmg0033.htm

翻译:品雪 pinxue@hotmail.com 1998.7.28

[注意,PC MAGAZINE提供的源码文件名给改乱了,这里提供修正后的,有少量中文注释]

上次给出DirectDraw的浏览和用法(“Using DirectDraw with C++”, PC Magezine June 25,1996);现在是更实用的看M$这个for Win95的快速图形API的时候了。本文将讨论可重用C++类(建议把DD封装在类中),及我们使用这些类建立的游戏。[figure1]

我们游戏开发方面的主要目标是演示DDC++构成了多媒体开发的绝妙队伍。我们已经许多类,多数是基于MFC的。(假定你对Windows编程及MFCC++已有所知)。

figure 1:This Asteroids-style program use DriectDraw to speed display performace.

框架

我们的游戏主要包括两个类及其衍生类:CGameAppCGameFrame。我们决定干掉M$ VC AppWizard 生成的Doc/View,因为我们犯不上跟Doc/view结构纠缠。CGameApp是标准CWinApp子类。我们在CGameAppInitInstance函数里直接建立一个CFrameWnd来取代创建一个文档模板(这将建立主窗口):

m_pMainWnd = GetNewFrame();

ASSERT(m_pMainWnd != NULL);

m_pMainWnd-> ShowWindow(SW_SHOWNORMAL);

注意对GetNewFrame虚函数的调用,它建立当前应用程序主窗口类的一个实例。它发生在我们建立CGameAppCGameFrame衍生类时。

我们应用程序类最重要的函数是Run,它被标准MFC处理过程在所有应用初始化工作完成后调用。在Run中,我们完成所有的游戏初始化,然后循环直到PumpMessage函数返回TRUE��应用程序结果信号。我们使用PeekMessage检查任何已排队消息,如果返回TRUEPumpMessage取得一条已经存在的队列消息并强制它通过MFC的处理过程。

if (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))

{

// pump message, but quit on WM_QUIT

if (!PumpMessage())

{

GetGameFrame()->KillGraphics();

GetGameFrame()->DestroyWindow();

return ExitInstance();

}

}

Run循环的下一部分是我们调用CGameFrameUpdateGame函数的地方,它更新游戏内部,完成绘制并切换视频页:

if (m_bPlaying)

{

if (!GetGameFrame()->UpdateGame())

{

StopPlay();

PostQuitMessage(0);

}

}

else

{

::WaitMessage();

}

你会发现UpdateGame的真正主体是CSpaceFrameStartPlayStopPlay是另外两个重要的函数,它们支持重新开始和暂停游戏。在CGameFrameActivateApp函数中,你会注意到我们在推动焦点时调用了StopLlay,在获得焦点时调用了StartPlay。注意,StopPlay设置CGameApp成员m_bPlayingFALSE,它将防止UpdateGame被调用。当程序得到或失去输入焦点时OnActivateApp被调用,这是调用StartPlayStopPlay的地方。当程序处理后台时不会进行循环。

CGameFrame封装了主窗口的建立及功能。它包括游戏所需基本数据成员:一个DirectDrawSurface指向主要的flipping surface(m_pFrontBuffer),一个指向绘制界面(m_pBackBuffer)的指针,及游戏的调色板(m_pGamePalette)。正如前文所述,所有绘制工作是在绘图界面上完成的,然后切换页实现平滑动画。

我们在建构函数中建立主窗口。首先用AfxRegisterWndClass注册自己的窗口类以改变默认的光标、图标和背景画刷。我们将光标设为NULL使之不显示;图标是从资源中装入的,背景刷设为black这样在第一次页切换之前主窗口不会很显眼。下一步我们用CreateEx建立窗口。注意,我们为debugrelease版建立不同的窗口。我们发现用小的、层叠窗口(overlap)会使用调试更容易些,而发行模式窗口是位于最顶层的弹出(pop-up)窗口,尺寸与屏幕分辨率相同。为什么我们要使用WS_SYSMENU风格呢?因为我们想在切换到其它程序时让游戏出现在任务栏上,如果不指定WS_SYSMENU风格,任务栏上不会显示出应用程序的图标。

InitGraphics函数包含所有的设定和初始化DirectDraw工作,正如本文前面所述。另一个重要的函数是RestoreSurface,当切换到其它程序后会需要它。当切换出现时,GDI恢复它控制范围内的视频内存。因此,当我们的游戏恢复时,我们需要调用为所有的界面调用RestoreSurface以重新请求它的视频内存,然后必要的话将图像复制回界面。

因为我们用WS_SYSMENU建立主窗口,窗口有一个菜单。这样,如果按下Alt键游戏将会停止,系统菜单将取得焦点。但我们可能想用Alt键进行控制。要允许那样,我们建立一个OnSysCommand函数,它截获并取消菜单键(AltF10),将任何其它键传给默认的消息处理程序。

动画

队从MFC衍生的类,我们还写了CAim类,一个直接的C++类以处理DirectDraw动画。这个类不依类于MFC,去掉animation.cpp开始时部分的#include “stdafx.h”就可以在任何其它DirectDraw程序中使用了。以下的片断显示通常如何建立一个CAnim

m_pShipAnim = new CAnim(m_pDD,”ship”,”media/”,32);

第一个参数是指程序的DirectDraw对象的指针,第二、三个参数告诉应用程序到哪找包含独立帧的位图文件。这第二个参数是文件名的路径,每个必须包含三个元素:一个四字符的root标识,表明帧所属群组,跟上以0填充的四位数字标识,表明帧在动画中的位置(从0000开始),以及一个.bmp扩展名。第三个参数是文件所在目录名。第四个参数是动画的帧数,第五个参数是默认为TRUE的布尔变量,它决定CAnim是否尝试将图像装入视频内存。如果视频内存不足则继续使用系统内存。类似的,如果显式设为FALSE,那么图像将会装进系统内存。上边这条命令将从子目录media中装入ship0000.bmpship0031.bmp,并在空间许可的情况下装入视频内存中。确认同一动画中所有图像宽高都相同是十分重要的。

建立一个CAnim并不装入图像或为保存图像分配内存;这些在Load函数中真正发生,所以在使用CAnim之前必须先调用Load函数。Load函数实际上调用保护成员CAnim::InternalLoad,它也会被CAnim::Restore调用。InternalLoad函数建立一个单独的DirectDrawSurface,动画所有的独立图像在它的网格里绘制。之后CAnim::Render根据所需的帧序号索引网格。这是CAnim::Render的原形:

HRESULT Render(int nX, int nY, int nFrameNum, LPDIRECTDRAWSURFACE pDestSurface, BOOL bTransparent = TRUE);

前两个参数是动画左上角象素的x,y坐标。第三个参数是要绘制的帧号,接着是目的界面(使用后缓冲区)。最后一个参数用于设置透明位传送操作,默认为TRUE或支持透明处理。返回值是来衢标界面BltFast函数的HRESULT。返回该值的基本原因是为了判断界面是否已经丢失。始终应当将返回的HRESULTDDERR_SURFACELOST比较,如果匹配那么恢复所有的界面。

恢复一CAnim的个界面只要简单的调用一下Restore函数。Restore将先调用为拥有的界面调用IDirectDrawSurface::Restore,取回界面的内存。然后调用CAnim::InternalLoad(TRUE),将图像文件重拷到刚恢复的界面里。CAnim::Release只简单调用CAnim界面的IDirectDrawSurface::Release,这应该在C++对象解析之前完成。(通常忘掉调用这个函数没什么影响,因为主要的DirectDraw对象会在释放时释放所有的界面和调色板,但显式调用是良好的编程习惯。

游戏

我们的游戏是简单的,并不需要很长的解释。你是紫色的船;射击屏幕上所有的盒子以进到下一级,在那里会有更多的盒子。进入一个盒子丢一条命。丢三条命Game Over。左右键旋转你的船,上键加速,下减速,Ctrl开火,Esc退出。屏幕顶上的数字是游戏当前的帧率(帧/秒)。它只是指示当前游戏动画的平滑程度,用户不能控制帧速率。

要运行游戏,系统中必须有Game SDK运行时文件。任何使用Games SDK的游戏都会安装它们,如果已经装过这样的游戏就可以运行了。要编译代码,需要Visual C++ 4.xGames SDK 它目前包括在MSDN level IIVisual C++4.1里了。

那么全部的源代码在哪里呢?我们决定使用C++的特性从我们已有的帧及应用程序类衍生建立一些类。这样我们可以分解,将通用的工作放到CGameAppCGameFrame中,而把游戏相关代码放在CSpaceAppCSpaceFrame类里。我们或许应该进一步分解,但我们不想被OOP弄得Crazy。注意CSpaceApp只由GetNewFrame和全局对象theApp组成。我们决定不把这些代码放进CGameApp,这样,通用代码在哪结束,游戏代码在哪开始会更清楚些。

CSpaceFrame使用CGameFrame作为功能基础,重载了许多关键的虚函数,颇加入了一些专用于实现游戏的非虚函数。CSpaceFrame::InitGraphics首先调用CGameFrame::InitGraphics,它建立我们的DirectDraw 对象,设定图形模式,建立基本的切换界面,调用InitPaletteCSpaceFrame::IntitGraphics然后建立两个DirectDrawSurface,一个用来显示帧率,另一个显示任何绘制在屏幕上的文字。我们也建立并装入了游戏中用到的所有CAnimsCSpaceFrameRestore函数调用CGameFrame::Restore以恢复基本界面内存,然后恢复帧率及文字界面,当然也调用所有CAnimsRestore

为接管游戏处理,CSpaceFrame也重载了UpdateGame。没必要调用基类版的UpdateGame,因为它只简单的返回TRUEUpdateGame是我们的基本的主游戏循环,以固定时间间隔被CGameApp::Run调用,并且是游戏真正的核心。UpdateGame确切的实现是相当简单的,因为所有复杂的工作都分解到各辅助函数里了。处理过程的本质是:

*移动精灵

*碰撞检测

*用黑色清屏

*绘制精灵

*切换页

我们用一个经典的双向链表结构表示精灵。这里是取自SpaceFrame.h的定义:

typedef struct _SPRITE

{

struct _SPRITE* pNext;

struct _SPRITE* pPrev;

SpriteType eType;

double dPosX, dPosY;

double dVelX, dVelY;

int nDirection;

int nFrame;

int nDelay;

CAnim* pAnim;

} SPRITE;

头两个变量指向链表中前后精灵,链表是循环的,最后个精灵的pNext指向表头。eType成员决定精灵的类型是用哪种节点表示的,可能的值为typeShiptypeEnemy,和typeBulletnDelay变量用来减慢敌人和子弹的动画速度及船发射子弹的速度。

CSpaceFrame的许多函数用于处理精灵清单。移动精灵(MoveSprites)循环经过整个链,改变量精灵的位置、速度、需要的帧号信息。每种类型的精灵的处理略有不同;如果,当前更新船时,MoveSprites取出键盘决定精灵运动。CollideSprites包含所有的碰撞检测代码,并象MoveSprites相似的循环经过列表并用略有不同的方式处理每种类型的精灵。

当一个碰撞出现时,CollideSprites会根据相关精灵的类型适当动作。DrawSprites用于绘制所有的精灵到后缓冲。它循环经过整个列表,调用每个精灵CAnimRender成员函数。函数AddSpriteRemoveSprite用于从列表中动态增加及删除精灵。并且m_head是列表的头及玩家的船,被保存在堆栈(stack)。

你可以从PC Magazine Online下载游戏的全部代码,1996.8volum archive里有。(see the sidebar "Guide to Our Utilities" in this issue's Utilities column for

downloading instructions)。编译程序步骤如下:

  1. unzip –d解开文件包。
  2. Visual C++ 4.x编译程序。degug build使用dllmfcrelease build 使用表态链接。
  3. DirectDraw库和头文件目录应加入搜索路径。图片存在media目录中。

有许多种另人激动的方法使用C++ 的威力,尤其将Visual C++4.xDirectDraw结合时。因为DirectDraw和所有其它Games SDK构件均基于COM对象,使得进行C++封装更为容易。我们希望本文提供的代码能加快你的工作进展。

 

Gyorgy Grell is on a quest for the perfect bitmap-editing/color-reduction/palette-editing program for Windows. He can be reached at

ggrell@gsoftinc.com. Paul Modzelewski is an independent consultant specializing in C++ interactive multimedia development for Windows 95. He can be

reached at pmodz@mnsinc.com. The actual implementation of Update game is fairly simple, since all the complex work is farmed out to helper functions. There are

many exciting ways to use the power of C++, particularly when combining Visual C++ with DirectDraw.

关于调试

使用M$ DevStudio调试DirectDraw应用程序的最好方法是在第二台计算机上使用远程调试特性。当然,我们中的多数只有一台可用,幸好,我们发现了一个简单的方法以调试程序。

当在debug模式编译应用程序时,窗口属性应该设成这样,以使单步通过多数程序成为可能(除了DirectDrawSurface Lock()Unlock()之间的代码)。在调试期间,我们在InitGraphic()函数的第一行放一个断点。一旦运行到该点,我们单步运行每行代码,直到SetDisplayMode()代码。因为我们单步分别运行每一行,DirectDraw将不会扩展接管GDI,我们甚至看不见调试器。这时,得要最大化主窗口,因为屏幕分辨会改变。从这以后,你就可以在代码里放置代码并确实使得调试窗口可见。