DirectDraw 游戏编程基础(2)
游戏使计算机的发展超越了晶体管时代
例程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对象的指针也就被释放了。