DirectDraw 游戏编程基础

微软公司供稿
内 容

1. 简介
2. 使用 DirectX 3 SDK的基本环境
3. DirectDraw API
4. DirectDraw, OLE, 和COM接口
5. DirectDraw 起步
6. DirectDraw 的基本知识 (DDEX1)
7. 例一的扩展(DDEX2和DDEX3)
8. 生成简单的动画(DDEX4和 DDEX5)
9. 检验Duel例子
10. 关于最优化(Optimizations)和规范化(Customizations)
11. 下一步你应该干什么

简 介
DirectDraw是微软新近发行的DirectX 3软件开发工具箱(SDK)中的一部分。对那些不甚了解的人来说,DirectX 3 SDK是原来称为游戏SDK的最新版本。和在游戏SDK中一样,DirectX 3 SDK包含了一组动态链接库,用来图形加速,3D图形服务,声音加速,扩展连接,游戏杆操纵以及CD-ROM自动化。
关于DirectX 3 SDK可以有很多的话题讨论,本篇只涉及如何用DirectDraw来编写游戏中的图形部分。尽管需要一些有关OLE和构件模型(COM)接口的基础知识,整个过程还是相当简单的。而且,所需的有关OLE和COM接口的知识也将在此讨论到。
读完本篇后,您就可以用DirectDraw来写简单的游戏了。在这里,我只想讨论如何使用最基本的DirectDraw函数。所举的例子仅涉及到全屏和翻页,不讨论如何在窗口中使用DirectDraw,在3维图中使用文字,使用视频剪接或是用Direct3D生成表面。如果对这些内容感兴趣,可以参阅有关的文档。

使用 DirectX 3 SDK的基本环境
DirectX 3 SDK可以在Windows 95或Windows NT4.0中使用。基于本文的目的,假定使用的环境是IBM PC和Windows 95.另外,还需要安装DirectX 3 SDK,以便编译连接应用。
同时,使用的C或 C++ 编译器应能生成32位的应用。您也可以使用其他的语言,但这里并不想涉及。当然您还应当具有Windows的编程技术。
如果使用的是C编译器,则还必须包含Win32 SDK。Win32 SDK包含了生成可执行程序时的一些库。

DirectDraw API
DirectDraw是整个软件开发工具箱的一个构件。DirectDraw是为速度而设计的,它绕过与Windows的图形设备相连的多个层次,直接与硬件的底层打交道。这很适合游戏编程来,因为它着重于快速产生平稳的图形。
但DirectDraw最重要的一点在于它对不同的显示适配器具有一个共同的接口。您不必管您的程序它是否会工作。DirectDraw利用包含在硬件抽象层(HAL)中的信息来决定显示适配器的功能。(HAL是由显示适配器厂商提供的)HAL为不同的硬件厂商和使用.DirectDraw的开发者提供了共同的接口。
然而,DirectDraw并不只限于利用显示适配器的硬件功能。如果您的程序指定了某一种特定的显示适配器,例如XXXX hardware blitter,但用户并没有该硬件,程序就会使用DirectDraw的硬件仿真层(HEL)。在这种情况下,DirectDraw利用内建的硬件仿真来仿真缺少的硬件。

图一说明了DirectDraw和其他Windows显示构件的联系。

图1. DirectDraw和其他Windows显示构件

DirectDraw API由DirectDraw对象组成,它表示具体的显示适配器。另外,DirectDraw API还包括表示surface的DirectDrawSurface对象,表示surface调色板的DirectDrawPalette对象和表示剪接列表的DirectDrawClipper对象。可以用DirectDraw对象来创建DirectDrawSurface和DirectDrawPalette对象。(也可用DirectDraw对象来创建DirectDrawClipper对象,但该剪接对象也可被独立地创建。这就涉及到另外的话题了。)创建全屏游戏要用到的只是DirectDraw,DirectDrawSurface和DirectDrawPalette对象。
要了解这些对象是如何工作的,必须对OLE和COM接口有基本的了解。COM接口是所有DirectDraw编程的基础。下一节将讨论COM接口,如果您熟悉OLE,则可跳过它。

DirectDraw, OLE, 和COM接口

DirectDraw是围绕着OLE和COM接口来设计的。如果您不熟悉OLE编程,那将很难开始用DirectDraw编程,这是最基本的一点。尽管在很多书上解释了很多概念,介绍OLE和COM接口是如何工作的,但要用好DirectDraw其实只要了解有限的一些。
首先看看OLE和COM的定义。OLE是微软为在不同的进程和机器间共享信息和服务而引进的基于对象的技术。COM指的是构件对象模型,在OLE编程中,它是接口模型。知道这些概念后就可不理它们了,因为这对我们编程并不重要。我们要继续深入的是为用好DirectDraw所需了解的COM接口的最少的知识。
从本质上说,COM接口由三个成员组成:对象,接口和方法。对象实质上是一个黑箱,不需要管它里面是什么,因为我们只是通过接口来与它打交道。而要完成这一过程,通过的则是方法。如果您有一些硬件知识的话,可以把一个对象想象成一集成电路。与集成电路(对象)通讯是通过管脚(接口)来发送或接收信号(方法)。
所有的COM接口都是从OLE的Iunknown接口派生来的。通过保持Direcdraw对象的引用次数,Iunknown接口维护对象的生存期。另外,它提供了决定对象可用接口的方法。Iunknown接口由三种方法组成:AddRef,Release和QueryInterface。
AddRef和Release负责保持对象的引用计数。当创建了一个对象的实例时,对象的引用计数设为1。只要函数返回了对象接口的指针,该函数必须通过得到的指针调用AddRef,将引用计数增加1。(另外,当另一个应用与该对象相连时,引用计数也要加1,对我们来说,这一点并不重要,因为我们并不将其他的应用连到游戏上。)
当用完一个接口后,要通过指针来调用Release,将引用计数减1。要收回一个对象时,它的引用计数必须为0。当对象的引用计数为0时,对象将被消除,它所有的接口都无效了。
第三个方法,QueryInterface,查询对象是否支持指定的接口。如果对象支持,则返回指向接口的指针。
AddRef,QueryInterface,和Release是如何与DirectDraw相连的呢?首先,我们没有理由在只用DirectDraw的简单游戏中使用AddRef和QueryInterface。DirectDraw创建了函数来负责增加引用计数并返回指向接口的指针。但您仍要对没一个隐式创建的接口指针执行Release操作。如果没有维护好对象的引用计数,将很容易引起内存泄露。在后面编码的例子中,将介绍如何进行。(也可在将应用与另一应用使用的DirectDraw对象相连时,使用AddRef和QueryInterface。同样,如果使用Direct3D,就不得不用QueryInterface来返回指向Direct3D接口的指针。这些内容都不在此讨论。)

