发信人: chensumin() 
整理人: nhyjq(2001-12-07 16:57:53), 站内信件
 | 
 
 
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 AP I
 函数(注意:这里我所说的是应该,而不是必须), 这是因为使用 OLE 中的
 CoCreatelnstance 函数也能创建一个 DirectDraw
 对象实体,但这不在我们的讨论范围之中)。DirectDrawCreate
 采用全球统一的标准,它代表显示设备,这些显示设备在大多数情况下被定为 N ULL
 (即:系统使用缺省的显示设备)。
 当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 successfull
 // however, the application can still run
 }
 
 如果 IDirectDraw::SetCooperativeLevel 不返回
 DD_OK,你的应用程序仍能继续执行,但是我不主张这样做。因为这样会使你的程 序无法工作在全屏幕模式下,而且可能不按照你预先的要求工作。如果你确实想 使你的应用程序继续运行下去,你应该显示一个错误信息,以便使终端用户知道 
 IDirectDraw ::SetCooperativeLevel
 没有返回DD_OK,然后由用户自己去决定是否继续执行该程序。
 使用IDirectDraw::SetCooperativeLevel
 时,有一个要求:给每一个窗口赋一个句柄。这样当你的应用程序被异常中止时 ,Windows
 能够知道。例如:
 当发生GP错误时,GDI被推入缓冲区,终端用户就再也不能返回到Windows界 面。为防止这种情况的发生,DirectDraw
 能够在关键时刻执行一个后台过程向前台弹出一定的信息,利用这些信息可以确 定应用程序是何时被中止的。这就给应用程序强加了一个限制。首先,你必须给 每个窗口赋一个特殊的句柄。通过这些句柄,可以知道应用程序的运行信息。也 就是说,要创建一个窗口,你必须设置一个处于活动状态的句柄。否则,无法很 好执行许多功能。诸如:
 当你的程序被终止时,可能会导致GDI的混乱,也可能会使组合键ALT+T AB不能正常工作。
 
 改变显示模式
 确定好应用程序的运行特征后,你就可以使用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::SetDisplayMo de
 会返回一个DDERR_INVALIDMODE错误值。因此,在设置显示模式之前,你应使用I DirectDraw::EnumDisplayMode方法来判别一下终端用户适配器所支持的显示类型 。
 创建一个可弹出式表面(Surface)集
 当你已经设置好上面的显示模式后,你就可以创建一个表面(Surface)系统,且可 以在它上面开发各种应用。尽管我们在DDEX1例程中把显示模式设为全屏幕模式, 你仍能够创建一系列的表面(Surface),并在这些表面(Surface)之间进行自由切 换。
 如果,我们调用方法DirectDraw::SetCooperativeLevel把显示模式设置为
 DDSCL_NORMAL的话, 则只能创建一个位拷贝表面(Surface)集。
 设置表面(Surface)的各项参数
 创建可切换式表面(Surface)集的第一步是:在DDSURFACEDESC结构中设定表面(S urface)的各
 项参数。下面的例子显示了要创建一个可切换式表面(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表明了你要使用结构DDSCAP S
 (DDSC_CAPS),以及你要创建一个后台缓冲区(DDSD_BACKBUFFERCOUNT)。
 例程中成员dwCaps包含了一些用在DDSCAPS结构中的标志。这样一来,成员
 dwCaps就定义了一个主表面(Surface)(DDSCAPS_PRIMARYSURFACE),一个弹出式表 面(Surface)(DDS-CPAS_PRIMARYSURFACE),和一个复表面(Surface)(DDSCAPS_CO MPLEX)。所谓复表面(Surface)是指,该表面(Surface)是由若干子表面(Surface )组成的。最后,上面的例程定义了一个后台缓冲区。这个后台缓冲区是背景和前 景真正被写入的内存区。写好背景和前景后,再把它们从后台缓冲区弹到主表面 (Surface)上。在例程DDEX1中,后台缓冲区的个数被设为1。
 你可以在显示存储器空间允许的前提下,设置任意多的后台缓冲区。有关创建多 个后台缓冲区的详细内容参见后面的“三重缓冲技术”(Triple
 Buffering)一节。表面(Surface)所占用的存储单元可以是显示存储器,也可以是 系统内存。如果应用程序所需的存储空间超出了系统内存,则DirectDraw会自动 使用显示存储器(例如,你的适配器只有
 1 兆
 RAM,而你同时定义了多个后台缓冲区)。当然,你也可以指定只使用系统内存或 只使用显示存储器。如果把DDSCAPS结构中的dwCaps设为DDSCAPS_SYSTEMMEMORY, DirectDraw就只使用系统内存。如果把结构DDSCAPS中的dwCaps设为DDSCAPS_VID EOMEMORY,则DirectDraw就只使用显示存储器。(如果,你设定只使用显存,但显 存的大小又不够用来创建一个表面(Surface),这时,IDirectDraw::CreateDurf ace就会返回一个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将指向主表面(S urface)。
 完成上述过程后,你就可以通过调用IDirectDrawSurface::GetAttached-Surfac e方法得到一个指向后台缓冲区的指针。如下例所示:
 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_TIME R期间只对后台缓冲区进行写操作。初始化函数和标题页只能放在主表面(Surfac e)缓冲区中。
 注意:调用IDirectDrawSurface::Unlock对表面(Surface)解锁后,指向表面(Su rface)存储单元的指针就失效。要想重新获得该指针,就必须调用IDirectDrawS urface::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方法将后台缓冲区弹出到主表面(Surfa ce)中了。
 下面的例程表示了在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::R estore即可恢复该表面(Surface)。
 如果恢复成功,应用程序就循环返回到IDirectDrawSurface::Flip的调用,并且 再运行一次。
 如果表面(Surface)恢复不成功,
 那末,应用程序便从当前循环中断,并且返回一个错误信息。一件很重要的事情 就是:即使在你已经调用IDirectDrawSurface::Flip之后,交换也不会立即完成 。
 此时,系统中的原表面(Surface)将缩小为一个条形图标,这样一来,就为下一次 点击该图标弹出原表面(Surface)作好了准备。例如,如果以前的弹出操作还没有 发生,那末方法IDirectDrawSurface::Flip就会返回参数DDERR_WASSTILLDRAWIN G的值。在这个例程中,方法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::Relea se方法,将IdirectDrawSurface对象的参考值减1。如果当参考值等于0时,IDir ectDrawSurface对象所占的内存就将被释放。然后通过设置IDirectDrawSurface 的值为空,DirectDrawSurface的指针就被释放了。应用程序然后调用IDirectDr aw::relese,并将DirectDraw对象的关联值减少到0,释放
 DirectDraw对象的操作是通过设置DirectDraw对象的值为空完成的,此时Direct Draw对象的指针也就被释放了。
  -- ※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 202.102.114.167]
  | 
 
 
 |