一个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]
我们游戏开发方面的主要目标是演示DD及C++构成了多媒体开发的绝妙队伍。我们已经许多类,多数是基于MFC的。(假定你对Windows编程及MFC和C++已有所知)。
figure 1:This Asteroids-style program use DriectDraw to speed display performace.
框架
我们的游戏主要包括两个类及其衍生类:CGameApp和CGameFrame。我们决定干掉M$ VC AppWizard 生成的Doc/View,因为我们犯不上跟Doc/view结构纠缠。CGameApp是标准CWinApp子类。我们在CGameApp的InitInstance函数里直接建立一个CFrameWnd来取代创建一个文档模板(这将建立主窗口):
m_pMainWnd = GetNewFrame();
ASSERT(m_pMainWnd != NULL);
m_pMainWnd-> ShowWindow(SW_SHOWNORMAL);
注意对GetNewFrame虚函数的调用,它建立当前应用程序主窗口类的一个实例。它发生在我们建立CGameApp和CGameFrame衍生类时。
我们应用程序类最重要的函数是Run,它被标准MFC处理过程在所有应用初始化工作完成后调用。在Run中,我们完成所有的游戏初始化,然后循环直到PumpMessage函数返回TRUE应用程序结果信号。我们使用PeekMessage检查任何已排队消息,如果返回TRUE,PumpMessage取得一条已经存在的队列消息并强制它通过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循环的下一部分是我们调用CGameFrame的UpdateGame函数的地方,它更新游戏内部,完成绘制并切换视频页:
if (m_bPlaying)
{
if (!GetGameFrame()->UpdateGame())
{
StopPlay();
PostQuitMessage(0);
}
}
else
{
::WaitMessage();
}
你会发现UpdateGame的真正主体是CSpaceFrame。StartPlay和StopPlay是另外两个重要的函数,它们支持重新开始和暂停游戏。在CGameFrame的ActivateApp函数中,你会注意到我们在推动焦点时调用了StopLlay,在获得焦点时调用了StartPlay。注意,StopPlay设置CGameApp成员m_bPlaying为FALSE,它将防止UpdateGame被调用。当程序得到或失去输入焦点时OnActivateApp被调用,这是调用StartPlay和StopPlay的地方。当程序处理后台时不会进行循环。
CGameFrame封装了主窗口的建立及功能。它包括游戏所需基本数据成员:一个DirectDrawSurface指向主要的flipping surface(m_pFrontBuffer),一个指向绘制界面(m_pBackBuffer)的指针,及游戏的调色板(m_pGamePalette)。正如前文所述,所有绘制工作是在绘图界面上完成的,然后切换页实现平滑动画。
我们在建构函数中建立主窗口。首先用AfxRegisterWndClass注册自己的窗口类以改变默认的光标、图标和背景画刷。我们将光标设为NULL使之不显示;图标是从资源中装入的,背景刷设为black这样在第一次页切换之前主窗口不会很显眼。下一步我们用CreateEx建立窗口。注意,我们为debug和release版建立不同的窗口。我们发现用小的、层叠窗口(overlap)会使用调试更容易些,而发行模式窗口是位于最顶层的弹出(pop-up)窗口,尺寸与屏幕分辨率相同。为什么我们要使用WS_SYSMENU风格呢?因为我们想在切换到其它程序时让游戏出现在任务栏上,如果不指定WS_SYSMENU风格,任务栏上不会显示出应用程序的图标。
InitGraphics函数包含所有的设定和初始化DirectDraw工作,正如本文前面所述。另一个重要的函数是RestoreSurface,当切换到其它程序后会需要它。当切换出现时,GDI恢复它控制范围内的视频内存。因此,当我们的游戏恢复时,我们需要调用为所有的界面调用RestoreSurface以重新请求它的视频内存,然后必要的话将图像复制回界面。
因为我们用WS_SYSMENU建立主窗口,窗口有一个菜单。这样,如果按下Alt键游戏将会停止,系统菜单将取得焦点。但我们可能想用Alt键进行控制。要允许那样,我们建立一个OnSysCommand函数,它截获并取消菜单键(Alt或F10),将任何其它键传给默认的消息处理程序。
动画
队从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.bmp到ship0031.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。返回该值的基本原因是为了判断界面是否已经丢失。始终应当将返回的HRESULT与DDERR_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.x及Games SDK, 它目前包括在MSDN level II及Visual C++4.1里了。
那么全部的源代码在哪里呢?我们决定使用C++的特性从我们已有的帧及应用程序类衍生建立一些类。这样我们可以分解,将通用的工作放到CGameApp和CGameFrame中,而把游戏相关代码放在CSpaceApp和CSpaceFrame类里。我们或许应该进一步分解,但我们不想被OOP弄得Crazy。注意CSpaceApp只由GetNewFrame和全局对象theApp组成。我们决定不把这些代码放进CGameApp,这样,通用代码在哪结束,游戏代码在哪开始会更清楚些。
CSpaceFrame使用CGameFrame作为功能基础,重载了许多关键的虚函数,颇加入了一些专用于实现游戏的非虚函数。CSpaceFrame::InitGraphics首先调用CGameFrame::InitGraphics,它建立我们的DirectDraw 对象,设定图形模式,建立基本的切换界面,调用InitPalette。CSpaceFrame::IntitGraphics然后建立两个DirectDrawSurface,一个用来显示帧率,另一个显示任何绘制在屏幕上的文字。我们也建立并装入了游戏中用到的所有CAnims。CSpaceFrame的Restore函数调用CGameFrame::Restore以恢复基本界面内存,然后恢复帧率及文字界面,当然也调用所有CAnims的Restore。
为接管游戏处理,CSpaceFrame也重载了UpdateGame。没必要调用基类版的UpdateGame,因为它只简单的返回TRUE。UpdateGame是我们的基本的主游戏循环,以固定时间间隔被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成员决定精灵的类型是用哪种节点表示的,可能的值为typeShip,typeEnemy,和typeBullet。nDelay变量用来减慢敌人和子弹的动画速度及船发射子弹的速度。
CSpaceFrame的许多函数用于处理精灵清单。移动精灵(MoveSprites)循环经过整个链,改变量精灵的位置、速度、需要的帧号信息。每种类型的精灵的处理略有不同;如果,当前更新船时,MoveSprites取出键盘决定精灵运动。CollideSprites包含所有的碰撞检测代码,并象MoveSprites相似的循环经过列表并用略有不同的方式处理每种类型的精灵。
当一个碰撞出现时,CollideSprites会根据相关精灵的类型适当动作。DrawSprites用于绘制所有的精灵到后缓冲。它循环经过整个列表,调用每个精灵CAnim的Render成员函数。函数AddSprite和RemoveSprite用于从列表中动态增加及删除精灵。并且m_head是列表的头及玩家的船,被保存在堆栈(stack)。
你可以从PC Magazine Online下载游戏的全部代码,1996.8的volum archive里有。(see the sidebar "Guide to Our Utilities" in this issue's Utilities column for
downloading instructions)。编译程序步骤如下:
有许多种另人激动的方法使用C++ 的威力,尤其将Visual C++4.x与DirectDraw结合时。因为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,我们甚至看不见调试器。这时,得要最大化主窗口,因为屏幕分辨会改变。从这以后,你就可以在代码里放置代码并确实使得调试窗口可见。