下面看一个使用IDirectDraw接口方法的C程序行。
Ddrval = lpDD->lpVtbl->SetDisplayMode( lpDD, ScreenX, ScreenY, ScreenBpp );

程序用了SetDisplayMode方法来设置显示模式,返回值表示成功或失败。在此重要的一点是如何取得指向方法的指针,不能直接访问IDirectDraw接口方法。当一对象实例化时,它生成一个虚函数表,称为vtable,它包含了指向该对象接口方法的所有指针。在上例中,指向DirectDraw对象的指针(lpDD)指向了指向vtable的指针(lpVtbl),vtable中包含了对象所有方法的指针,具体地说,本例中指向了SetDisplayMode方法。应用和方法接口间的联系可以清楚地看成这样:

下一个例子看看如何用C++来做:
ddrval = lpDD->SetDisplayMode( ScreenX, ScreenY, ScreenBpp );
注意vtable不再显式地使用,C++自动地将对象(lpDD)作为第一个参数。不需要this指针,因为C++用指向当前对象的指针(本例中为lpDD)隐式地执行该方法。
如果想要了解更多的关于OLE和COM接口的知识,可以参阅Kraig Brockschmidt写的“Inside OLE”(可在MSDN中得到)。这是我所知的关于OLE和COM的最好的书。我推荐您去阅读该书的第一部分和第二部分的前半部分,以对COM接口有一个清楚的了解。这对您很好地理解DirectDraw和COM接口间的关系就已足够了。
开始使用DirectDraw
前面已经说过应当安装了DirectX SDK和C或C++编译器。在这我们假定使用的是Microsoft Visual C++ 4.0,并已安装在缺省的目录下。如果使用的是其他的编译器或软件安装在其他的目录下,则必须对下面的例子做相应的修改。
由于所涉及的是DirectDraw的基本知识,因此这里使用的例子都是关于DirectX 3 SDK的简单例子。它们示例了如何设置DirectDraw以及使用DirectDraw方法来执行简单的操作。学完这些例子后,你将能比较容易地理解DirectX 3 SDK光盘上所带的更为复杂的例子。
在正式开始前,还必须设置好编译环境,这取决于你将如何使用Visual C++来编译这些例子。我将示范如何用Microsoft Developer Studio来设置这些环境,或是在命令行下使用NMAKE。

安装微软开发工作室(Micorsoft Developer Studio):
Visual C++为那些喜欢使用接口的程序员提供了微软开发工作室(Microsoft Developer studio)。在开始编辑例程Directx 3 SDK前,需要打开一个新project 工作区,然后插入适当的文件,再设置一系列的环境变量参数,以使编辑器能够找到正确的库文件和头文件。下面我们要讨论例程DDEX1。以下所列步骤说明如何创建DDEX1:
当你打开Microsoft Developer Studio 后,可使用以下步骤来创建project工作区:
1. 在菜单 File 中,选择选项New。
2. 在新对话框中,选择 Project Workspace(项目工作区),然后单击 OK。
3. 在Type list 中选择选项 Application。
4. 在Location box(搜索框),你可以通过选择一个路径来查找你所需的项目
5. 在 Name 文本框中,键入DDEX1。
6. 单击Create后,一个名为DDEX1 Classes 的新文件夹出现在工作区窗口的
左部。
这样你就创建好了一个项目工作区,使用以下步骤使你在该工作区插入一个适当的文件:
1. 在菜单 Insert 中,单击选项 Files Into Project。
2. 浏览目录:DXSDK/SDK/SAMPLES/DDEX1,并选择该目录下的所有文件。
3. 单击 Add 选项,则工作区窗口左侧的文件夹就会变成DDEX1。
4. 单击folder name(文件夹)左侧的"+",可查看到DDEX1文件夹中的文件列表。
为了能够正确地编辑和链接 DirectDraw 例程,还需完成以下步骤为头文件设置正确的路径:
1. 在菜单 Tools 中,单击 Options,则Options对话框就会被击活。
2. 选择 Directories 选项。
3. 在列出的目录中,选择头文件所在的目录。
4. 在目录框中,双击列表底部的空白行(以长方形表示),然后键入: C:/DXSDK/SDK/INC。
5. 按回车键。
6. 在目录框中,双击列表底部空白行并键入: c:/DXSDK/SDK/SAMPLES/MISC。
7. 单击 OK 选项。
接下来,按以下步骤为库文件选择路径:
1. 在目录列表框中,选择库文件所在的目录。
2. 选择 Link 选项
3. 在目录下拉盒中,找到 General 选项
4. 在 Objext/library 模块下拉盒中,加入 Draw.lib 和 Winmm.lib
5. 单击 OK 选项。

上面所列出的大多数步骤在创建新 Project 的过程中都会用到。其中目录路径和链接模块被永久地加入到环境参量中。
为 NMAKE 设置路径:
如果你更喜欢使用 NMAKE,你需要为库模块和头文件设置正确的路径。下面列出了 Visual C++ 4.0 所用到的所有路径:
@echo off
set path=c:/MSDEV/BIN;%PATH%
set
INLUDE=C:/MSDEV/INCLUDE;C:/MSDEV/MFC/INCLUDE;C:/DXSDK/SDK/INC;%I
NCLUDE%
set LIB=C:/MSDEV/LIB;C:/MSDEV/MFC/LI;C:/DXSDK/SDK/LIB;%LIB%
set INIT=C:/MSDEV/;%INIT%
在批处理 autoexec.bat 中加入以上各行,并重新启动计算机,你就可以方便地编辑你的程序了。(如果你使用的是Microsoft Visual C++ 2.0, 则你需要把上面的 MSDEV 改为 MSVC20,且其他路径均不变)
为了实际编辑一个程序,进入该程序子目录(例如:C:/DXDSK/SDK/SAMPLES/ DDEX4),并键入:
NMAKE
这将在当前目录下创建一个DEBUG子目录,并且在该子目录下生成可执行文件。

