細心的朋友可以很明顯地注意到圖示中的右上角的圖解中說明,表面對象有兩個寬度,一個是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中的SamplesC++DirectInputBinKeyboard.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就是我們討論的工程。