源代码下载(3个工程) (编辑者:链接失效)
我想关于这个主题的文章,不算少,但也不算太多。但大多是分别介绍 DirectDraw 与 DirectInput,而并没有将其结合起来,也许你会问:“分开与合并起来并没有本质区别啊!”。其实的确没有本质区别,但那样使那些最初对游戏编程报有极大热情的爱好者感到非常失望,因为这其中的一个并不能完全满足他们的要求,并且使其感到巨大的阻力,从而失去信心。所以本文将 DirectDraw 与 DirectInput结合起来去讲一个主题就是“游戏编程”,请注意是“游戏编程”,当然这只是一个简单的桌面游戏,但这已经与先前有很大的不同了,这已不是简单的 DirectDraw或 DirectInput编程。我想你现在应该能够体会出其中的区别了。
声明:在这之前需要你具有一定的 WIN32 API 函数的知识,并且可以熟练使用。和 DirectDraw的知识,关于DirectDraw可以参见 www.frontfree.net 中的 <<动画程序编写——DirectDraw之旅>> 1-3),或其它文章。最后是 c++ 语言,当然也要包括面向对象的那部分。在 Visual C++ .NET 编译环境下进行开发的。
首先 ,我们还是先简要复习一下DirectDraw的概念吧! DirectDraw本质上是显存管理程序。它最重要的性能是允许程序员直接在显存里存储和操纵位图。它使你能够利用视频硬件bliter(位块传输器)在显存内部进行位图的blit(位块传输)。用视频硬件的blitter从显存向显存进行blit比从内存向显存更快。这在64位显卡向显存提供64位数据路径的今天显得尤其重要,硬件独立于促CPU进行位块传输操作,使得CPU得以继续工作。另外DirectDraw支持显卡的其他硬件加速特性,例如对精灵和z -buffering的硬件支持。
DirectDraw的工作原理
我们这里还是用图表方式展现给大家吧!
细心的朋友可以很明显地注意到图示中的右上角的图解中说明,表面对象有两个宽度,一个是WIDTH,一个是PITCH。WIDTH就是创建表面时所给出的那个宽度,而PITCH是表面的实际宽度,是按字节算的。在许多显卡上,PITCH和WIDTH是相等的,比如在640x480的高彩模式下,PITCH为1280。而在某些显卡上,PITCH比WIDTH要大。比如在640x480的256色模式下,当WIDTH是640时,PITCH为1024而不是640,这些显卡这样做是为了更好地进行数据对齐来提高性能或达到其它目的。所以,我们在实际编程时,为了保证程序的兼容性,必须按PITCH处理。 但这些硬件的底层问题,我们不用太关心,只要稍有了解就可以了。
下面我们再简要叙述一下,如何使用 DirectX 9.0 中提供的 DirectDraw 类库来创建对象并使用操作对象。
宏定义在先,定义删除指针和释放对象的宏
#define SAFE_DELETE(p) { if(p) { delete (p); (p)=NULL; } } #define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } }
|
先创建一个 CDisplay 的全局对象 CDisplay就是ddutil.h中定义的类,用于处理表面之间的拷贝翻页等操作的类,再次定义一个全局变量,用于以后对指向的表面之间进行操作
CDisplay* g_pDisplay = NULL; |
然后创建表面,当然可以创建很多的表面,这些表面都是离屏表面,在更新画面时,都可以用 CDisplay 类的对象中的方法,将其拷贝到后备缓冲区表面上。只要创建离屏表面,就要用到 CSurface 类。CSurface也是ddutil.h头文件中定义的类,用于对表面本身进行操作,如设置色彩键码,在此定义的图画指针。
CSurface* g_pBackSurface = NULL; |
DirectX 中就一共用这两个类封装了 DirectDraw 对象的大部分操作,如果你觉得这还不能满足要求,那么你也可以在程序中用 DirectDraw API 函数编写程序,不过在本文中不再介绍。
这之后,我们会用到 InitDirectDraw 函数。这个函数是我们自己创建的。在此函数中作所有的 DirectDraw 的对象初始化工作。
HRESULT InitDirectDraw( HWND hWnd ) { HRESULT hr; //接受返回值,其实是long型变量 LPDIRECTDRAWPALETTE pDDPal = NULL; //定义程序中的调色板 int iSprite; //定义与sprite个数有关的计数器 g_pDisplay = new CDisplay(); //动态开辟一个CDisplay类 if( FAILED( hr = g_pDisplay->CreateFullScreenDisplay( hWnd, SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP ) ) ) /*设置程序为全屏,并且 g_pDisplay 就是动态开辟一个CDisplay类的指针,而在这个类的域中,有一个DirectDraw主表面指针,和一个后备缓冲区表面的指针。在从我建议你可以先去阅读一下 ddutil.h 和 ddutil.cpp 文件。*/ { MessageBox( hWnd, TEXT("This display card does not support 1024x768x8. "), TEXT("DirectDraw Sample"), MB_ICONERROR | MB_OK ); return hr; } if( FAILED( hr = g_pDisplay->CreatePaletteFromBitmap( &pDDPal, MAKEINTRESOURCE( IDB_DIRECTX ) ) ) ) //顾名思义,就是从bmp图片中获得调色板值,并赋值在pDDPal结构指针所指向的结构体中。 return hr; if( FAILED( hr = g_pDisplay->SetPalette( pDDPal ) ) ) //用刚才从IDB_DIRECTX中获得的调色板制来设置程序调色板 return hr; SAFE_RELEASE( pDDPal );//释放指针,在用过后,一定要释放,这是良好的编程习惯 // 用IDB_WINXP图片创建一个表面,并用g_pBackSurface指向这个表面 if( FAILED( hr = g_pDisplay->CreateSurfaceFromBitmap( &g_pBackSurface, MAKEINTRESOURCE( IDB_WINXP ), SCREEN_WIDTH, SCREEN_HEIGHT ) ) ) return hr;//设置色彩键码为黑色,0代表黑色,这样在表面的拷贝过程中黑色像素的点将不会被拷贝,这样可以产生镂空效果。当然你可以任意设置关键颜色,而颜色的表示法可以用 RGB 宏定义。例如 红色:RGB( 255,0,0 ), 黑色 RGB( 255,255,255 ) if( FAILED( hr = g_pBackSurface->SetColorKey( RGB( 255,255,255 ) ) ) ) return hr; return S_OK; } |
下面的函数是用于更新画面的。
HRESULT DisplayFrame() { HRESULT hr; g_pDisplay->Clear( 0 ); //清空后备缓冲区表面 //将g_pBackSurface所指向的图片拷贝到后备缓冲区表面 g_pDisplay->Blt( 0, 0, g_pBackSurface, NULL );//最关键的地方在这里,请看下面的语句,只要我们一执行翻页操作,就可以将改动了的图像了显示在屏幕上了 if( FAILED( hr = g_pDisplay->Present() /*翻页操作*/) ) return hr; return S_OK; } |
下面的函数是用于在程序失去焦点时调用的。
HRESULT RestoreSurfaces() { HRESULT hr; LPDIRECTDRAWPALETTE pDDPal = NULL; /*当程序失去焦点,要保存当前的画面,请注意这里,g_pDisplay->GetDirectDraw()函数返回的才是真正的 DirectDraw 对象 */ if( FAILED( hr = g_pDisplay->GetDirectDraw()->RestoreAllSurfaces() ) ) return hr;//在此我们还要重新创建调色板 if( FAILED( hr = g_pDisplay->CreatePaletteFromBitmap( &pDDPal, MAKEINTRESOURCE( IDB_DIRECTX ) ) ) ) return hr;//重新设置调色板 if( FAILED( hr = g_pDisplay->SetPalette( pDDPal ) ) ) return hr; SAFE_RELEASE( pDDPal );//重新画出图画 if( FAILED( hr = g_pLogoSurface->DrawBitmap( MAKEINTRESOURCE( IDB_WINXP ), SPRITE_DIAMETER, SPRITE_DIAMETER ) ) ) return hr; return S_OK; } |
下面这个函数是释放表面指针所用的。
VOID FreeDirectDraw() { SAFE_DELETE( g_pBackSurface ); SAFE_DELETE( g_pDisplay ); } |
我们的回顾到此结束,下面我们开始本文要介绍的一个关键技术,DirectInput 的使用。 游戏编程可不仅仅是图形程序的开发工作,实际上包含了许多方面,本文所要讲述的就是关于如何使用 DirectInput 来对键盘编程的问题。 而我们为什么要选择用 DirectInput 来处理游戏中的输入问题呢?其实用 Win32 API 函数也完全可以处理这些工作,例如其中,有一个
GetAsyncKeyState() 的函数可以返回一个指定键的当前状态是按下还是松开。这个函数还能返回该指定键在上次调用 GetAsyncKeyState() 函数以后,是否被按下过。虽然这个函数听上去很不错,但需要我们自己轮换查询每个键盘的状态。而在 DirectInput 中我们已经可以脱离这些烦琐的工作,只因它的功能更强大。
由于本文重点在二者的结合,故在此只介绍 DirectInput 中比较简单的,而且最容易上手的立即模式的工作方式。 而这里我们要用到 DirectInput 的 API 函数。有人会问,为什么在 DirectDraw 中用 DirectX 提供的类库编程,而对于 DirectInput 却直接使用要用其 API 函数呢,是因为没有提供 DirectInput 的类库吗?不是!而是因为使用类库并不很方便而且不灵活。
OK,让我们开始我们游戏编程的第二部——DirectInput编程。
前面讲 DirectDraw 时,并没有提到,微软是按 COM 来设计DirectX的,所以就有了一个 DIRECTINPUT 对象来表示输入设备,而某个具体的设备由 DIRECTINPUTDEVICE 对象来表示。也许会感到很无奈,怎么游戏编程需要这么多的知识啊,其实您也无需烦恼,只要知道一下就可以了,其实这并不;影响您的设计,而且就算您不知道,也同样可以驾驭DIRECTINPUT。
实际的建立过程是先创建一个 DIRECTINPUT 对象,然后在通过此对象的 CreateDevice 方法来创建 DIRECTINPUTDEVICE 对象。
#include <dinput.h> #define DINPUT_BUFFERSIZE 16 LPDIRECTINPUT lpDirectInput; // DirectInput 对象实际上是一个com对象 LPDIRECTINPUTDEVICE lpKeyboard; // DirectInput 设备 BOOL InitDInput(HWND hWnd) { HRESULT hr;// 创建一个 DIRECTINPUT 对象 if( FAILED( hr = DirectInputCreate(hInstanceCopy, DIRECTINPUT_VERSION, &lpDirectInput, NULL))) {// 失败提示或处理 return hr; }// 创建一个 DIRECTINPUTDEVICE 界面 //参数 GUID_SysKeyboard 指明了建立的是键盘对象 if( FAILED( hr = lpDirectInput->CreateDevice(GUID_SysKeyboard, &lpKeyboard, NULL))) { // 失败提示或处理 return hr; }// 设定为通过一个 256 字节的数组返回查询状态值 if( FAILED(hr = lpKeyboard->SetDataFormat(&c_dfDIKeyboard))) { // 失败提示或处理 return hr; }// 设定协作模式为独占模式和前台模式,独占模式表面本程序在运行中占有所有键盘资源,而前台模式指出当程序具有焦点时才可以占有键盘资源 if( FAILED( hr = lpKeyboard->SetCooperativeLevel(hWnd, DISCL_EXCLUSIVE | DISCL_FOREGROUND))) { // 失败提示或处理 return hr; } // 设定缓冲区大小 // 如果不设定,缓冲区大小默认值为 0,程序就只能按立即模式工作 // 如果要用缓冲模式工作,必须使缓冲区大小超过 0 // 在此,我们没有必要设定,因为我们就用立即模式工作(还有一种缓冲模式),所有我们将其注调了
/* DIPROPDWORD property;
property.diph.dwSize = sizeof(DIPROPDWORD); property.diph.dwHeaderSize = sizeof(DIPROPHEADER); property.diph.dwObj = 0; property.diph.dwHow = DIPH_DEVICE; property.dwData = DINPUT_BUFFERSIZE;
if( FAILED(hr = lpKeyboard->SetProperty(DIPROP_BUFFERSIZE, &property.diph))) { // 失败 return FALSE; } */ //此处是关键,我们要通过这个函数来锁定键盘,记住,所有的DirectInput资源在使用前都要锁定,在此即获得键盘资源,在知识我们刚才设定的键盘模式才能起作用 hr = lpKeyboard->Acquire(); if FAILED(hr) { // 失败 return FALSE; } return TRUE; }
|
在这段代码中,我们首先定义了 lpDirectInput 和 lpKeyboard 两个指针,前者指向 DIRECTINPUT 对象,后者指向一个
DIRECTINPUTDEVICE 界面。其顺序就是这样的。这和其它COM对象的使用方法都一样,即先创建 COM 对象,然后创建界面,然后再获得 硬件资源,然后使用资源,然后释放。
通过 DirectInputCreate(), 我们为 lpDirectInput 创建了一个 DIRECTINPUT 对象。然后我们调用 CreateDevice 来建立一个DIRECTINPUTDEVICE 界面。 完成这些工作以后,我们便调用 DIRECTINPUTDEVICE 对象的 Acquire 方法来激活对设备的访问权限。在此要特别说明一点,任何一个
DIRECTINPUT 设备,如果未经 Acquire,是无法进行访问的。还有,当系统切换到别的进程时,必须用 Unacquire 方法来释放访问权限,在系统切换回本进程时再调用 Acquire 来重新获得访问权限。
立即模式的数据查询
HRESULT ReadImmediateData( HWND hWnd ) { HRESULT hr; BYTE diks[256]; // 创建键盘状态数据缓冲区存取键盘信息 int i; // 计数器 if( NULL == g_pKeyboard ) return S_OK;// 键盘状态数据缓冲区清0 ZeroMemory( &diks, sizeof(diks) );// 获得键盘所有键的信息,这只是检查一次 hr = g_pKeyboard->GetDeviceState( sizeof(diks), &diks ); if( FAILED(hr) ) { // 如果键盘资源丢失,我们要重新获得 hr = g_pKeyboard->Acquire(); while( hr == DIERR_INPUTLOST ) hr = g_pKeyboard->Acquire(); return S_OK; }// 进行一下轮循,处理键盘信息。 for( i = 0; i < 256; i++ ) { if( diks[i] & 0x80 ) //记录此键的状态,低字节最高位是 1 表示按下,0 表示松开,一般用 diks[i]&0x80 来测试 { switch(i) { //我们可以通过测试计数器i,来判断是哪个键被按下了。 //我们提供几个数据 UP:200 down:208 left:203 right:205 enter:28 space:57 //其实你可以用DirectX中的Samples\C++\DirectInput\Bin\Keyboard.exe程序来测试,只不过那是用 //16进制显示的。 case 200: break; case 0xc8: break; } } } return S_OK; } |
请注意,上面的这段代码只是一个示例,重在使你明白其原理,但并不能满足游戏的需求,因为这其中只查询了一次键盘的全部信息,做了一次轮循,而在游戏中要周期性地查询,并轮循,这就需要你自己用Win32 API函数 SetTimer和 KillTimer 设置初始化 DirectInput 对象函数中在相应的地方设置计计时器,让windows定时向程序发送 WM_TIMER消息,你要通过此消息进行周期性地键盘查询,并在相应的地方解除计时器。
最后一个函数是用于释放指针或DirectInput对象的
void ReleaseDInput(void) { if (lpDirectInput) { if(lpKeyboard) { // Always unacquire the device before calling Release(). lpKeyboard->Unacquire(); lpKeyboard->Release(); lpKeyboard = NULL; } lpDirectInput->Release(); lpDirectInput = NULL; } } |
在这些函数中的注释很明确,关键在于理解其原理,而怎样将他们融入到 Win32 API 程序的基本框架中的,在<<动画程序编写——DirectDraw之旅>> 1-3中的示例代码中已经解释得很明确了,在此不再赘述。不过我们提供其中的代码示例下载。同时你也可以去仔细阅读DirectX 8.0 SDK 包中的 samples\ multimedia\ directdraw\ fullscreenmode 或 \ samples\ multimedia\ directdraw\ windowedmode 这两个工程中的文件,因为为了我们的示例也是照这两个工程改编过来的,读者可以通过仔细阅读代码和对比我们的更改,而更加了解 DirectDraw的运行运行原理。(请注意:是 DirectX 8.0 SDK 包中的示例,而在 9.0 中 DirectX SDK 已经不提供 DirectDraw的示例代码了)
我们就用这七个函数就已经可以创造出一个小游戏了。
我们下面就要利用<<动画程序编写——DirectDraw之旅>> 1-3 中所用的代码进行进一部的游戏开发。 我们先展示一下 DirectX中\ samples\ multimedia\ directdraw\ windowedmode 工程中的截图 在这个动画中有黑色背景,并有很多 DirectX 精灵在漂浮。
这是一个全屏的动画程序,而我们在<<动画程序编写——DirectDraw之旅>> 1-3其中做的改动就是为其加了一个背景,改屏幕分辨率 640×480 为 1024×768.注意,因为我们应用的是全屏模式,即可以独占显存资源,所以我们可以更改屏幕的分辨率。这只是做的小小的改动,而我们的目的只在于让大家更加深入了解。且看下面的这副截图:
而我们还要继续深入编程,我们的思路是,先将程序由先前的全屏程序改编成一个windows的窗口程序,然后将其所有的界面翻新,并改编 DirectX精灵为许多小蘑菇在漂浮,还要加入DirectInput 的组建,用键盘控制一个小娃娃。可以上下左右,并可以斜向飞行。 我们先将此动画的截图展现给大家
怎么样,你有什么想法,是想说:“唉,这还不好办,就是又多加了一个!”,但不要光看截图,不要忘记,我们一定让她动起来,并且是可以控制的,这就不是那么简单的事了! 什么?若有人看到这里感到有些迷茫和泄气,不禁想问:“你说了这么多,那么源代码在那里呢!,光给我们几个函数,又能做什么呢?”,如果你这么想,你也不要太急迫。我们还是先分析一下程序框架吧。
不过,还有一件重要的事情,我还是要重申一边。一定要将 DirectX 的头文件价,和lib文件夹加入到 Visual C++.NET 的默认目录中去,这样编译器就可以正确地找到它们了。 如果你不会加入,就请通过工具栏上的 Tool -> Option… 打开Option 对话框,设置如图:
好了,这样我们的准备工作就算已经做好了。
来看看我们的工程文件结构吧,还有工程中的资源。
在工程资源中我们的 ID 号是都用的字符串表示的,笔者认为这样更加方便。 我想对于工程文件中的 ddutil.cpp 和 dxutil.cpp 文件,读者如果了解有些 DirectDraw编程是不会感到陌生的,我们只是将其引入到我们的工程中了。而我们自己实际编程的文件是 outfly.cpp 文件。
我们的程序叙述如下: 首先进行宏定义,结构设置,和全局变量的声明。 后在 WinMain (windows程序的入口点)中首先初始化一切需要初始化的物件(有windows窗口,DirectDraw对象,和 DirectInput对象),在此我们就调用前文讲过的函数,但要有写改动,读者在会在后面看到的。然后进入消息循环,在其中没有消息时,程序会自动更新画面,在有消息时处理消息。 当遇到 WM_QUIT 消息后,结束整个程序。
我们在一些地方有一些小小的改动,我们来看看吧。
1 我们在 HRESULT InitDirectInput( HWND hWnd ) 函数中的开始加入了
KillTimer( hWnd, 0 ); FreeDirectInput(); |
关掉上一次使用的计时器,并释放 DirectInput 设备。 而在最后加入了
SetTimer( hWnd, 0, 1000 / 100, NULL ); |
用来重新设置计时器。
2 我们在主窗口的消息处理函数中加入了
case WM_ACTIVATE: //当程序先失去焦点,而现在有重新得到焦点时,要重新锁定键盘资源 if( WA_INACTIVE != wParam && g_pKeyboard ) { // Make sure the device is acquired, if we are gaining focus. g_pKeyboard->Acquire(); } break; case WM_TIMER: //因为设置了计时器所以要处理此消息 if( FAILED( ReadImmediateData( hWnd ) ) ) { KillTimer( hWnd, 0 ); MessageBox( NULL, _T("Error reading input state. ") _T("The sample will now exit."), _T("Keyboard"), MB_ICONERROR | MB_OK ); } break; case WM_DESTROY:// Cleanup and close the app FreeDirectDraw(); FreeDirectInput(); // 释放资源 PostQuitMessage( 0 ); return 0L; |
3 在HRESULT ReadImmediateData( HWND hWnd ) 函数中进行了这样的处理,来时时改变小娃娃的坐标。
for( i = 0; i < 256; i++ ) { if( diks[i] & 0x80 ) { switch(i) { case 200: //上键 if( g_me.fPosY > g_me.fVelY) g_me.fPosY -= g_me.fVelY; else g_me.fPosY = 0; break; case 208: //下键 if( g_me.fPosY <= WINDOW_HEIGHT - SPRITE_DIAMETER - g_me.fVelY) g_me.fPosY += g_me.fVelY; else g_me.fPosY = WINDOW_HEIGHT- SPRITE_DIAMETER; break; case 203://左键 if( g_me.fPosX > g_me.fVelX) g_me.fPosX -= g_me.fVelX; else g_me.fPosX = 0; break; case 205://右键 if( g_me.fPosX <= WINDOW_WIDTH - SPRITE_DIAMETER - g_me.fVelX) g_me.fPosX += g_me.fVelX; else g_me.fPosX = WINDOW_WIDTH- SPRITE_DIAMETER; break; } } } |
这些只是其中一些比较重要的改动,还有许多改动,读者会在实际的程序中看到的。如果你觉得:“啊!到这里就结束了,可是我还是感到似乎莫不到头绪,就这样草草收尾了?”,其实文章并没有结束,重头戏还在后面呢,那就不是我的工作了,而是看你有没有耐心去仔细阅读代码了,因为想要把握程序的整体,与其让我将代码放在文章中,还不如读者自己在编译器中自己运行实践一下好,其实我们已经在第二个工程代码中有过详细的解释。但记住一定要按照顺序阅读 工程1,工程2 ,工程3。工程1就是 DirectX中提供的原代码,工程2就是我们改了一个背景的工程,而3就是我们讨论的工程。
|