例程1(DDEX1):DirectDraw 的基本知识
在使用 DirextDraw时,需要首先创建一个对象DirectDraw 的实体,该对象实体代表了微机显示适配器。然后,使用接口所提供的方法来操作该对象实体,使之完成有关命令和任务。接着,你还需要创建一个或多个 DirectDraw-surface对象的实体,以便能在图形表面(Surface)上展示你的游戏画面。
下面,在例程 DDEX1 中展示如何使用Directx 3 SDK来 DirectDraw对象实体,如何创建一个带有后台缓冲区的基本表面(Surface),以及如何弹出表面(Surface)。

注意:所有的例程都是用C++写成的,如果你的编辑器是C,你需要在文件中作出某些改动(至少,你要加入 Vtable 和指向各种接口方法的 this 指针)。
DirectDraw 初始化:
DirectDraw 初始化代码写在例程 DDEX1 的 doInit 函数中。

/*
* Create the main DirectDraw object.
*/
ddrval = DirectDrawCreate(NULL,&lpDD,NULL);
if(ddrval==DD_OK)
{
//Get exclusive mode.
Ddrval=lpdd->SetCooperativeLevel(hwnd,
DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN);
if(ddrval==DD_OK)
{
//Create the primary surface with 1 back buffer.
Ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS / DSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
DDSCAPS_FLIP |
DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 1;
ddrval = lpDD->CreateSurfae(&ddsd, &lpDDSPrimary,
NULL);
if(ddrval == DD_OK)
{
//Cet a pointer to the back buffer.
Ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps,
&lpDDSBack);
if( ddrval == DD_OK)
{ // Draw some text.
If(lpDDSPrimary->GetDC(&hdc) == DD_OK)
{
SetBkColor(hdc, RGB(0,0,255));
SetTextColor( hdc,RGB(255,255,0 ) );
TextOut( hdc, 0, 0, sxFrontMsg, lstrlen(szFrontMsg ));
lpDDSPrimary->ReleaseDC(hdc);
}
if(lpDDSBack->GetDC(&hdc) == DD_OK)
{
SetBkColor( hdc, RGB(0, 0, 255 ) );
SetTextColor( hdc, RGB( 255,255, 0 ) ):
TexOut( hdc, 0, 0, szBackMsg, lstrlen( szBackMsg ) );
lpDDSBack->ReleaseDC(hdc);
}
// Create a timer to flop the pages.
If(SetTimer( hwnd, TIMER_ID, TIMER_RATE, NULL))
{
return TRUE;
}
}
}
}
}
}
wsprintf(buf,"Direct Draw Init Failed (%08lx)/n",ddrval);
.
.
.

以下针对初始化 DirectDraw 对象和准备表面(Surface)集的各个步骤分别进行讨论:

创建一个 DirectDraw 对象
为了创建一个 DirecDraw 对象实体,你应该在程序中使用DirectDrawCreate API 函数(注意:这里我所说的是应该,而不是必须), 这是因为使用 OLE 中的 CoCreatelnstance 函数也能创建一个 DirectDraw 对象实体,但这不在我们的讨论范围之中)。DirectDrawCreate 采用全球统一的标准,它代表显示设备,这些显示设备在大多数情况下被定为 NULL (即:系统使用缺省的显示设备)。 当DirectDraw对象实体创建好后,就会有一个指针指向该对象实体。而且,在调色板中有三分之一的指针指向 NULL (这样做的目的是为了今后的扩展)。
接下来的例子说明如何创建一个DirectDraw 对象,并判别该对象是否创建成功:
ddrval = directDrawCreat( NULL, &lpDD, NULL );
if( ddrval == DD_OK )
{
//lpDD is a valid DirectDraw object.
}
else
{
//DirectDraw object could not be created.
}

使用 IDirectDraw2 和 IDirectDrawSurface2 接口

在你读本文的其他部分时,你会注意到所有的例程都使用的是 IDirectDraw和 IDirectDrawSurface 的老版本接口。这是因为 DirectX 3 SDK 所给出的例程还没有来及使用 IDirectDraw 和 IDirectDrawSurface 更新后的接口。你可以通过调用 IDirectDraw::QueryInterface 方法来得到 IDirectDraw2 和IDirectDrawSurface2 接口。
下面的代码给出如何得到 IDirectDraw2 接口:

// Create an IDirectDraw2 interface.
LPDIRECTDRAW lpDD;

ddrval = DirectDrawCreate( NULL, &lpDD, NULL);
if(ddrval != DD_OK)
return;
ddrval = lpDD->SetCooperativeLevel(hwnd, DDSCL_NORMAL);
if(ddrval != DD_OK)
return;
ddrval = lpDD->QueryInterfave(IID_IDirectDraw2, (lPVOID *)&lpDD2);
if(ddrval !=DD_OK)
return;

下面的代码给出如何得到 IDirectDrawSurface2 接口:
LPDIRECTDRAWSURFACE lpSurf;
LPDIRECTDRAWSURFACE lpSurf2;

// Create surfaces.
Memset( &ddsd, 0, sizeof(ddsd ));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS |DDSD_WIDTH |DDSD_HEIGHT;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN |
DDSCAPS_SYSTEMMEMORY;
ddsd.dwWidth = 10;
ddsd.dwHeight = 10;

ddrval = lpDD2->CreateSurface( &ddsd, &lpSurf, NULL);
if(ddrval != DD_OK)
return;

