天天看點

遊戲開發新手入門之位圖化圖形

   簡介

   終于,你已經掌握了制作一個完整遊戲的基礎知識了,隻不過你現在還隻能使用GDI。今天,我們就學習使用DirectX來執行每一件你以前用GDI完成的工作,以及一些關于DirectX其它的東東。具體内容是:裝載(調用)位圖,使用位塊傳輸,填充表面,使用剪裁闆、顔色鍵等拷貝位圖。

   你可以在不了解前一章内容的基礎上學習本章,但象素格式是很重要的,我将經常直接或間接的提到它,是以你至少應該看看上一章關于象素格式的部分!另外,我假設你已經本系列的第一、二、三、四章,并且擁有一個DirectX SDK遊戲開發平台。準備好了嗎?發動引擎吧,女士們、先生們!

   裝載位圖

   不管你信不信,你的确已經知道了把位圖裝載到DirectDraw表面的大部分知識。怎麼會這樣呢?Well,在Windows GDI下裝載位圖同在DirectDraw下極其相似,隻是有一點點不同。輕輕的回憶一下,我們曾經使用LoadImage()函數得到位圖的句柄,然後把位圖選入到記憶體裝置上下文中,最後利用BitBlt()函數把圖形從記憶體裝置上下文中拷貝到顯示裝置上下文中,裝置上下文可以用GetDC()函數得到。如果這個承擔顯示任務的就是DirectDraw表面(現在我們就是要用它),我們就可以針對性的得到DirectDraw表面的裝置上下文!感謝上帝,IDirectDrawSurface7接口提供了一個極其簡單的函數來得到這個裝置上下文:

HRESULT GetDC(HDC FAR *lphDC);

   該函數的傳回類型同所有DirectDraw函數的傳回類型相同。如果函數調用成功,參數就是一個HDC類型的裝置上下文的指針,很簡單吧!本章就是從把一個位圖裝載到DirectDraw表面講起的。千萬要記住使用完了表面裝置上下文後,你一定要釋放它哦!你可能已經想到了,用表面接口函數ReleaseDC()完成:

HRESULT ReleaseDC(HDC hDC);

   你不用回頭去看關于GDI部分的位圖調用,我将把适合于DirectDraw的位圖調用展現給你。唯一不同的是:不是直接把裝置上下文作為一個參數,而是用一個DirectDraw表面指針取代了它,然後函數從表面得到裝置上下文,用它來拷貝圖形,最終釋放裝置上下文。(可能這裡我說的有些混亂,但你看一下下面的程式代碼就都明白了):

