游戏开发新手入门之DirectX入门

时间:2021-07-28 13:25:48

游戏开发新手入门之DirectX入门

[文章导读]
 
    今天我们要接触到令人敬畏的DirectX。它比Windows GDI要快好几倍,可用于不同的语言和多种平台

[正文]

  ☆ 简介

  今天我们要接触到令人敬畏的DirectX。它比Windows GDI要快好几倍,可用于不同的语言和多种平台,支持从绘制象素到高级3D图象,从播放简单声音到数字音乐,从键盘控制到反震手柄……它给你游戏编程所需的一切(有点夸张)。当然了,它是巨大的,需要好几本书才能含盖它的全部。先不要去担心我在这里所教给你之外的数不清的知识,毕竟我把你推到了起跑线上。

  阅读本章,你需要前几章的知识和C语言的知识,由于我们还要谈到组件对象模型(COM),它是面向对象系统的基础,你最好还要有一点儿C++的知识。没有也不太要紧,我在讲到这处时会照顾你的。反正你记住,使用DirectX并不需要多少C++的知识。开始吧!

  ☆ 什么是DirectX?

  DirectX是游戏制作者的API(Application Development Interface)。它是一组允许你直接控制计算机硬件设备的软件。如果你的硬件支持DirectX,并且你用硬件加速你的程序,这就意味着一个字——快。不用担心你的硬件知识,你不会真正的接触到它们。我们是通过硬件抽象层(HAL)和硬件仿真层(HEL)来保证设备无关性和让你的程序正常运行。

  DirectX由很多组件构成,每一个都有特定的用途。组件DirectDraw是最为重要的一个,因为所有的图形都要用到它,它是2D图形的引擎,3D图形也同样离不开它。DirectDraw是我们今天就要说的。其它的组件是:

  ▲ DirectSound:提供硬件和软件的声音混合与回放。

  ▲ DirectMusic:处理基于消息的音乐数据。它支持乐器数字接口(MIDI)并为创建交互式音乐提供创作工具。

  ▲ DirectPlay:使得通过调制解调器链接或通过网络来与应用程序相连成为可能。

  ▲ Direct3D:是一个三维图形包,它提供一个高级的保留模式(Retained Mode)接口,这使得你能够实现一个完整的三维图形系统。它还包含一个低级的即时模式(Immediate Mode)接口,使得应用程序获得对渲染管线的完全控制。

  ▲ DirectInput:为包括游戏杆、鼠标、键盘和游戏控制器在内的输入设备提供支持。它还为反馈游戏设备提供支持。

  ▲ DirectSetup:为DirectX提供了一个简单的安装过程。它简化了更新显示和音频驱动程序的过程,并且确保没有硬件或软件冲突的存在。

  ▲ AutoPlay:让你能够制作一张一旦插入驱动器就能自动安装的光盘。AutoPlay并非DirectX所独有,因为它是Microsoft Win32 API的一部分。

  组件对象模型(COM)是DirectX的基础,有一些技巧建立COM对象——别问我怎么做——但你知道一点点还是有好处的。我只是简单说一下,如果你有兴趣,具体的细节就自己查资料吧!可能下一节你有些困惑,但不要紧,我所说的你不用太明白,毕竟我们的目的是使用COM对象,这可比创建容易多了。

  ☆ 组件对象模型(COM)

  COM接口是DirectX技术的基础,没有COM就没有DirectX。(不用担心,你只需要对COM技术有一个粗浅的了解就可以使用DirectX——只要你在编写DirectX应用程序时遵循一定的步骤,甚至都可以在不了解COM的情况下使用DirectX。

  DirectX的大多数API都是基于COM结构的。COM为软件模块化和软件重用提供了最坚实的基础,它的最重要的概念就是接口(interface),接口是软件重用的最基本方法。更专业的说,接口是一系列操作的规范描述,即接口规范。

  所有的COM接口都是从Iunknown接口继承而来的,IUnknown接口是所有COM接口的根。IUnknown接口具有3个方法:

  · QueryInterface():此方法查询新接口,并在新接口存在时返回之。

  · AddRef():此方法在接口或其它应用程序连编到此COM对象上时将引用计数值递加1。

  · Release():此方法将COM对象的引用计数递减1。当引用计数递减到0时,该COM对象自动释放。

  所有COM对象都具有这三个方法。虽然DirectX应用程序一般不需要考虑引用计数的问题,但引用计数确实是存在的,它已经由DirectX自动完成了。我们所要做的,就是创建DirectX对象,然后在使用完毕后调用Release方法释放引用。

  ☆ 设置

  用DirectX创建程序,你需要有三件主要的事要做。第一件事是COM对象本身,它们包含在.DLL文件里,这些.DLL文件需要在Windows里注册,这在安装DirectX软件包时已经完成了。这些对象是我们创建DirectX应用程序时用到的接口,例如IdirectDraw。但这还不够,因为在COM层上直接使用DirectX是令人沮丧的和乏味的。我们希望有更容易的办法解决它。利用静态库(.LIB文件)是个好办法,它是DirectX软件包的一部分,你可以从Microsoft免费获得。它有一个“打包”函数使你工作更轻松。使用DirectX的不同组件,你需要链接不同的静态库。例如你要使用DirectDraw组件,你就需要ddraw.lib。

  最后,你还需要DrectX头文件,它包含函数原形、宏、常量和你需要用到的各种类型。对于DirectDraw,这个头文件是ddraw.h。
要确认你使用了正确的文件版本,你还得让编译器包含软件开发包的目录。具体的做法是:

  首先点击Tool菜单,选择Options,然后点击Directories,在Show Directories for 组合框下拉菜单中选择Include files,增加一个新的目录。将你的DirectX的路径填入。(例如:C:DXSDKinclude)然后将它移到列表的第一位,使编译时第一个寻找它(防止寻找老版本)。然后选择Show Directories for组合框下拉菜单中的Library files,方法同前,只是把include改成lib。现在,你已经设置完了DirectX。你仍然需要手动增加一些库文件到你的项目中,但先不急,我将在以后讲它。我们将使用DirectX 7.0。

  ☆ DirectX版本号

  你可能认为版本号没有什么好讲的,但我们确实要说一说。Microsoft在DirectX里创建了令人难以置信的科技,但它并不代表不使人迷惑。对于每一个DirectX版本,并不是所有的接口都一次次的升级。因此,尽管DirectX有了7个版本(我写文章时DirectX8.0正准备发布),但DirectDraw并没有7个版本。当DirectX6是最新版本时,DirectDraw的最新接口版本是IDirectDraw4,不是IDirectDraw6。现在最新的版本是DirectX7,所以我们要用IDrectDraw7。很奇怪,是不是?我想你已经明白了我的意思,请不用因为以后看到的感到困惑了。

  最后一件事。当我写这篇文章时,DirectX7是最新的可用版本,但或许现在你已经有了DirectX8,并且或许你还听说了,DirectDraw将不再升级了,取代它的是DirectX Graphics,这是一个功能强大的图形API。但DirectDraw不升级就意味着我们不学习它了,毕竟都离不开COM。如果你想用DirectX8的接口写2D的游戏,你需要用3D方法去创建2D观点。听起来很棒,是的,的确如此,因为使用3D接口将给你更多的硬件支持,例如阿尔发混合。但这也恰恰是个问题,如果机器没有相应的硬件设备,程序会以更慢的速度运行。

  DirectDraw是很容易学的。由于DirectX中的3D图形是基于DirectDraw的,3D应用程序在DirectDraw环境中执行;极少有应用程序专门使用3D。大多数程序使用3D函数对一些对象建模,而另一些对象,诸如背景和精灵,是以2D图形渲染的。所以本系列将使用DirectDraw。关于DirectX8,我还没有太多的了解,因此我只能对DirecX7做详细介绍。总的来说,你使用DirectX,还是离不开DirectDraw的。

  ☆ DirectDraw概述

  在你的程序中使用DirectDraw,你至少要做四件事,它们是:

  1、建立一个DirectDraw对象。

  2、设置协作等级。

  3、设置显示模式和色彩深度(全屏模式)。

  4、至少创建一个DirectDraw表面。

  在讲怎样完成以上步骤之前,先让我们了解一下每一步的含义。第一个,要创建一个DirectDraw对象,这意味着我们要建立一个指向IDirectDraw7接口的指针。这很简单,不是吗?有三种办法可以实现它,你可以直接使用COM,或使用DirectDraw的两个函数之一。三种办法各有千秋,我们过一会儿将详细介绍。第二个,设置操作等级。这可能对你来说比较新鲜。协作是由于Windows是一个多任务操作系统而产生的概念。意思是所有运行的程序都要随时告知Windows它们将要或正在使用的资源,这将保证你所要使用的资源不会被windows再分配给别的应用程序。不用担心,有一个很简单的函数完成它。

  第三个你是否有点熟悉?如果你要写一个全屏的程序,通常是游戏程序,你需要设置显示模式和色彩深度。在Windows的应用程序里做这些通常不是一个好主意,因为它能导致其它程序的同时运行出现问题。当你结束自己的程序时,你当然要恢复到改变前的状态。设置全屏模式,只是调用一个单独的DirectDraw的函数,程序结束后,要恢复原来的状态。

  最后,也是最重要的,是DirectDraw表面的概念。操纵表面是DirectDraw的全部。简单的说,DirectDraw表面是一个用于存储图象数据的线性内存区域。DirectDraw表面的大小就是以象素为单位,用宽和高来定义。所以你可以认为表面是一个用来画图的矩形区域。它有自己的接口,称作IDirectDrawSurface7。有三种主要的表面,我们将在本章和下一章分别用到它们。

  · 主表面:每一个DirectDraw应用程序都有一个主表面。主表面就相当于用户的显示器。它的内容是可见的。同理,主表面就是根据显示器的显示模式设定宽和高。

  · 后缓冲区:后缓冲区是紧随主表面的表面,但它不可见。它是动画没有闪烁的主因,通常,你在后缓冲区画好每一帧,然后把后缓冲区的内容拷贝到主表面,使它显示出来。由于它紧随着主表面,所以它的大小同主表面相同。(你就理解为楼上和楼下的关系)

  · 离屏缓冲区:它很象后缓冲区,只是它不是紧挨着主表面。尽管你可以用它作任何事,但它经常被用来存储位图。离屏缓冲区你可以任意设置大小,唯一的限制是你内存的大小。

  DirectDraw表面可以在系统内存中建立,或直接建立在显示内存中。如果你都建立在显示内存中,速度效果将是最好的,如在系统内存就要慢一些了。如果你把一个表面存储在显示内存中,另一个在系统内存中,性能会有一些损失的,尤其是显示卡与主板之间有一个令人恶心的带宽。总之,如果能把表面都建立在显示内存中,你或许应该尽力做到。

  OK,我们总算有了一点儿认知,让我们看看具体怎么做吧!这儿有一个计划,我们将建立一个全屏模式下,16位色彩,640×480分辨率的程序,我将告诉你全部你需要做的。但开始前,你需要对Windows编程有一点了解。想必你看过了前面几章,应该对创建窗口已经熟悉了。由于这是一个全屏的程序,你不需要任何地窗口控制,所以你的窗口风格应该用WS_POUP|WS_VISIBLE。弄好了吗?All right,出发吧!

  ☆ 建立DIRECTDRAW对象

  象我前面说过的,有三种方法。我们可以用两个DIRECTDRAW函数中的任何一个,或者直接调用COM对象。让我们每一个都试试,使我们自己熟悉它们。我将告诉你的最后一个方法可能是目前为止最简单的,可能你会喜欢用它。至于另外两个,打眼儿一看,你会觉得有些奇怪。首先,看看DirectDrawCreate():

HRESULT WINAPI DirectDrawCreate(
GUID FAR *lpGUID,
LPDIRECTDRAW FAR *lplpDD,
IUnknown FAR *pUnkOuter
);

  看起来是不是有点儿复杂?HRESULT返回的类型是DirectDraw函数的标准。如果成功,返回值是DD_OK。如果失败,函数将返回一个错误常量,有几个错误常量供选择,但我不想细讲,更不想列出这些常量,反正你可以通过帮助文件随时查阅它们。但有一件事儿我得告诉你,有两个非常有用的宏可以帮助你知道函数调用成功与否:SUCCEEDED()和FAILED()。从字面上你就知道它们的分工了,是不是?只要把函数放到宏里面,你就知道结果了。无论如何,我们还得看看函数的参数:

  · GUID FAR *lpGUID:是一个全局唯一标识符(GUID)的地址,代表将要创建的驱动程序。如果该参数是NULL,那么该调用指向当前的显示驱动程序。新版本的DirectDraw允许向该参数传递下列两种标志之一,以控制当前显示的行为:

  ◎ DDCREATE_EMULATIONONLY:DirectDraw只使用仿真(HEL),不使用硬件支持特性。

  ◎ DDCREATE_HARDWAREONLY:DirectDraw对象不使用仿真特性。只能使用硬件抽象层(HAL),如果硬件不能支持应用程序,将不再寻求硬件仿真层(HEL)的支持而返回错误信号。

  · LPDIRECTDRAW FAR *lplpDD:表示如果调用成功则返回有效的DirectDraw对象指针的地址,它是DirectDraw对象指针的指针(“DD”表示DirectDraw,“lp”表示32位长指针,“lplp”表示长指针的长指针)。应用程序一般需要使用此指针的地址(即DirectDraw对象指针)初始化DirectDraw对象。

  · IUnknown FAR *pUnkOuter:这是为高级COM应用保留的参数,设置为NULL好了。

  不要被我罗里罗嗦的解释吓倒,实际应用起来很简单,解释这么多,不过是为了让你明白根本道理。现在有一个问题,这个函数给你一个指向IDirectDraw接口的指针,但我们想要一个指向IDirectDraw7接口的指针,我们应该怎么做呢?一旦DirectDraw应用程序通过DirectDrawCreate()函数获得了指向DirectDraw对象的指针,COM就有一种机制可以用来查看该对象是否支持其它接口。IUnknown的QueryInterface()方法使得你能够确定一个对象是否支持一个特定的接口:

HRESULT QueryInterface(
 REFIID iid, // Identifier of the requested interface
 void **ppvObject // Address of output variable that receives the
);

  第一个参数是一个要查询的对象的引用标识符。对于IDirectDraw7来说就是IID_IDirectDraw7。使用它,你必须把dxguid.lib链接入你的项目中;第二个参数是一个变量的地址,我们应该在程序的头部先声明一个LPDIRECTDRAW7类型的指针,再把指针的地址传递给这个参数。如果你使用的是Visual C++6.0,你在这儿或许还需要一个类型强制符。如果机器支持你指定的接口,函数将返回一个指向该接口的指针。通过该指针,代码就获得对新接口的方法的访问。如果函数调用成功,返回值是S_OK。

  现在我们有了两个接口指针:一个是IDirectDraw接口,另一个是IDirectDraw7。后一个是我们想要的;前一个就没有用了。我们注意,在代码中每当找到一个新的有效对象时,前一个对象就通过Release()函数被释放掉。这个函数很简单:

ULONG Release(void);

  返回的值是一个参考数字,只有在程序测试和调试时才用得着这个数字。为了安全起见,你还应该把释放了的指针赋值为NULL。我们也通常在声明这样的指针时设置它为NULL。你跟上我的节奏了吗?可能要记忆的东西太多了,但是你不得不记忆。让我们把谈到的这些做个实例吧,实例的目的是得到IDirectDraw7接口的指针:

LPDIRECTDRAW lpdd = NULL; // pointer to IDirectDraw (temporary)
LPDIRECTDRAW7 lpdd7 = NULL; // pointer to IDirectDraw7 (what we want)

// get the IDirectDraw interface pointer
if (FAILED(DirectDrawCreate(NULL, &lpdd, NULL)))
{
 // error-handling code here
}

// query for IDirectDraw7 pointer
if (FAILED(lpdd->QueryInterface(IID_IDirectDraw7, (void**)&lpdd7)))
{
 // error-handling code here
}
else
{
 // success! release IDirectDraw since we don t need it anymore
 lpdd->Release();
 lpdd = NULL;
}

  现在,如果你是一个C程序员,你可能被调用QueryInterface()和Release()这两个函数的方法弄得有点模糊。你以前可能看过“->”这个符号,在C语言的结构部分,当结构声明了一个指针变量,调用结构成员时,就用“结构指针名->结构成员”,同样的道理,只是这里把结构成员换成了函数。既然说到这个话题,我就介绍一下另一个C++符号,范围定义符号“::”,它是表示从属关系的符号,举个例子你就明白了:比如QueryInterface()函数是属于IUnknown类的,就可以表示为IUnknown::QueryInterface()。我们将来会经常用到这个符号的,所以记住它。

  说实在的,以上的主要目的是为了演示怎样使用QueryInterface()方法,它是所有DirectX接口的一部分,所以让我们往下进行。我们将直接使用COM方法获得接口指针。这种方法的好处是你可以立即获得IDirectDraw7接口指针,不用象刚才那么麻烦。首先,你必须得初始化COM,象这样:

HRESULT CoInitialize(LPVOID pvReserved);

  不能在容易了,你必须把参数设置为NULL。当你结束COM调用,你需要抛弃它,也很简单:

void CoUninitialize(void);

  我通常在DirecX程序的一开始就调用CoInitialize()函数,在程序的最末端,当我释放了所有的DirectX对象后,使用CoUninitialize()。一旦你初始化了COM,你就可以用CoCreateInterface()函数得到你想要的指针,它看起来有点丑陋:

STDAPI CoCreateInstance(
 REFCLSID rclsid, // Class identifier (CLSID) of the object
 LPUNKNOWN pUnkOuter, // Pointer to whether object is or isn t part
 // of an aggregate
 DWORD dwClsContext, // Context for running executable code
 REFIID riid, // Reference to the identifier of the interface
 LPVOID *ppv // Address of output variable that receives
); // the interface pointer requested in riid

  如果成功,返回值是S_OK。参数需要好好解释一下,看下面:

  · REFCLSID rclsid:这是一个类标识符(不要同GUID搞混了哦),有为它准备好的常量标识符供你选择。对于IDirectDraw7来说,使用CLSID_DirectDraw。注意没有版本号,因为它是类标识符,不是接口标识符。

  · LPUNKNOWN pUnkOuter:这个同我们在DirectDrawCreate()中看到的一样,设置为NULL。

  · DWORD dwClsContext:这个必需的值叫作执行上下文,它定义了控制新生成对象的代码将要执行的方式。这个值可以从CLSCTX列表中选取,对于我们现在的情况,我们用CLSCTX_ALL,它包含了所有可能的值。

  · REIID riid:我们在QueryInterface()中看过它。这个IID是IID_DirectDraw7。

  · LPVOID *ppv:依然同DirectDrawCreate()中的一样,是指向接口指针的地址。

  调用这个函数将取代我们上一个方法中的DirectDrawCreate()、QueryInterface()和Release()三个函数,所以简捷一些。当然,使用哪种随便你了。直接调用COM比我们先前用的方法少了一个多于地接口指针。一旦你用CoCreateInstance()建立了一个对象,你还得调用Initialize()函数初始化这个对象。在C++里可能写成这样IDirectDraw7::Initialize()。以下是它的原形:

HRESULT Initialize(GUID FAR *lpGUID);

  将使用同DirectDrawCreate()中一样的GUID,就是NULL。在我们继续前,让我给你看一个使用COM创建DirectDraw对象的例子:

LPDIRECTDRAW7 lpdd7; // interface pointer

// initialize COM
CoInitialize(NULL);

// create the object
CoCreateInstance(CLSID_DirectDraw, NULL, CLSCTX_ALL, IID_IDirectDraw7, (void**)&lpdd7);

// initialize the object
lpdd7->Initialize(NULL);

  直接看例子可能使你更容易理解一些。好了,建立DirectDraw对象的最难的两种方法你已经学会了,那就让我们看看最简单的方法吧! 它只有一步,没有多于的接口指针,不用设置COM,什么都没有。就是下面这个函数:

DirectDrawCreateEx(
 GUID FAR *lpGuid,
 LPVOID *lplpDD,
 REFIID iid,
 IUnknown FAR *pUnkOuter
);

  所有的参数我们看起来都比较熟悉,因为我们刚才看过它们了。第一个,第二个和第四个参数同DirectDrawCreate()中的一样,只是这里需要用(void**)来修饰一下我们接口指针的地址——别问我为什么,这不是我的主意。第三个参数,riid,是我们在函数CoCreateInstance()中传递的接口ID,所以我们就用IID_IDirectDraw7。就这样,无论用哪种方法,我们得到了我们的DirectDraw对象,我们可以继续使用这个对象了。要做的头两件事是设置协作等级和显示协议。

  ☆ 设置协作等级和显示模式

  我不需要说太多。Windows编程设置协作级别你只需要调用IDirectDraw7::SetCooperativeLevel()函数;设置显示模式你就调用IDirectDraw7::SetDisplayMode()函数。就这么简单!先来看看协作级别。这就是函数原形:

HRESULT SetCooperativeLevel(
 HWND hWnd,
 DWORD dwFlags
);

  返回的类型是HRESULT,你应该已经熟悉它了。对于所有的DirectX函数调用,你都可以用SUCCEEDED()和FAILED()宏检测调用的结果。以下是函数SetCooperativeLevel()的参数:

  · HWND hWnd:很熟悉吧!传递主窗口的句柄给它,使Windows知道谁将使用它的资源。

  · DWORD dwFlags:这个也很眼熟吧!每次我们看到dwFlags参数,几乎都有一个大的标志常量列表供我们选择,并且可以用“|”组合。这次也不会让你失望的哦!

  ◎ DDSCL_ALLOWMODEX:启用Mode X 显示模式(如320×200,320×240或者320×400)。该标志只能用于DDSCL_EXCLUSIVE和DDSCL_FULLSCREEN模式。

  ◎ DDSCL_ALLOWREBOOT:在独占模式中启用Ctrl+Alt+Del组合键功能。

  ◎ DDSCL_EXCLUSIVE:请求独占模式,必须与DDSCL_FULLSCREEN同时使用。

  ◎ DDSCL_FULLSCREEN:独占模式的拥有者负责整个主表面,GDI被忽略,必须与DDSCL_EXCLUSIVE同时使用。

  ◎ DDSCL_NORMAL:表示常规的Windows应用程序,不能与DDSCL_ALLOWMODEX、DDSCL_EXCLUSEIVE或DDSCL_FULLSCREEN标志同时使用,在该模式下运行的应用程序不能进行页交换或者更改主调色板。

  ◎ DDSCL_NOWINDOWCHANGES:防止DirectDraw最小化或恢复应用程序窗口。
 
  还有几个标志常量我们暂时用不到,就不说了。由于我们要建立一个全屏的640×480×16的显示模式,所以我们得这样设置:

lpdd7->SetCooperativeLevel(hwnd, DDSCL_ALLOWREBOOT | DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);

  现在协作级别已经设置好了,让我们再看看改变显示模式的函数:

HRESULT SetDisplayMode(
DWORD dwWidth,
DWORD dwHeight,
DWORD dwBPP,
DWORD dwRefreshRate,
DWORD dwFlags
);

  别忘了用宏去检测调用函数的成功或失败!大多数的参数同你料想的差不多:

  · DWORD dwWidth,dwHeight:以象素为单位,新显示模式的尺寸。

  · DWORD dwBPP:新显示模式的色彩深度。就是每一个象素有多少位字节。可以设置为8,16,24或32。警告:很多显示卡不支持24-bits。

  · DWORD dwRefreshRate:屏幕的刷新频率。但你最好设置为0,使用默认的刷新频率。

  · DWORD dwFlags:对不起,这次没有列表了^_^,唯一的选择是DDSDM_STANDARDVGAMODE,它把显示模式设置为0x13(DOS程序员的好朋友),取代了Mode X的320×200×8的模式。如果你还想使用其它的模式(你可能经常需要),没有问题,把它设置为0好了。

  这些就是显示模式的设置,事先最好了解你的显示卡支持的显示模式,它们通常都支持640×480,800×600,1024×768等等,这些都是标准的模式。但是如果你非得设置成542×366的模式,你可能就会得到错误的反馈。科技在发展吗,什么都是可能的。让我们继续吧!

  ☆ 创建表面

  这一次,我们需要比调用一个函数多一点点的东东。创建表面不是很难的,实际上,也是由一个单独的函数完成的,但是首先你要填充一个描述你所要创建的表面的结构。给你看这个结构之前,我只想告诉你,你不必填满所有的成员。^_^这就是它,DDSURFACEDESC2:

typedef struct _DDSURFACEDESC2 {
 DWORD dwSize;
 DWORD dwFlags;
 DWORD dwHeight;
 DWORD dwWidth;

 union
 {
  LONG lPitch;
  DWORD dwLinearSize;
 } DUMMYUNIONNAMEN(1);

 DWORD dwBackBufferCount;

 union
 {
  DWORD dwMipMapCount;
  DWORD dwRefreshRate;
 } DUMMYUNIONNAMEN(2);

 DWORD dwAlphaBitDepth;
 DWORD dwReserved;
 LPVOID lpSurface;
 DDCOLORKEY ddckCKDestOverlay;
 DDCOLORKEY ddckCKDestBlt;
 DDCOLORKEY ddckCKSrcOverlay;
 DDCOLORKEY ddckCKSrcBlt;
 DDPIXELFORMAT ddpfPixelFormat;
 DDSCAPS2 ddsCaps;
 DWORD dwTextureStage;
} DDSURFACEDESC2, FAR *LPDDSURFACEDESC2;

  坦率的说,编写DirectDraw的应用程序其实并不难。但是事情往往是这样,80%的工作只需要我们花费20%的时间就可以完成,而剩下的20%的工作却需要我们花费80%的时间来完成。DirectDraw编程比这还要严重,就笔者的看法,至少90%的工作只需要我们不到10%的时间来完成,而剩下的不到10%的工作却至少需要我们90%的时间!结构DDSURFACEDESC就是10%的一部分,它较为复杂,它嵌套了其它的结构。让我们看看这个怪物到底做了什么。我只说说重点的部分:

  · DWORD dwSize:任何DirectX结构都有dwSize这个成员,表示结构的大小。有了它,当函数接收到指向这些结构的指针时,就可以测定结构的大小了。

  · DWORD dwFlags:太好了,又有一大堆标志常量了^_^ !这些标志告诉接收函数哪些数据成员是有效的。要想使需要的数据成员有效,就必须传递相对应的标志常量给dwFlags,你当然可以用“|”组合它们。以下是列表:

  ◎ DDSD_ALL:所有的数据成员都有效。
  ◎ DDSD_ALPHABITDEPTH:表示数据成员dwAlphaBitDepth有效。
  ◎ DDSD_BACKBUFFERCOUNT:表示数据成员dwBackBufferCount有效。
  ◎ DDSD_CAPS:表示数据成员ddsCaps有效。
  ◎ DDSD_CKDESTBLT:表示数据成员ddckCKDestBlt有效。
  ◎ DDSD_CKDESTOVERLAY:表示数据成员ddckCKDestOverlay有效。
  ◎ DDSD_CKSRCBLT:表示数据成员ddckCKSrcBlt有效。
  ◎ DDSD_CKSRCOVERLAY:表示数据成员ddckCKSrcOverlay有效。
  ◎ DDSD_HEIGHT:表示数据成员dwHeight有效。
  ◎ DDSD_LINEARSIZE:表示数据成员dwLinearSize有效。
  ◎ DDSD_LPSURFACE:表示数据成员lpSurface有效。
  ◎ DDSD_MIPMAPCOUNT:表示数据成员dwMipMapCount有效。
  ◎ DDSD_PITCH:表示数据成员lPitch有效。
  ◎ DDSD_PIXELFORMAT:表示数据成员ddpfPixelFormat有效。
  ◎ DDSD_REFRESHRATE:表示数据成员dwRefreshRate有效。
  ◎ DDSD_TEXTURESTAGE:表示数据成员dwTextureStage有效。
  ◎ DDSD_WIDTH:表示数据成员dwWidth有效。

  · DWORD dwheight,dwWidth:表示要创建表面的尺寸。以象素为单位。

  · LONG lPitch:这个需要好好解释一下。lPitch表示从画面一行行首数据到下一行行首数据的距离,以字节为单位。例如,640×480×16,每一行有640个象素,每个象素需要两个字节装颜色的信息,所以pitch应该是1280个字节,对不对?可能有一些显示卡要多于1280,这每行多于的内存没有装置任何的图形数据,但是防备有些显示卡不能在线性内存模式显示图形,你还是把多于地放在那吧。这种情况很少发生,但你最好还是考虑在内。

  · LPVOID lpSurface:指向表面内存开始地址的指针。不管你使用什么显示模式,你都可以用DirectDraw创建的线性地址模式操作表面象素。要想这样,你必须锁住表面,但这已经超出我们现在所学的了。

  · DWORD dwBackBufferCount:后缓冲区的数目。以后我们会在提到它。

  · DWORD ddckCKDestBlt,ddckCKSrcBlt:前者为描述位转换操作的目标颜色值,后者是源颜色值。我们将在以后的文章中具体介绍。

  · DDPIXELFORMAT ddpfPixelFormat:这个结构包含了描述显示模式的象素格式标识符。以后会具体介绍,现在就不多说了。

  · DDSCAPS2 ddsCaps:这是最后一个重要的结构。它是一个充满控制标志的结构。感谢菩萨,这是一个小结构,结构成员中只有一个很重要。让我们看一看:

typedef struct _DDSCAPS2{
 DWORD dwCaps;
 DWORD dwCaps2;
 DWORD dwCaps3;
 DWORD dwCaps4;
} DDSCAPS2, FAR* LPDDSCAPS2;

  最重要的就是dwCaps了。第三个和第四个成员从来没有用过,是为将来准备的。总之,dwCaps可以使用如下的值,当然可以用“|”组合。以下是最为常用的,其它的你若有兴趣,自己查好了。

  · DDSCAPS_BACKBUFFER:指出这个表面是需要表面切换结构的后缓冲区。

  · DDSCAPS_COMPLEX:是一个复杂表面,由主表面,一个或多个粘贴表面组成,通常是为了页面切换。

  · DDSCAPS_FLIP:指出这个表面是表面切换结构的一部分。前缓冲区紧跟着一个或多个建立好的后缓冲区。

  · DDSCAPS_FRONTBUFFER:是关于表面切换结构的前缓冲区。

  · DDSCAPS_LOCALVIDMEM:指出在true、local video memory【不知怎么翻译】中建立表面。如果使用该标志,必须也同时使用DDSCAPS_VIDEOMEMORY标志,但不能同DDSCAPS_NONLOCALVIDMEM标志同时使用。

  · DDSCAPS_MODEX:指出这个表面是Mode X模式(320×200或320×240)的表面。

  · DDSCAPS_NONLOCALVIDMEM:指出表面建立在non-local video memory【不知怎么翻译】中。如果定义该标志,必须也同时使用DDSCAPS_VIDEOMEMORY标志。但是不能同DDSCAPS_LOCALVIDMEM同时使用。

  · DSCAPS_OFFSCREENPLAIN:这是一个简单的离屏表面。

  · DDSCAPS_OWNDC:这个表面将具有长周期的设备上下文。

  · DDSCAPS_PRIMARYSURFACE:主表面。

  · DDSCAPS_STANDARDVGAMODE:是标准的VGA模式表面。不能同DDSCAPS_MODEX同用。

  · DDSCAPS_SYSTEMMEMORY:建立在系统内存里的表面。

  · DDSCAPS_VIDEOMEMORY:这个表面建立在显示内存里。

  天啊,终于介绍完了这个结构。现在我们准备建立表面吧。第一步当然是填充DDSURFACEDESC2结构。Microsoft推荐大家当你使用一个结构之前,你应该把它先初始化为0。有鉴于此,我经常使用这样一个宏:

#define INIT_DXSTRUCT(dxs) { ZeroMemory(&dxs, sizeof(dxs)); dds.dwSize = sizeof(dxs); }

  它可以用于任何一个DirectX结构,因为它们都有dwSize成员,所以这是很方便的。如果你以前从来没有看过ZeroMemory()这个函数,它只是由函数memset()扩充来的宏,在Windows的头文件中用#define定义好了,所以你不需要用#indlude添加任何东西就可以用它。
初始化了结构之后,你得根据实际情况设置表面了。对于主表面,你需要ddsCaps和dwBackBufferCount,对于离屏缓冲区,你也需要dwHeight和dwWidth,但不需要dwBackBufferCount。对于一些表面你可能还需要颜色值,但我们不把它弄得太复杂了。填充完结构后,你需要调用IDirectDraw7::CreateSurface()函数,原形是这样:

HRESULT CreateSurface(
 LPDDSURFACEDESC2 lpDDSurfaceDesc,
 LPDIRECTDRAWSURFACE7 FAR *lplpDDSurface,
 IUnknown FAR *pUnkOuter
);

  这些参数的意义可能你也能猜出个大概了,毕竟我们已经习惯了这些疯狂的DirectX素材:

  · LPDDSURFACEDESC2 lpDDSurfaceDesc:表示要创建表面的描述结构。当然是个指针了。

  · LPDIRECTDRAWSURFACE7 FAR *lplpDDSurface:为指向表面指针的指针。此参数在此函数调用成功后填充。为什么要使用指向指针的指针呢?这是因为我们的任务就是分配一片表面内存区域,这样只能使用指针(表面指针)作为操作该表面内存区域的标志,返回值应该是该指针值而不是该指针所表示的内容(具体的表面)。当我们使用函数参数传递该值时,又只能使用指针(即指针的指针)修改表面指针的内容而不是表面指针所代表的表面内存区域。(理论复杂,使用简单,不明白不要太在意)

  · IUnknown FAR *pUnkOuter:看过这个模式吧,无论何时调用pUnkOuter,都是关于COM应用的,我们不想在这儿浪费时间,设置为NULL好了。

  来个实例吧,你会明白一切的。希望在实例里,我们要一个主表面和一个紧随主表面的后缓冲区,还有一个离屏缓冲区用来放置位图。假设我们已经得到了IDirectDraw7接口指针,代码如下:

DDSURFACEDESC2 ddsd; // surface description structure
LPDIRECTDRAWSURFACE7 lpddsPrimary = NULL; // primary surface

// set up primary drawing surface
INIT_DXSTRUCT(ddsd); // initialize ddsd
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; // valid flags
ddsd.dwBackBufferCount = 1; // one back buffer
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | // primary surface
DDSCAPS_COMPLEX | // back buffer is chained
DDSCAPS_FLIP | // allow page flipping
DDSCAPS_VIDEOMEMORY; // create in video memory

// create primary surface
if (FAILED(lpdd7->CreateSurface(&ddsd, &lpddsPrimary, NULL)))
{
 // error-handling code here
}

  你当然还可以用CreateSurface()函数创建复杂表面,只是使用DDSCAPS_COMPLEX标志罢了。由于刚才我们创建了一个后缓冲区,所以我们还得必须得到指向它的指针。那就得调用IDirectDrawSurface7::GetAttachedSurface()函数了:

HRESULT GetAttachedSurface(
 LPDDSCAPS2 lpDDSCaps,
 LPDIRECTDRAWSURFACE7 FAR *lplpDDAttachedSurface
);

  参数很简单啦:

  · LPDDSCAPS2 lpDDSCaps:指向创建后缓冲区表面的DDSCAPS2结构。你就可以使用DDSCAPS2结构中相应的成员了。

  · LPDIRECTDRAWSURFACE7 FAR *lplpDDAttachedSurface:后缓冲区表面指针的地址。简单理解为声明一个指针,然后把指针的地址传递给该参数。

  看看下面的代码就明白了:

LPDIRECTDRAWSURFACE7 lpddsBack = NULL; // back buffer

// get the attached surface
ddsd.ddsCaps.dwCaps = DDSCAPS_BACKBUFFER;
if (FAILED(lpddsPrimary->GetAttachedSurface(&ddsd.ddsCaps, &lpddsBack)))
{
 // error-handling code here
}

  感觉有点儿入门了吗?如果你很难记住以上步骤,那么你是一个正常人,反复运用就会熟悉了。没有人能记住所有的庞大的结构成员和标志常量,这就是我们手边总是准备程序员参考手册或者拥有MSDN Library CD的原因了^_^ !OK,最后一步是建立离屏缓冲区。假设它的宽400,高300,(单位是象素)代码如下:

LPDIRECTDRAWSURFACE7 lpddsOffscreen = NULL; // offscreen buffer

// set up offscreen surface
INIT_DXSTRUCT(ddsd); // initialize ddsd
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT; // valid flags
ddsd.dwWidth = 400; // set width
ddsd.dwHeight = 300; // set height
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | // offscreen buffer
DDSCAPS_VIDEOMEMORY; // video memory

// create offscreen buffer
if (FAILED(lpdd7->CreateSurface(&ddsd, &lpddsOffscreen, NULL)))
{
 // error-handling code here
}

  表面这些学问就介绍到这儿,还有好多东西要介绍,可是唯一的问题是文章太长了,我们先暂停吧。你现在可以建立一个最基本的,但是什么也不显示的表面。

  千万记住了,你使用的每一个DirectDraw接口和所有的表面,用完后一定要释放(Release)它们啊!切记、切记!!!!!!!

  ☆ 总结

  很抱歉在这里中断了,尤其是你还没有看到显示的图形,但关于图形有太多的内容了,不是三言两语就能说清除的,所以放到下两章。下一章讨论DirectDraw中的调色板和象素,再下下一章讨论DirectDraw中的位图。精彩在后面哦!请耐心期待。