ddrval = lpSurf->QueryInterface(
IID_IDirectDrawSurface2, ( LPVOID *(&lpSurf2);

if(ddrval !=DD_OK)
return;

设置显示模式
安装DirectDraw 的下一步是设置显示模式。在DirectDraw应用程序中设置显示模式关键有两个步骤:第一,调用Idirectdraw::SetCooperativelevel方法设置底层参数。设置好底层参数后,再调用 IDirectDraw::SetdisplayMode方法来设置显示方式。

确定应用程序的运行特征:
如果你想改变你的显示方式 ,你必须先在调色板的 dwFlags 中设置 DDSCL_ EXCLUSIVE 和 DDSCL_FULLSCREEN 标志。其中,调色板的dwFlags包含在IDirectDraw::SetCooperativelLevel 方法中。这样一来,你的应用程序就可以单独占用显示设备,而使其它进程不能共享显示设备。 另外 , 标志DDSCL_FULLSCREEN 把显示设备置为全屏幕方式。正如在运行DDWX1时见到的那样,当同时按下ALT键和TAB键后,最初的表面(Surface)(尽管仍然有效)会被你的表面(Surface)所覆盖,且只有你的表面(Surface)能够进行写屏操作。
下面的例子说明如何使用 IDirectDraw::SetCooperativeLevel:
HRESULT ddrval;
LPDIRECTDRAW lpDD; // already created by DirectDrawCreate

ddrval = lpDD->SetCooperativeLevel(hwnd, DDSCL_EXCLUSIVE |
DDSCL_FULLSCREEN);
if(ddrval == DD_OK)
{
// exclusive mode was successful
}
else
{
// not successful
// however, the application can still run
}

如果 IDirectDraw::SetCooperativeLevel 不返回 DD_OK,你的应用程序仍能继续执行,但是我不主张这样做。因为这样会使你的程序无法工作在全屏幕模式下,而且可能不按照你预先的要求工作。如果你确实想使你的应用程序继续运行下去,你应该显示一个错误信息,以便使终端用户知道 IDirectDraw ::SetCooperativeLevel 没有返回DD_OK,然后由用户自己去决定是否继续执行该程序。
使用IDirectDraw::SetCooperativeLevel 时,有一个要求:给每一个窗口赋一个句柄。这样当你的应用程序被异常中止时,Windows 能够知道。例如:
当发生GP错误时,GDI被推入缓冲区,终端用户就再也不能返回到Windows界面。为防止这种情况的发生,DirectDraw 能够在关键时刻执行一个后台过程向前台弹出一定的信息,利用这些信息可以确定应用程序是何时被中止的。这就给应用程序强加了一个限制。首先,你必须给每个窗口赋一个特殊的句柄。通过这些句柄,可以知道应用程序的运行信息。也就是说,要创建一个窗口,你必须设置一个处于活动状态的句柄。否则,无法很好执行许多功能。诸如:
当你的程序被终止时,可能会导致GDI的混乱,也可能会使组合键ALT+TAB不能正常工作。

改变显示模式

确定好应用程序的运行特征后,你就可以使用IDiredctDraw::SetDisplay方法来改变显示模式了。下面的例子展示出如何把显示模式设置为 640’480’8 bpp:
HRESULT ddrval;
LPDIRECTDRAW lpDD; // already created

ddrval = lpDD->SetDisplayMode(640, 480, 8);
if( ddrval == DD_OK)
{
// mode changed
}
else
{
// mode cannot be changed
//mode is either not supported
//or someone else has exclusive mode
}

在设置显示模式时,你应该判别一下终端用户的硬件设备是否支持高级显示模式。如果不支持,则你应把显示模式设为标准模式,以便能支持更多的适配器。例如,如果你想设计能在所有系统下运行的应用程序,则应把显示模式设为:640 480 8。如果适配器不支持你要设置的显示模式,则IDirectDraw::SetDisplayMode 会返回一个DDERR_INVALIDMODE错误值。因此,在设置显示模式之前,你应使用IDirectDraw::EnumDisplayMode方法来判别一下终端用户适配器所支持的显示类型。
创建一个可弹出式表面(Surface)集
当你已经设置好上面的显示模式后,你就可以创建一个表面(Surface)系统,且可以在它上面开发各种应用。尽管我们在DDEX1例程中把显示模式设为全屏幕模式,你仍能够创建一系列的表面(Surface),并在这些表面(Surface)之间进行自由切换。 如果,我们调用方法DirectDraw::SetCooperativeLevel把显示模式设置为 DDSCL_NORMAL的话, 则只能创建一个位拷贝表面(Surface)集。

设置表面(Surface)的各项参数
创建可切换式表面(Surface)集的第一步是:在DDSURFACEDESC结构中设定表面(Surface)的各 项参数。下面的例子显示了要创建一个可切换式表面(Surface)集需用到的各项定义和标志:
// Create the primary surface with 1 back buffer.
Ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS / DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
DDSCAPS_FLIP | DDSCAPS_COMPLEX;
ddsd,dwBackBufferCount = 1;
在上面的例子中,DDSURFACEDESC结构的大小被赋给dwSize成员。这样做的目的是:防止你在调用DirectDraw的方法时返回一个无效值(且更关键的是:这样做便于今后DDSURFACEDESC结构的扩展)。
成员dwFlags用于标明DDSURFACEDESC结构中哪些区域填入的信息有效,哪些区域的信息无效。正如例程DDEX1,我们用dwFlags表明了你要使用结构DDSCAPS(DDSC_CAPS),以及你要创建一个后台缓冲区(DDSD_BACKBUFFERCOUNT)。
例程中成员dwCaps包含了一些用在DDSCAPS结构中的标志。这样一来,成员dwCaps就定义了一个主表面(Surface)(DDSCAPS_PRIMARYSURFACE),一个弹出式表面(Surface)(DDS-CPAS_PRIMARYSURFACE),和一个复表面(Surface)(DDSCAPS_COMPLEX)。所谓复表面(Surface)是指,该表面(Surface)是由若干子表面(Surface)组成的。最后,上面的例程定义了一个后台缓冲区。这个后台缓冲区是背景和前景真正被写入的内存区。写好背景和前景后,再把它们从后台缓冲区弹到主表面(Surface)上。在例程DDEX1中,后台缓冲区的个数被设为1。
你可以在显示存储器空间允许的前提下,设置任意多的后台缓冲区。有关创建多个后台缓冲区的详细内容参见后面的“三重缓冲技术”(Triple Buffering)一节。表面(Surface)所占用的存储单元可以是显示存储器,也可以是系统内存。如果应用程序所需的存储空间超出了系统内存,则DirectDraw会自动使用显示存储器(例如,你的适配器只有 1 兆 RAM,而你同时定义了多个后台缓冲区)。当然,你也可以指定只使用系统内存或只使用显示存储器。如果把DDSCAPS结构中的dwCaps设为DDSCAPS_SYSTEMMEMORY,DirectDraw就只使用系统内存。如果把结构DDSCAPS中的dwCaps设为DDSCAPS_VIDEOMEMORY,则DirectDraw就只使用显示存储器。(如果,你设定只使用显存,但显存的大小又不够用来创建一个表面(Surface),这时,IDirectDraw::CreateDurface就会返回一个DDERR_OUTOFVIDEOMEMORY的错误信息。)

创建表面(Surface)集
定义完DDSURFACEDESC结构中的各项参数后,你就可以使用这个结构和指针IpDD 来调用IDirectDraw::CreatSurface方法了。其中,指针IpDD指向由函数 DirectDrawCreate所生成的对象DirectDraw。上述的具体过程见下例:
ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary,NULL);
if( ddrval == DD_OK )
{
// lppDDSPrimary points to new surface
}
else
{
// surface was not created
return FALSE;
}
调用IDirectDraw::CreateSurface成功后,调色板IpDDSPrimary将指向主表面(Surface)。
完成上述过程后,你就可以通过调用IDirectDrawSurface::GetAttached-Surface方法得到一个指向后台缓冲区的指针。如下例所示:
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
ddrval = lpDDSPrimary->GetAttachedSurface( &ddcaps, &lpDDSBack );
if( ddrval == DD_OK)
{
// lpDDSBack points to the back buffer
}
else
{
return FALSE;
}
通过提供主表面(Surface)的地址和设定DDSCAPS_BACKBUFFER标志,调用方法: IdirectDrawSurface::GetAttackedSrface成功后,调色板lpDDSBack就指向后台缓冲区。

对表面(Surface)进行写操作
创建好主表面(Surface)和后台缓冲区后,例程DDEX1调用Windows GDI标准函数, 对主表面(Surface)和后台缓冲区进行写文本操作。如下例所示:
if ( lpDDSPrimary->GetDC( &hdc) == DD_OK)
{
SetBkColor( hdc, RGB(0, 0, 255 ) );
SetTextColor( hdc, RGB(255, 255, 0) );
TextOut(hdc, 0, 0, szFrontMsg, lstrlen(szFrontMsg) );
lpDDSPrimary->ReleaseDC(hdc);
}
if (lpDDSBack->GetDc(&hdc) == DD_OK)
{
SetBkColor(hdc,RGB(0,0,255));
SetTextColor(hdc, RGB(255,255,0));
TextOut(hdc,0,0,szBackMsg,lstrlen(szBackMsg));
lpDDSBack->ReleaseDC(hdc);
}
上例中调用了方法IDirectDrawSrface::GetDC来用一个句柄给表面(Surface)加锁。使用Windows标准函数需要一个指向deviceContext的句柄。如果你不习惯这样做,你可以不调用Windows标准函数,而调用IDirectDrawSurface::Lock方法和IDirectDrawSurface::UnLock方法来给后台缓冲区加锁和解锁。
给表面(Surface)加锁(可以是整个表面(Surface),也可以是表面(Surface)的一部分),能确保应用程序和位拷贝进程不能覆盖表面(Surface)所占存储空间。这样可以避免应用程序对表面(Surface)进行写操作时发生错误。另外,只有当表面(Surface)存储单元处于开锁状态时,你的应用程序才能把表面(Surface)一页一页地由后台弹至前台。
在表面(Surface)加锁状态下,上例调用Windows GDI标准函数SetBkColor来设定背景颜色,用SetTextColor来设定前景文本的颜色,还调用了TextOut函数把背景和前景显示到表面(Surface)上。
当把文本写到缓冲区后,例程调用了IDirectDrawSurface::ReleaseDC方法解锁表面(Surface),并释放句柄。无论何时,只要你停止对缓冲区进行写操作,你就必须调用IDirectDrawSurface::ReleaseDC 或者IDirectDrawSurface::Unlock来解锁表面(Surface),释放句柄。至于具体调用上述两种方法的哪一个,要视具体情况而定。重申一遍,只有表面(Surface)处于开锁状态,应用程序才能把它们由后台弹至前台。
你可能还有点疑惑,为什么这里只对主表面(Surface)进行写操作?通常,在你写一个表面(Surface)时,只有那些写到主表面(Surface)后台缓冲区的内容才能显示出来。在本例DDEX1中,程序完成第一次弹出表面(Surface)操作时,会有一个明显的延时,所以DDEX1的初始化函数中只对主表面(Surface)缓冲区进行写操作是为了防止程序刚开始时显示不连贯。正如后面所看到的,例程DDEX1在WM_TIMER期间只对后台缓冲区进行写操作。初始化函数和标题页只能放在主表面(Surface)缓冲区中。
注意:调用IDirectDrawSurface::Unlock对表面(Surface)解锁后,指向表面(Surface)存储单元的指针就失效。要想重新获得该指针,就必须调用IDirectDrawSurface::Lock方法。

对表面(Surface)集进行写和弹出操作
初始化结束后,DDEX1应用程序进入消息环。就是在这个循环中,后台缓冲区被锁定,新的内容被写入,当后台缓冲区未被锁定时,表面(Surface)就被弹出。WM TIMER包括了用以写入和弹出表面(Surface)大部分代码。

写入表面(Surface)
WM TIMER信息的前半部分是用来写入到后台缓冲区的。在这里使用到的大部分 技术,在"对表面(Surface)进行布置操作"部分中,就已经被讨论过了。但是我将再次简要地讨论一下。以下就是在DDEX1中WM TIMERde的内容:
case WM TIMER:
// Flip surface.
If(bActive)
{
if (LpDDSBack->GetDC(&hdc)_== DD_OK)
{
SetBKColor( hdc, RGB(0, 0, 255 ) );
SetTextColor( hdc, RGB( 255,255, 0 ) );
if( phase )
{
TextOut( hdc, 0, 0, szFrontMsg, Lstrlen(szFrontMsg));
phase = 0;
}
else
{
TextOut(hdc, 0, 0, szBackMsg, Lstrlen(szBackMsg) );
phase = 1;
}
LpDDSBack_>ReleaseDC(hdc);
}

在准备写入之前,GetDC行将锁定后台缓冲区。SetBKColor和SetTextColor函数设置背景和文本的颜色。
下一步,变量"phase"决定应该写入主缓冲区信息还是写入后台缓冲区信息。如果"phase"等于1, 主表面(Surface)信息便被写入, 并且将"phase"置为0。 如果"phase" 等于0,后台缓冲区信息便被写入, 并且将"phase"置为1。 但是, 你应当注意,在这两种情况下, 这些信息都要被写入到后台缓冲区中。一旦信息被写入到后台缓冲区中,那末通过使用IDrectDrawSurface::ReleaseDC方法, 后台缓冲区就被解锁。

弹出表面(Surface)
一旦表面(Surface)内存被打开, 你就可以使用IDirectDrawSurface::Flip方法将后台缓冲区弹出到主表面(Surface)中了。 下面的例程表示了在DDEX1中如何做到这一点:
while( 1 )
{
HRESULT ddrval;
ddral = LpDDSPriprimary->Flip( NULL, 0 );
if( ddral == DD_OK )
{
break;
}
if( ddral == DDERR_SURFACELOST )
{
ddral = LpDDSPrimary->Restore();
if( ddral != DD_OK )
{
break;
}
}
if( ddral != DDERR_WASSTILLDRAWING )
{
break;
}
}
在这个例程中,IPDDSPrimary指针指明了主表面(Surface)和与之相关联的后台缓冲区。当IDirectDrawSurface:Flip被调用时, 前表面(Surface)和后表面(Surface)被交换(注意:只是表面(Surface)的指针被交换, 并无数据移动)。 如果弹出是成功的, 并且返回到DD_ok,那末,应用程序就从当前循环中断。

在弹出的同时,返回一个DDERR_SURFACELOST值,则调用IDirectdrawSurface::Restore即可恢复该表面(Surface)。 如果恢复成功,应用程序就循环返回到IDirectDrawSurface::Flip的调用,并且再运行一次。 如果表面(Surface)恢复不成功, 那末,应用程序便从当前循环中断,并且返回一个错误信息。一件很重要的事情就是:即使在你已经调用IDirectDrawSurface::Flip之后,交换也不会立即完成。 此时,系统中的原表面(Surface)将缩小为一个条形图标,这样一来,就为下一次点击该图标弹出原表面(Surface)作好了准备。例如,如果以前的弹出操作还没有发生,那末方法IDirectDrawSurface::Flip就会返回参数DDERR_WASSTILLDRAWING的值。在这个例程中,方法IDirectDrawSurface::Flip调用将会继续循环,直到调用返回DD_OK值为止。
Deallocating the DirectDraw Objects
当你按下F12时,在退出应用程序之前,DDEX1应用程序处理WM DESTROY信息。该信息调用finiObjects函数,该函数包括了所有的Iunknown Release调用,如下所示:
static void finiObjects( void )
{
if( LpDD != NULL )
{
if( LpDDSPrimary !+Null )
{
LpDDSPrimary->Release();
LpDDSprimary = NULL;
}
LpDD->Release();
LpDD = NULL;
}
}/* finiObjects */
该程序是相当直观的。该应用程序检查DirectDraw和DirectDrawSurface对象的指针是否为空,当然,这些指针非空。然后DDEX1调用IDirectDrawSurface::Release方法,将IdirectDrawSurface对象的参考值减1。如果当参考值等于0时,IDirectDrawSurface对象所占的内存就将被释放。然后通过设置IDirectDrawSurface的值为空,DirectDrawSurface的指针就被释放了。应用程序然后调用IDirectDraw::relese,并将DirectDraw对象的关联值减少到0,释放 DirectDraw对象的操作是通过设置DirectDraw对象的值为空完成的,此时DirectDraw对象的指针也就被释放了。

例一的扩展(DDEX2和DDEX3)

DDEX1包含了一个最基本的DirectDraw的实现方法。它生成了DirectDraw和DirectDrawSurface对象,同时也生成了一个主表面(Surface)和与之相关的后台缓冲区,并在后台缓冲区打印文本,并可以在表面(Surface)之间进行切换。

在DirectX 3 SDK(DDEX2)中的第二个DirectDraw 例程扩展了关于DDEX1应用程序。DDEX2包括将一个位图文件载入到后台缓冲区的函数。

第三个DirectDraw 例程将这一函数进一步地扩展了。除了主表面(Surface)和后台缓冲区之外,DDEX3还生成了两个隐屏表面(Surface),并且在每一个隐屏表面(Surface)之中都载入了一个位图文件。然后,DDEX3使用IDirectDrawSurface::BltFast方法,将一个隐屏的内容复制到后台缓冲区中。之后,弹出这些缓冲区,并且将下一个隐屏表面(Surface)的内容复制到后台缓冲区。

以下的部分将更详细地检查这一新的函数。

在一个表面(Surface)上载入一个位图(Bitmap)

就与DDEX1一样,dolnit是DDEX2应用程序的初始化函数。虽然,在DDEX2中,DirectDraw的初始化方式表面上与在 DDEX1中的DirectDraw的初始化方式不太一样,但它们的实质是一样的。这一过程如下列的程序代码所示:

LPddPal = DDLoadPalette(LpDD, szBackground);

if (LpddPal == NULL)

goto error;

ddrval = LpDDSprimary->SetPalette(LpDDPal);

if( ddral != DD_OK )

goto error;

// Load a bitmap into the back buffer。

ddrval = DDReLoadBitmap(LpDDSBack, szBackground);

if( ddrval != DD_OK )

goto error;

生成调色板

这个程序代码的第一行是:从DDLoadPalette函数返回一个值。如果你想知道在哪能找到DDLoadPalette,你可以在/DXSDK|SAMPLES|MISC目录中的Ddutil.cpp文件中找到它。你会发现,在DirectX 3 SDk的大部分DirectDraw例程中都使用了Ddutil.cpp文件。最为关键的是:该文件上包括了能从文件中或是从资源中载入位图和调色板的函数。这些函数的代码并非一遍遍地重复出现在例程文件中,而且被放置在能被重复使用的同一文件之中。

注意:如果你正在使用MS Developer Studio(微软开发工作室)编辑DDEX2和用DirectX 3 SDK提供的其它工具,你必须把Ddutil.cpp文件插入到DDEXx文件工作区的文件表中。重申一遍:在工作区中必须包括Ddutil.cpp:

1. 在插入(insert)菜单上,单击Files进入Projeects。

2. 单击Browse.

3. 单击DXSDK/SDK/SAMPLES/MISC/目录。

4. 单击Ddutil.cpp

5. 单击ADD

对于DDEX2来说,从Back.bmp文件中,DDLoadPalette创建了一个DirectDraew对象。DDLoadPalette函数实际上是来检查用以产生调色板的一个文件或资源是否存在。如果不存在的话,该函数就创建一个缺省的调色板,对于DDEX2 来说,DDLoadPalette函数从文件中提取调色板信息,并通过ape指针将其存储在一个指定的结构中,然后它生成DirectDrawPalette 对象。如下面的代码所示:

pdd->CreatePalette(DDPCAPS_8BIT, ape, &ddpal, NULL);

return ddpal;


当IDirectDraw::Createpalette方法返回后,ddpal参数将指向DirectDrawPalette对象,其中,对象DirectDrawPalette是从DDLoadPalettede的调用返回的。

ape参数是一个指针,它可以包括2,4,16或256个入口,呈直线分布。这些入口的数目由IDirctDraw::CreatePalette参数决定。在这种情况下,dwFLags参数被设置为DDPCAPS_8BIt,它表示:在这个结构中有256个入口。每个入口包括4位(一位红通道,一位绿通道,一位兰通道和一个标志位)。

设置调色板

在生成调色板之后,你要通过调用IDirectDrawSurface::SetPalette方法,将DirectDrawPalette对象的指针转到主表面(Surface)上,如下列代码所示:

ddrval = LpDDSPrimary->SetPalette(LpDDPal);

if( ddrval != DD_Ok )

// SetPalette failed

一旦你已经调用了IdirectDrawSurface::SetPalette,DirectDrawPalette对象就被嵌入到DirectDrawSurface对象中了。不论何时你需要改变这一调色板,你要作的就是生成一个新的调色板并重新设置该调色板。(这就如例程中所做的一样。然而,也有其它的改变调色板的方式。我们可以在其它例程中看到)。

在后台缓冲区中载入一个位图文件

一旦DirectDrawPalette对象被嵌入到DirectDrawSurface对象之中,DDEX2就将Back.bmp bitmap载入到后台缓冲区中。使用下例的程序代码可实现该过程:

// Load a bitmap into the back buffer.

ddrval = DDReLoadBitmap(LpDDSBack, szBackground);

if( ddrval != DD_Ok )

// Load failed

DDReLoadBitmap是出现在Ddutil.cpp中的另一个函数。它从一个文件或资源中将一个位图文件载入到一个已经存在的DirectDraw表面(Surface)之中。(就象在DDEX5中那样,你可以使用DDLoadBitmap创造一个表面(Surface)并且将位图载入那个表面(Surface))。对于DDEX2来说,DDReLoadBitmap把szBackground指向的Back.bmp载入到ipDDSBack指向的后台缓冲区,DDReLoadBitmap调用DDCopyBitmap函数,将文件复制到后台缓冲区中,并且将缓冲区扩展到适当的。

DDCopyBitmap函数将位图复制到内存之中,然而利用GetObject函数得到位图的大小。DDCopyBitmap然后使用下列的代码得到后台缓冲区的大小(它可以放置位图):

//

// get size of surface

//

ddsd.dwSize = sizeof(ddsd);

ddsd.dwFlags = DDSD_HEIGHT DDSD_WIDTH;

pdds->GetSurfaceDesc(&ddsd);

ddsd是指向DDSRFACEDESC结构的一个指针。该结构存储了DirectDraw表面(Surface)的当前描述。在这种情况下,我们需要注意的是:DDSURFACEDESC的成员描述这个表面(Surface)的高度和宽度,分别表示为:DDSD_HEIGHT和 DDSD_WIDTH。调用IDirectDrawSurface::GetSurfaceDesc方法,把适当的值来载入到这个结构。对于DDEX2来说,这些值将是:高480和宽640。

DDCopyBitmap函数锁定表面(Surface)并将位图文件复制到后台缓冲区,使用StretchBit函数延伸或压缩后台缓冲区到可适用的大小。表示如下:

if ((hr = pdds->GetDC(&hdc)) == DD_OK)

{

StretchBlt(hdc, 0,0,ddsd.dwWidth, ddsd.dwHeight, hdcImage,x, y, dx, dy, SRCCOPY);

pdds->ReleaseDC(hdc);

}

弹出表面(Surface)

在DDEX2例程中的弹出表面(Surface)操作本质上与在DDEX1例程中的弹出操作是同样的过程。但是在表面(Surface)丢失的情况下,你必须通过调用DDReLoadBitmap函数,在表面(Surface)恢复之后,,将bitmap再次载入到后台缓冲区中。

从一个隐屏表面(Surface)按位隔行拷贝

DDEX2是在后台缓冲区中取出和放入位图的,然后在后台缓冲区和主缓冲区之间切换。这并不是一个展示位图的很实际的方法。DDEX3扩展了DDEX2的功能,它包括了两个隐屏缓冲区,且在其内部存放有两个位图(一个对应于偶行屏幕,另一个对应于奇行屏幕)。DDEX3把一个屏幕按位隔行拷贝到后台缓冲区中,再把另外一个屏幕按位隔行拷贝到另一个后台缓冲区中,然后弹出表面(Surface)。

生成隐屏表面(Surface)

下列的代码在DDEX3 中加到dolnit 函数可生成两个隐屏 缓冲区:

// Create an offscreen bitmap.

ddsd.dwFlags = DDSD_CAPS DDSD_HEIGHT DDSD_WIDTH;

ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;

ddsd.dwHeight = 480;

ddsd.dwWidth = 640;

ddrval = lpDD->CreateSurface( &ddsd,&lpm DDSOne,NULL);

if (ddrval != DD_OK)

{

return initFail(hwnd);

}

//Create another offscreen bitmap.

ddrval = lpDD->CreateSurface( &ddsd,&lpm DDSTwo,NULL);

if (ddrval != DD_OK)

{

return initFail(hwnd);

}

如代码中所示,dwFlags成员设定了应用程序将使用DDSAPS结构,并且设置缓冲区的高度和宽度。表面(Surface)是一个平面式隐屏缓冲区,就如同通过设置在DDSCAPS结构中的DDSCAPS_OFFSCREEN标志所表示的一样。在DDSURFACEDESc结构中,高度和宽度被分别设置为480和640。通过使用IDirectDraw::CreateSurface方法,表面(Surface)就这样被生成了。

因为两个隐屏缓冲区有着同样的大小,故生成第二个缓冲区的唯一要求就是再运行IDirectDraw::CreateSurface(当然,要用不同的指针名字)。

通过在DDSCAPS结构中,或是设置DDSCAPS_SYSTEMMEMORY,或是设置DDSCAP_VIDEOMEMORY的容量,你可以将该:隐屏缓冲区或是放置在系统内存中或是显存中。通过将位图存盘在显存中,你可以增加隐屏表面(Surface)和缓冲区之间切换的速度。当我们开始讨论位图动画时,速度将变得更加重要。但是,此时你应当注意:如果你仅为隐屏缓冲区设置DDScAPS_VIDEOMEMORY,而没有足够的显存来保存整个位图文件,那么,当你试图创建表面(Surface)时,就会返回一个DDERR_OUTOFVIDEOMEMORY的错误值。


将位图文件载入后台缓冲区

在两个隐屏表面(Surface)生成后,DDEX#使用INITSURFACES函数,从Frnt.bmp文件中将位图文件载入到表面(Surface)中。InitSurfaces函数使用Ddutil.cpp中的DDCopyBitmap载入这两个位图文件,如下列代码所示:

// Load our bitmap resource.

hbm = (HBITMAP)LoadImage(GetModuleHandle(NULL),szBitmap,

IMAGE_BITMAP,0,0,LR_CREATEDIBSECTION);


if (hbm == NULL)

return FALSE;


DDCopyBitmap(lpDDSone,hbm,0,0,640,480);

DDCopyBitmap(lpDDSTwo,hbm,480,640,480);

DeleteObject(hbm);

return TRUE;

如果你在MS Paint(微软画笔)或是另一个绘画程序中看到Frnt.bmp文件,你可以看到位图文件是由两个屏幕组成的(其中一个在另一个的上部)。DDCopyBitmap函数在屏幕相汇点上将位图文件一分为二,并将第一份位图文件载入第一个隐屏表面(Surface)(IPDDSOne)中,同时将第二份位图载入第二个隐屏表面(Surface)(IPDDSTwo)中。

将隐屏表面(Surface)按位隔行拷贝到后台缓冲区

WM TIMER包含了写表面(Surface)和弹出表面(Surface)的代码。在DDEX3的情况下,它包含下列的代码,用来选择适当的隐屏表面(Surface),并将它按位隔行拷贝到后台缓冲区中。

rcRECT.LEFT =0;

RCRECT.TOP =0;

RCRECT.right =640;

rcRect.bottom =480;

if(phase)

{

pdds = lpDDSTwo;

phase = 0;

}

else

{

pdds = lpDDSOne;

phase = 1;

}

while(1)

{

ddrval =lpDDSBack->BltFast(0,0,pdds,&rcRect,FALSE);

if(ddrval == DD_OK)

{

break;

}


"phase"决定了将哪一个隐屏表面(Surface)按位隔行拷贝到后台缓冲区中。然后,IDirectDrawSurface::BltFAst方法被调用,并将已经被选择好的隐屏表面(Surface)按位隔行拷贝在后台缓冲区中,开始位置为(0,0),它位于屏幕的左上角。参数rcRect指向结构Rect,它定义了隐屏表面(Surface)的左上角和右下角。最后的参数被设置为FALSE(或0),这就表明了没有专门的转移标志以备使用。

在这里,我很想补充说明的是:在何种情况下应该选择IDirectDrawSurface::Blt方法,在何种情况下应该选择IDirectDrawSurface::BltFast方法。如果你正在从一个隐屏缓冲区中进行一次按位隔行拷贝,你应当使用IDirectDrawSurface::BltFast。如果你的系统显存中是使用硬件进行按位隔行拷贝,你虽然不会真正提高拷贝的速度,但是,它会节省系统模拟硬件时间,从而使整个按位隔行拷贝时间缩短约10%。因此,我推荐读者使用IDirectDrawSurface::BltFast进行所有的显示操作(从显存按位隔行拷贝到显存中)。如果你正在从系统内存中按位隔行拷贝,或者要求专门的硬件标志位,这样的话,你就必须使用IDirectDrawSurface::Blt。

一旦隐屏表面(Surface)被载入后台缓冲区中,后台缓冲区和主表面(Surface)就如同前边的例程中所显示的一样被弹出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值