int LoadBitmapResource(LPDIRECTDRAWSURFACE7 lpdds, int xDest, int yDest, int nResID) {   HDC hSrcDC; // source DC - memory device context   HDC hDestDC; // destination DC - surface device context   HBITMAP hbitmap; // handle to the bitmap resource   BITMAP bmp; // structure for bitmap info   int nHeight, nWidth; // bitmap dimensions   // first load the bitmap resource   if ((hbitmap = (HBITMAP)LoadImage(hinstance, MAKEINTRESOURCE(nResID), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION)) == NULL)    return(FALSE);   // create a DC for the bitmap to use   if ((hSrcDC = CreateCompatibleDC(NULL)) == NULL)    return(FALSE);   // select the bitmap into the DC   if (SelectObject(hSrcDC, hbitmap) == NULL)   {    DeleteDC(hSrcDC);    return(FALSE);   }   // get image dimensions   if (GetObject(hbitmap, sizeof(BITMAP), &bmp) == 0)   {    DeleteDC(hSrcDC);    return(FALSE);   }   nWidth = bmp.bmWidth;   nHeight = bmp.bmHeight;   // retrieve surface DC   if (FAILED(lpdds->GetDC(&hDestDC)))   {    DeleteDC(hSrcDC);    return(FALSE);   }   // copy image from one DC to the other   if (BitBlt(hDestDC, xDest, yDest, nWidth, nHeight, hSrcDC, 0, 0, SRCCOPY) == NULL)   {    lpdds->ReleaseDC(hDestDC);    DeleteDC(hSrcDC);    return(FALSE);   }   // kill the device contexts   lpdds->ReleaseDC(hDestDC);   DeleteDC(hSrcDC);   // return success   return(TRUE); }

   上面這段代碼被設計成從資源調用位圖,但你可以很容易就把它修改成從外部檔案調用位圖,或者更理想的是,首先你從資源調用位圖,如果失敗,再試圖從外部檔案調用位圖。從外部調用,需要記住的是調用LoadImage()函數時加上LR_LOADFROMFILE标志。最美妙的事情是,函數BitBlt()自動完成象素格式的轉換。舉例說,當我們把24-bit的位圖放入記憶體裝置上下文,再把它傳送(拷貝)到16-bit色彩深度的表面,所有的顔色将得到正确的顯示,不用顧忌象素格式是555還是565,很友善吧,哦?

   如果你要控制位圖傳遞的實際過程,而不是使用BitBlt()這樣簡單的函數,你有兩個選擇。第一個,你可以修改這個函數,需要利用BITMAP結構的bmBits成員,它是一個組成圖象的位的LPVOID指針變量。第二種方法,如果你真的想控制圖象的調用過程,你可以自己編寫函數,思路是使用标準的I/O函數來打開圖象檔案,然後讀取它。要這樣做,你需要了解位圖檔案的結構。我們将不涉及這種函數的編寫,因為目前的對我們來說已經足夠了,但我還是要為你将來的大展鴻圖做一點點鋪墊。

  位圖格式

   令人高興的是,要自己寫一個調用位圖的函數,有一個Win32結構的位圖頭檔案可以利用。讀取這個頭檔案的資訊,用fread()這樣簡單的函數就可以了。所有的位圖檔案都有這樣一個頭檔案,它包含了位圖的全部資訊。BITMAPFILEHEADER就是這個頭檔案結構的名字,下面是它的原形:

typedef struct tagBITMAPFILEHEADER { // bmfh   WORD bfType; // file type - must be "BM" for bitmap   DWORD bfSize; // size in bytes of the bitmap file   WORD bfReserved1; // must be zero   WORD bfReserved2; // must be zero   DWORD bfOffBits; // offset in bytes from the BITMAPFILEHEADER   // structure to the bitmap bits } BITMAPFILEHEADER;

   我就不詳細介紹這些成員了,因為注釋裡已經說得很清楚了,隻要使用fread()讀取它們就可以了。注意要檢測bfType成員是否等于字元“BM”,若是,說明你正在處理一個有效的位圖。在此之後,有另一個頭檔案需要讀取,它包含位圖的尺寸、壓縮類型等圖象資訊。以下是它的結構:

typedef struct tagBITMAPINFOHEADER{ // bmih   DWORD biSize; // number of bytes required by the structure   LONG biWidth; // width of the image in pixels   LONG biHeight; // height of the image in pixels   WORD biPlanes; // number of planes for target device - must be 1   WORD biBitCount; // bits per pixel - 1, 4, 8, 16, 24, or 32   DWORD biCompression; // type of compression - BI_RGB for uncompressed   DWORD biSizeImage; // size in bytes of the image   LONG biXPelsPerMeter; // horizontal resolution in pixels per meter   LONG biYPelsPerMeter; // vertical resolution in pixels per meter   DWORD biClrUsed; // number of colors used   DWORD biClrImportant; // number of colors that are important } BITMAPINFOHEADER;

   隻有幾個成員需要解說一下。第一個,注意壓縮格式。大多數的位圖你都需要做解壓縮的操作。最普通的位圖壓縮格式是run-length編碼(RLE),但隻能應用于4-bit或8-bit圖象,在此情況時,成員biCompression将分别是BI_RLE4和BI_RLE8,我們就不讨論這種壓縮格式了,但它真的很簡單,很容易了解,你如果要了解它是不會有任何麻煩的。

   第二個,對于高色彩的位圖,biClrUsed和biClrImportant這兩個成員通常設定為0,是以不用太在意它們。對于BI_RGB這種未壓縮格式的位圖,成員biSizeImage也将被設定為0。最後,針對我們的目的,其它的結構成員都不是很重要的,我們隻需要注意位圖的長、寬和色彩的深度(biWidth、biHeight、biBitCount)。

   讀取完了這些頭檔案的資訊後,如果位圖是8-bit或者以下色彩深度的(也就是調色闆模式),調色闆的資訊會緊跟在這些資訊之後。也許出乎你的意料,調色闆的資訊不是存儲在PALETTEENTRY結構中,而是在RGBQUAD結構中。RGBQUAD結構如下:

typedef struct tagRGBQUAD { // rgbq   BYTE rgbBlue;   BYTE rgbGreen;   BYTE rgbRed;   BYTE rgbReserved; } RGBQUAD;

   不要問我為什麼紅、綠、藍以倒序方式排列,事實就是這樣!讀取RGBQUAD中的資料,把資料傳遞給DirectDraw調色闆的數組。記得要把每個PALETTEENTRY的peFlag設定成PC_NOCOLLAPSE。

   之後呢(調色闆資訊不一定存在,因為高彩模式下就沒有),你将發現圖象位(image bits),你可能會想到建立一個指針,在記憶體中配置設定足夠的空間來控制這些圖象位資料,然後讀取它們。對極了,我正要這樣幹。假設把存儲在BITMAPINFOHEADER結構中的資訊頭檔案稱作info,你的圖象位指針稱作fptr,實施的代碼如下:

UCHAR* buffer = (UCHAR*)malloc(info.biSizeImage); fread(buffer, sizeof(UCHAR), info.biSizeImage, fptr);

   要記住,在一些情況下,biSizeImage的值可能為0,是以有必要在上面的代碼運作前檢測它一下。如果它被設定為0,你将不得不計算圖象由多少個象素構成,每個象素需要多少個位元組。

   寫你自己的位圖調用函數,并非什麼難事兒。但你覺得不需要,就用我們開始介紹的方法好了。這個話題告一段落,下面讓我們看看DirectDraw的精華:使用位塊傳輸。

  使用位塊傳輸

   位塊傳輸是顯示卡操控位圖資料的一部分,你同樣可以用它來進行顔色填充。就像我們過一會兒看到的,随着硬體的性能提高,會有很多經典的技巧。DirectX有權使用硬體的加速功能,但要記住,如果DirectX使用的加速功能不被機器硬體支援,将自動啟用硬體仿真層(HEL),但這也并非萬無一失,因為有些功能靠硬體仿真層是無法實作的(否則誰還買3D加速卡^_^),是以你需要檢測你的函數是否調用成功。

   GDI位塊傳輸可以在DirectDraw程式設計中使用,而且有時也的确是這樣做的。然而DirectDraw具有其自身的位塊傳輸函數,它們通常更加适合于程式設計環境,而且比GDI的相應的函數執行得更快。DirectDraw位塊傳輸函數名為Blt()和BltFast(),都是有IDirectDrawSurface7接口提供的。兩者不同處是BltFast()不處理剪切、放縮等其它Blt()做的有趣的事情。如果在硬體仿真層上,BltFast()要比Blt()快10%左右,但如果有硬體加速卡支援(硬體加速卡主要就是為位塊傳輸服務的),二者的速度就差不多了,而且現在大多數的機器都有硬體加速卡,是以我總是使用Blt()。讓我們仔細看看這個神奇的東東:

HRESULT Blt(   LPRECT lpDestRect,   LPDIRECTDRAWSURFACE7 lpDDSrcSurface,   LPRECT lpSrcRect,   DWORD dwFlags,   LPDDBLTFX lpDDBltFx );

   由于Blt()所擁有的最後一個參數,使其能做很多特殊的事兒。該參數配有一個标志常量清單,我将會向你介紹其中最有用的幾個。另外,注意在把位圖從一個表面向另一個表面傳遞時,你應該調用目的表面的Blt(),不是源表面的。好了嗎?以下是函數的參數說明:

   ※ LPRECT lpDestRect:參數lpDestRect為指向結構RECT的指針,它給出了位塊傳輸操作的目标表面的左上角和右下角的坐标。如果源表面和目标(目的)表面的大小不一緻,Blt()将把源表面的圖象自動按照比例适應目标表面的大小。如果此參數為NULL,則使用整個目标表面。

   ※ LPDIRECTDRAWSURFACE7 lpDDSrcSurface:參數lpDDSrcSurface為指向DirectDraw表面的指針,該DirectDraw表面為位塊傳輸之源表面。如果你隻是要用顔色填充目的表面,你可以把它設定為NULL。

   ※ LPRECT lpSrcRect:參數lpSrcRect為指向結構RECT的指針,它給出了位塊傳輸(有的書上也叫作“位轉換”)操作的源表面的左上角和右下角的坐标。如果此參數為NULL,則使用整個源表面。

   ※ DWORD dwFlags:對于這個參數有一個巨大的标志常量清單,可以用“|”組合使用标志常量。其中一些是為Direct3D服務的,是以我将把我們常用的列出來: · DDBLT_ASYNA:位塊傳輸異步的以先入先出(FIFO)的順序接收。如果沒有空間可用于FIFO硬體,則該調用失敗。

   · DDBLT_COLORFILL:使用DDBLTFX結構的資料成員dwFillColor作為RGB顔色填充目标表面的矩形。

   · DDBLT_DDFX:DDBLTFX結構的dwDDFX成員指定了位塊傳輸的使用效果。

   · DDBLT_DDROPS:DDBLTFX結構的dwDDROP成員指定了光栅操作(ROPS),該操作不是Win32 API的一部分。

   · DDBLT_KEYDEST:顔色鍵與目标表面相關聯。

   · DDBLT_KEYDESTOVERRIDE:DDBLTFX結構的dckDestColorkey成員是目标表面的顔色鍵。

   · DDBLT_KEYSRC:顔色鍵與源表面相關聯。

   · DDBLT_KEYSRCOVERRIDE:DDBLTEX結構的dckSrcColorkey成員是源表面的顔色鍵。

   · DDBLT_ROP:DDBLTFX結構的dwROP成員是位塊傳輸的ROP(光栅操作代碼),這些ROP與Win32 API中定義的那些相同。

   · DDBLT_ROTATIONANGLE:DDBLTFX結構的dwRotationAngle成員是表面的旋轉角度,其機關為1/100度。

   · DDBLT_WAIT:在位塊傳輸器忙的情況下,推遲DDERR_WASSTILLDRAWING傳回值(位塊傳輸函數調用失敗傳回的值之一),而當位塊傳輸開始或發生另一個錯誤時立即傳回。

   我幾乎總是使用DDBLT_WAIT标志。顔色鍵标志也是很重要的,我們過一會兒再說它。現在,還有最後一個Blt()參數需要說一下:

   ※ LPDDBLTFX lpDDBltFX:這是一個指向DDBLTFX結構的指針,它可以包含各種特殊要求的資訊。如果沒有什麼特殊要求,你就設定為NULL好了。讓我們仔細看看這個結構。我警告你,它是很魁梧的:^_^

typedef struct _DDBLTFX{   DWORD dwSize;   DWORD dwDDFX;   DWORD dwROP;   DWORD dwDDROP;   DWORD dwRotationAngle;   DWORD dwZBufferOpCode;   DWORD dwZBufferLow;   DWORD dwZBufferHigh;   DWORD dwZBufferBaseDest;   DWORD dwZDestConstBitDepth;   union {    DWORD dwZDestConst;    LPDIRECTDRAWSURFACE lpDDSZBufferDest;   };   DWORD dwZSrcConstBitDepth;   union {    DWORD dwZSrcConst;    LPDIRECTDRAWSURFACE lpDDSZBufferSrc;   };   DWORD dwAlphaEdgeBlendBitDepth;   DWORD dwAlphaEdgeBlend;   DWORD dwReserved;   DWORD dwAlphaDestConstBitDepth;   union {    DWORD dwAlphaDestConst;    LPDIRECTDRAWSURFACE lpDDSAlphaDest;   };   DWORD dwAlphaSrcConstBitDepth;   union {    DWORD dwAlphaSrcConst;    LPDIRECTDRAWSURFACE lpDDSAlphaSrc;   };   union {    DWORD dwFillColor;    DWORD dwFillDepth;    DWORD dwFillPixel;    LPDIRECTDRAWSURFACE lpDDSPattern;   };   DDCOLORKEY ddckDestColorkey;   DDCOLORKEY ddckSrcColorkey; } DDBLTFX, FAR* LPDDBLTFX;

   如果我整個詳細的介紹這個結構,恐怕我們都會受不了的,并且也沒有這個必要。是以我隻告訴你一些重點的部分。謝天謝地,該結構的大部分都是為z緩沖區(z-buffers)和α消息服務的,我們不用理會它。嘻嘻,我的工作量變得很小了:

   ※ DWORD dwSize:象所有的DirectX的結構一樣,當你初始化這個結構時,該成員放置結構的大小。

   ※ DWORD dwDDFX:這些是位塊傳送所能接受的一些特殊操作。清單并不長,别擔心喔!

   · DDBLTFX_ARITHSTRETCHY:位塊傳輸時,在Y軸算術拉伸位圖。

   · DDBLTFX_MIRRORLEFTRIGHT:y軸上的鏡像變換。表面從左到右完成鏡像效果。

   · DDBLTFX_MIRRORUPDOWN:x軸上的鏡像變換。表面從上到下完成鏡像效果。

   · DDBLTFX_NOTEARING:把動畫圖像塊轉移到前段緩存時可以使用這個參數,這樣位塊傳輸操作的時間會與螢幕重新整理率相一緻,并使畫面撕裂的可能性減小到最小。

   · DDBLTFX_ROTATE180:位塊傳輸時,把表面順時針旋轉180度。

   · DDBLTFX_ROTATE270:位塊傳輸時,把表面順時針旋轉270度。

   · DDBLTFX_ROTATE90:位塊傳輸時,把表面順時針旋轉90度。

   需要詳細解釋的可能隻有DDBLTFX_NOTEARING。遊戲離不開動畫,動畫制作者主要關心的通常是動畫的速度和性能,速度太快會導緻圖象品質的惡化。光栅掃描顯示系統(我們基本上用的都是這種顯示器)利用電子束掃描每一條水準線上的螢幕象素點。象素行從螢幕的左上角開始更新,到螢幕的右下角結束。各象素行都被稱為掃描線。電子束在每一行掃描線的末端被關掉,而電子槍重新瞄準下一行的起始點,這個過程成為水準回掃。當過程執行到螢幕掃描線的最後一行時,電子束再次被關掉,電子槍重新瞄準螢幕的左上角。電子槍從螢幕的右下角重新瞄準到左上角的過程所需的時間被稱為垂直回歸或者螢幕空白周期。如果在視訊控制器顯示視訊資料的同時,視訊資料被CPU做了更改,這時就會産生問題。在PC機中,螢幕的重新整理率通常在60Hz到100Hz之間,而現在的CPU則可以在每秒處理成百上千的指令,這樣就很可能導緻位于視訊記憶體區的圖象在視訊系統完成顯示之前發生更改。圖象斷裂的結果被稱為圖象撕裂。就我的經驗而言,使用了DDBLTFX結構的DDBLTFX_NOTEARING後,圖象撕裂就不是什麼問題了。

   ※ DWORD dwROP:使用這個标志來指定Win32模式的光栅操作代碼。同GDI函數BitBlt()和StretchBlt()中的相對應參數的功能一樣。可以通過IDirectDraw7::GetCaps()函數得到可能的光栅操作清單,可以通過“|”組合标志常量,确定源矩形表面和目标矩形表面是怎樣結合的。

   ※ DWORD dwRotationAngle:這是用來旋轉位圖角度的,可以旋轉任意角度。這是非常棒的,但不幸的是,它隻能在HAL層(硬體抽象層)上工作,這就意味着使用者的顯示卡要支援加速旋轉,否則……,但不能保證每個使用者都有這種高檔的顯示卡,是以你需要考慮周全。如果你真的需要旋轉處理,你隻好自己寫這樣的函數了,這可是一個大話題,需要另寫一部指南了,是以我們就越過它。但請注意,如果是90度倍數的角度,你可以使用DDBLTFX_ROTATE90等。這将使你回避顯示卡不幹活的風險。

   ※ DWORD dwFillColor:如果你要使用位塊傳輸來填充顔色,你必須把顔色放入到這個參數中。

   ※ DDCOLORKEY ddckDestColorKey,ddckSrcColorKey:你要使用顔色鍵時必須要指定這些成員。這兩個家夥是很重要的。但暫時我們還不讨論它們,因為我們過一會兒才講顔色鍵。

   以上這些就是DDBLTFX結構中比較有用的成員了,這就意味着你現在擁有足夠的知識進行位塊傳輸了!如果你現在感覺有些混亂,不要緊,你實踐過一段時間就會好了。讓我們看看幾個例子。假設你已經有了一個叫作lpddsBack的後緩沖區,你想把它裡面的内容傳輸到主表面,很簡單的,你看:

lpddsPrimary->Blt(NULL, lpddsBack, NULL, DDBLT_WAIT, NULL);

   再輕輕回憶一下,第一個參數和第三個參數分别是位塊傳輸的目标矩形和源矩形。由于我把它們都設定為NULL,就說明是對全部的表面進行拷貝。現在,讓我們在看看,假設你有一個在離屏表面裡的名字叫作lpddsTileset的16×16大小的圖形,你想把它傳輸到後緩沖區,變成32×32大小的,你需要這樣做:

RECT dest, src; SetRect(&src, 0, 0, 16, 16); // the coordinates of the tile SetRect(&dest, 0, 0, 32, 32); // where you want it to end up on the back buffer lpddsBack->Blt(&dest, lpddsTileset, &src, DDBLT_WAIT, NULL);

   這個例子同上一個例子不同處在與這個例子設定了位塊傳輸的坐标。由于這兩個矩形區的大小不同,Blt()依照比例适當的變更了圖形的大小。最後,我們還得舉例說明一下DDBLTFX結構,就做一個顔色填充的例子吧。假設你在16-bit色彩模式下,是565象素格式,你要把你的後緩沖區填充為藍色。下面就是你應該做的:

DDBLTFX fx; INIT_DXSTRUCT(fx); // zero out the structure and set dwSize fx.dwFillColor = RGB_16BIT565(0, 0, 31); // set fill color to blue lpddsBack->Blt(NULL, NULL, NULL, DDBLT_WAIT | DDBLT_COLORFILL, &fx);

   注意參數的設定,前三個都是NULL,你自己想想原因吧!好了,讓我們看看另一個位塊傳輸函數BltFast()吧。它隻是Blt()的簡化版本,是以我們不需要太多的時間解釋它。下面是它的原形:

HRESULT BltFast(   DWORD dwX,   DWORD dwY,   LPDIRECTDRAWSURFACE7 lpDDSrcSurface,   LPRECT lpSrcRect,   DWORD dwTrans );

   你可以看得出來,它同Blt()極其相似。它也是IDirectDrawSurface7接口的成員函數,被目标表面調用。來看看它的參數:

   ※ DWORD dwX,dwY:這是Blt()和BltFast()之間不同的地方。是目标表面上進行位塊傳輸的x和y坐标。如果源矩形大于目标矩形,則調用失敗,因為BltFaxt()不能幹按比例變換的事兒及其它能夠通過Blt()完成的工作。

   ※ LPDIRECTDRAWSURFACE7 lpDDSrcSurface:這是源表面,同Blt()的一樣。

   ※ LPRECT lpSrcRect:這個也同Blt()的一樣。是在源表面中定義了矩形左上角和右下角的RECT結構。

   ※ DWORD dwTrans:定義了位塊傳輸類型,它的标志清單很簡單,隻有四個标志:

   · DDBLTFAST_DESTCOLORKEY:使用目标顔色鍵的透明位塊傳輸。

   · DDBLTFAST_NOCOLORKEY:沒有透明的普通複制位塊傳輸。

   · DDBLTFAST_SRCCOLORKEY:使用源顔色鍵的透明位塊傳輸。

   · DDBLTFAST_WAIT:如果位塊傳輸忙的話,不産生DDERR_WASSTILLDRAWING消息。一旦位塊傳輸能夠開始或者發生另一個錯誤時傳回。

   就這些!BltFast()支援顔色鍵。下面讓我們看一個簡單的示例。把整個後緩沖區拷貝到主表面:

lpddsPrimary->BltFast(0, 0, lpddsBack, NULL, DDBLTFAST_WAIT);

   到現在為止,你已經是一個位塊傳輸的專家了,還有幾件事情對于DirectX程式很重要:顔色鍵和剪裁闆。你可能知道在某些情況下關于顔色鍵有上百萬種标志,那麼到底怎樣使用它呢?讓我們一起看看吧!

  顔色鍵

   顔色鍵使一個位圖被拷貝到另一個位圖上時,不使所有的象素都顯現。例如:當你把一個精靈(遊戲中會動的對象一般都稱作精靈)拷貝到地圖上(背景上)時,這個精靈位圖一般不會是一個精靈形狀的位圖,它通常都是一個矩形位圖,位圖裡包含你所需要的精靈(除非你的精靈就是一個矩形機器人):

   遊戲中,地圖是先于精靈顯示的,那麼精靈走到樹後時,還應有相應被遮擋的部分,這個先不讨論,下一節再說。現在,對我們更重要的是,如果不應用顔色鍵,這個精靈将永遠帶着這個黑色底框,這是絕對不能容忍的。

   為了解決這個問題,我們使用源顔色鍵。這個源顔色鍵告訴你精靈矩形的哪些顔色将不被拷貝(當然我們是讓黑色不被拷了)。一個顔色鍵由兩個值組成:一個低位顔色值,一個高位顔色值。當一個顔色鍵被申請使用時,在兩個值之間的顔色,包括這兩個值的顔色都将不會被拷貝。在DirectX中有一個結構用來處理它,叫作DDCOLORKEY,看看吧:

typedef struct _DDCOLORKEY{ DWORD dwColorSpaceLowValue; DWORD dwColorSpaceHighValue; } DDCOLORKEY, FAR* LPDDCOLORKEY;

   很簡單的結構,我就不解釋了。我将展示給你使用了顔色鍵之後的效果。我使用顔色鍵的高位和低位兩個值僅僅把黑色包括在它們之間。是以,黑色是唯一不會被拷貝的顔色。

   好多了,是不是?這就是我們想得到的結果!現在,在我告訴你怎樣建立和使用顔色鍵之前,我還有說一說目标顔色鍵,盡管我們的确我們不常用到它(我們常用的是源顔色鍵)。鑒于源顔色鍵定義了哪些顔色鍵不能被拷貝,目标顔色鍵定義了哪些顔色不能被寫入(覆寫)。聽起來很怪異,是不是?我也有同感。舉個執行個體你就明白了。當你要把A位圖拷貝到B位圖的下面,意思就是把A位圖作為背景,例如由于某種理由,需要把一個文本框拷貝到空的後緩沖區,然後再把背景畫面拷貝到這個後緩沖區,但你又不能覆寫先前的文本框。是以,在後緩沖區裡除了文本框的那些黑色的部分才能被寫入象素。

   我也不清楚你什麼時候需要處理這種情況,但是你的确可能用到(一旦你用到了,可千萬要告訴我哦,我一直沒有遇到這種情況呢^_^)。現在,你已經知道什麼是顔色鍵了,讓我們看看怎樣使用它們吧!

  置顔色鍵

   在DirectDraw中有兩種方法使用顔色鍵。第一種,你可以連結一個顔色鍵(或者兩個,如果你同時使用源和目标顔色鍵)到表面,然後在位塊傳輸時定義DDBLT_KEYSRC,DDBLT_KEYDEST,DDBLTFAST_SRCCOLORKEY或DDBLTFAST_DESTCOLORKEY标志,具體使用哪個标志,取決于你使用哪個位塊傳輸函數和使用哪種顔色鍵。第二種,你可以建立一個顔色鍵,然後通過DDBLTFX結構傳送給位塊傳輸操作。當你不斷地需要使用顔色鍵時,我向你推薦第一種方法;反之,當你偶然要使用一次顔色鍵,就用第二種方法吧!

   你可以把顔色鍵連結到已經建立好了的表面,也可以在建立表面的同時建立顔色鍵。兩種方法我都将詳細告訴你。假設你工作在16-bit顯示模式下,是565象素格式,你要在後緩沖區使用一個僅包含黑色的源顔色鍵。如果你的後緩沖區已經建立好了,你就可以簡單建立一個DDCOLORKEY結構,然後把它傳遞給IDirectDrawSurface7::SetColorKey()函數,如下所示:

HRESULT SetColorKey(   DWORD dwFlags,   LPDDCOLORKEY lpDDColorKey );

   記住要用FAILED()宏檢測這個函數的傳回值,保證一切都在計劃之中。函數的參數很簡單:

   ※ DWORD dwFlags:決定所使用顔色鍵類型的标志。以下三個是你将用到的:

   · DDCKEY_COLORSPACE:該結構包含一個顔色範圍,如果結構包含的是單獨的顔色鍵則不作設定。

   · DDCKEY_DESTBLT:該結構指定顔色鍵或者顔色範圍作為用于位塊傳輸操作的目标顔色鍵。

   · DDCKEY_SRCBLT:該結構指定顔色鍵或者顔色範圍作為用于覆寫操作的顔色鍵。

   ※ LPDDCOLORKEY lpDDColorKey:這是指向DDCOLORKEY結構的指針。

   就這麼多。你可以根據你所需要使用的顔色鍵适當地定義位塊傳輸的标志。注意,一個顔色鍵連結到表面,并不意味着你每一次必須使用它。如果你隻定義了DDBLT-WAIT或DDBLTFAST_WAIT标志,顔色鍵将被忽略。下面是設定顔色鍵的方法:

DDCOLORKEY ckey; ckey.dwColorSpaceLowValue = RGB_16BIT565(0, 0, 0); // or we could just say '0' ckey.dwColorSpaceHighValue = RGB_16BIT565(0, 0, 0); if (FAILED(lpddsBack->SetColorKey(DDCKEY_SRCBLT, &ckey))) {   // error-handling code here }

   如果你要為已經建立的顔色鍵連結一個表面,有幾件事情你需要做。首先,當你定義DDSURFACEDESC2結構的有效成員時,你需要使dwFlags成員包含DDSD_CKSRCBLT或者DDSD_CKDESTBLT标志,具體使用哪個标志,取決于你要使用哪種顔色鍵。回頭再看看DDSURFACEDESC2結構,它包含兩種DDCOLORKEY結構。一種稱為ddcdCKSrcBlt,另一種稱為ddcdCKDestBlt,填寫适當的結構來建立表面。你就需要幹這麼多!以下是關于640×480大小的離屏表面的執行個體代碼:

// set up surface description structure INIT_DXSTRUCT(ddsd); ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT | DDSD_CKSRCBLT; ddsd.dwWidth = 640; // width of the surface ddsd.dwHeight = 480; // and its height ddsd.ddckCKSrcBlt.dwColorSpaceLowValue = RGB_16BIT(0,0,0); // color key low value ddsd.ddckCKSrcBlt.dwColorSpaceHighValue = RGB_16BIT(0,0,0); // color key high value ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; // type of surface // now create the surface if (FAILED(lpdd7->CreateSurface(&ddsd, &lpddsBack, NULL))) { // error-handling code here }

   關于顔色鍵的部分到此結束。現在我們可以進行本章最後一項了——剪切。

  IDirectDrawClipper接口

   假設你有一個圖形,而你卻隻想把它的一部分顯示在螢幕上。你應該怎樣做呢?如果你曾經在DOS下編寫過遊戲,你可能對剪切望而生畏。Well,在DirectX下,這隻是小菜一碟!首先的首先,這的确是很容易做到的,因為DirectX用矩形來做位塊傳輸,改變矩形的坐标要比指定記憶體中的哪一部分圖形被拷貝(就像在DOS下所做的)要容易的多。其次,DirectDraw還為此提供了一個接口——IDirectDrawClipper。

DirectDraw的剪切性能完全可以滿足你的要求,你不但可以剪切矩形區域,你還可以剪切任意多邊形區域!真的是很棒!如果你在螢幕上同時要顯示一個主視窗,在螢幕的一邊顯示一個狀态欄,在螢幕的底部顯示一個文字提示欄,并且用黑色分隔開這些區域,你可以用DirectDraw的剪切功能完成它,非常容易的!

   做這樣的操作你需要分幾步走。首先你得得到一個指向IDirectDrawClipper接口的指針。沒什麼難的,隻需要調用IDirectDraw7::CreateClipper(),如下:

HRESULT CreateClipper(   DWORD dwFlags,   LPDIRECTDRAWCLIPPER FAR *lplpDDClipper,   IUnknown FAR *pUnkOuter );

   在你調用這個函數前,你應該首先聲明一個LPDIRECTDRAWCLIPPER類型的指針,這樣你才能把它的位址傳遞給這個函數。記着要檢測函數的調用是否成功哦!以下是函數參數的解釋:

   ※ DWORD dwFlags:簡直就是幸福——這個參數還沒有使用過,設定為0。

   ※ LPDIRECTDRAWCLIPPER FAR *lplpDDClipper:把你的LPDIRECTDRAWCLIPPER指針的位址傳遞給它。

   ※ IUnknown FAR *pUnkOuter:你知道怎麼做,設定為NULL。^_^

   一旦你有了自己的接口指針,下一件事就是建立剪切清單(clip list)。多個剪切的矩形組成了剪切清單。需要使用到RGNDATA結構,它包含了足夠的資訊來定義一個任意的區域,看看它的原形吧:

typedef struct _RGNDATA { RGNDATAHEADER rdh; char Buffer[1]; } RGNDATA;

   我需要詳細解說一下它的參數。

   ※ RGNDATAHEADER rdh:它是RGNDATA結構中嵌套的一個結構。它包含了第二個參數——Buffer的所有資訊。它定義了需要剪切區域裡的矩形的數目,整個區域的形狀等資訊。我們過一會兒再具體讨論它。

   ※ char Buffer[1]:這并不意味着是隻有一個值得數組;它将是在記憶體中任意大小的區域來控制着實際的剪切區域資料。同樣的,對于RGNDATA結構,我們要聲明一個指向該結構的指針,然後使用malloc()函數為RGNDATAHEADER設定足夠的記憶體空間,也就是為剪切清單設定足夠的空間。有一件事我要提醒你:剪切清單裡的矩形按從上到下,然後從左到右排列,不能交疊。

   我已經意識到你有些胡塗了,不要緊,繼續學習,一切會好起來的。下面是RGNDATAHEADER結構的原形,它比較好了解:

typedef struct _RGNDATAHEADER {   DWORD dwSize;   DWORD iType;   DWORD nCount;   DWORD nRgnSize;   RECT rcBound; } RGNDATAHEADER;

   ※ DWORD dwSize:結構的大小。簡單的使用sizeof(RGNDATAHEADER)好了。

   ※ DWORD iType:它描述了每個區域的外形。它是另有玄機的,以後我們再把它擴充開來讨論。現在,你隻要把它設定為RDH_RECTANGLES就好了,這也正是我們需要的。

   ※ DWORD nCount:這是組成該區域的矩形的數量。換句話說,就是你的剪切清單理的矩形數。

   ※ DWORD nRgnSize:為緩沖區的大小設定它,将得到自身的區域資料。由于我們使用n個矩形組成了剪切區域,是以它的大小應該是sizeof(RECT) *nCount。

   ※ DWORD rcBound:這是一個矩形類型,包含了剪切清單裡的所有矩形。通常你把它設定成表面上需要剪切部分的尺寸。

   現在,我們可以建立一個剪切清單了。首先我們聲明一個LPRGNDATA的指針,配置設定給剪切清單足夠的記憶體空間;然後根據上面我們所學的設定每個成員。讓我們看看最簡單的執行個體,你可能經常要用到它哦!它隻有一個剪切區域,并且,就是整個螢幕,是640×480顯示模式的。以下就是代碼:

// first set up the pointer -- we allocate enough memory for the RGNDATAHEADER // along with one RECT. If we were using multiple clipping area, we would have // to allocate more memory. LPRGNDATA lpClipList = (LPRGNDATA)malloc(sizeof(RGNDATAHEADER) + sizeof(RECT)); // this is the RECT we want to clip t the whole display area RECT rcClipRect = {0, 0, 640, 480}; // now fill out all the structure fields memcpy(lpClipList->Buffer, &rcClipRect, sizeof(RECT)); // copy the actual clip region lpClipList->rdh.dwSize = sizeof(RGNDATAHEADER); // size of header structure lpClipList->rdh.iType = RDH_RECTANGLES; // type of clip region lpClipList->rdh.nCount = 1; // number of clip regions lpClipList->rdh.nRgnSize = sizeof(RECT); // size of lpClipList->Buffer lpClipList->rdh.rcBound = rcClipRect; // the bounding RECT

   一旦有了剪切清單,你需要把它作為你的剪裁闆。你将調用IDirectDrawClipper接口的函數SetClipList()。就是下面這個東東:

HRESULT SetClipList(   LPRGNDATA lpClipList,   DWORD dwFlags );

   你所要做的就是把RGNDATA的指針傳遞給它。參數dwFlags沒有用,設定為0。現在,剪切清單設定好了,還需要一步,就是把剪切清單連結到你所要控制的表面上,這需要調用SetClipper()函數,它将表面指針作為其唯一的參數:

HRESULT SetClipper(LPDIRECTDRAWCLIPPER lpDDClipper);

   你知道應該怎樣做:就是把你已經設定好的接口的指針傳遞給它。任何時候,你要位塊傳輸一個有剪裁闆相關聯的表面,剪裁闆将做所有的工作。是以如果你要在螢幕上顯示一個貼片的一部分,例如傳輸一個矩形坐标為{-10,-10,6,6},或者類似的矩形貼圖,都不會有麻煩的。很不錯吧,嗯?

   關于剪切的最後一件事情是必須要用free()函數釋放你用malloc()設定的記憶體空間。還有,就是由于某種原因在調用SetClipList()或SetClipper()失敗後,在傳回錯誤代碼前或你要根據失敗的結果進行操作前,要釋放記憶體空間。在你完成用LPRGNDATA的指針設定剪切清單後,這個指針就沒有存在的意義了,是以它占用的記憶體空間将被立即釋放。

  總結

   到此,關于DirectDraw的部分就讨論完了!你真的從這六章裡學到了很多的知識,如果你堅持到現在,那麼祝賀你,你真的已經走了很長一段路了。對于這一章我們所學習的,我将把它們組合在一起,給你一個Demo程式,它将從資源調用位圖,使用位塊傳輸位圖,顔色填充,放縮位圖比例,使用剪切功能。

   仍然有些東東我們應該讨論,例如頁面的切換(flipping),雙緩沖區的應用等,無論如何,我們還将繼續,是以不必擔心我們會遺漏重要的内容。

   現在,最初的原始積累已經結束,我們以後的焦點會從建立Windows程式轉移到建立一個貼圖基礎的RPG遊戲。以後的章節将包括用DirectInput建立一個好的輸入機制,寫一個基礎的引擎腳本,播放背景音樂和配音,等等。下一章,我們将學習為貼圖遊戲制作一個簡單的卷軸遊戲引擎,很容易的,沒有你想象的那麼難哦!

繼續閱讀