| DirectDraw編程基礎 |
|
|
| pcant gameres 2006-09-04 |
|
作者:pcant |
|
| |
DirectDraw編程基礎 |
|
| |
本文面向有幾個月學習編程經歷的初學者:看過C++的教程,看的懂基本的C++語法;有點點VC使用經驗,知道怎麼去組建一個工程;理解一些windows編程的基本概念,比如窗口、消息循環等;還有,不懂的地方會去查資料:)。 看過幾本關于DirectDraw的書,這些書都不錯,在此感謝她們的作者。美中不足的是這些書的部分起點較高,雖然我們仍然能夠清晰的理解一些概念,但在組織這些文件上會有不少困惑。在此我重申一下書中的概念,也借此梳理一下自己的思路。廢話少說,言歸正傳。 首先說一些不可不說的東西。我認為它們不可不提,是因為這些東西也許太基礎,高手們往往忽略這些東西對新手的作用。作為一個新手,我覺得掌握程序的框架及組織方法,比多熟悉幾個APIs更迫切一些。Now lets begin: 寫一個遊戲程序,要熟悉其流程,另外要鍛鍊組織程序文件的能力。對新手來說,我建議按部就班的來處理及分析要寫的程序,不主張這個時候你在搞思維跳躍。這是個良好的習慣,當然也有利于我們盡快掌握編程的思想方法。下面來看一個概括的流程及相應的程序框架
(框架顯示不出來。。)
那麼,如何利用上面的流程來構建我們的大體程序框架呢?
我們已經知道一些windows編程方面的東西了,也許你還比較了解MFC。我們這裡不提倡用MFC,盡管它封裝了好多有用的模式,但對我們編遊戲來說,倒是累贅了。好,接著說。既然採用windowsAPI,可以建立個文件WinMain.cpp來處理windows編程中有關窗口的一些問題。這樣,我們在該文件中應該完成創建窗口,處理基本消息(比如按“esc”退出等),控制程序退出等。遊戲過程中窗口的消息是不是也要在這處理呢?當然,不過遊戲當中的窗口就不僅是windows窗口了,顯示部分要靠DirectDraw來控制,那麼我們只好在WinMain.cpp中調用相關的模塊來處理。這麼看來,在WinMain.cpp中幾乎囊括了整個流程,不錯,它就控制了程序的整個框架,為你的程序內核提供了一個平台。平台有了,那麼下一步,GameMain.cpp要誕生了,這個主要用來控制整個遊戲的各個組件,協調各部分工作,完成遊戲設置初始化,遊戲中消息循環,控制遊戲退出。你的才華就在這兒來盡情的發揮了。一般,遊戲程序會有幾個固定的組件的:顯示,音樂,信息輸入。在DirectX中提供了很方便的組件DirectDraw,DirectSound和DirectMusic,DirectInput。相應的我們建立MyDirectDraw.cpp,MyDirectAudio.cpp,MyDirectInput.cpp來控制各部分組件的相應功能。 顯然,這3部分都是為GameMain.cpp服務的,被GameMain.cpp調用。那麼我們可以看出我們的程序應該包括的文件及其包含關系為:
(圖表顯示不出來了,555)
程序文件怎麼去組織,應該由這個表可以看出來。這麼一看,我們發現,WinMain.cpp好像是一個投資者,提供開發平台,他只關注整個項目總的進程,不關注細節。GameMain.cpp好像個項目負責人,整個項目的細節過程由他來策劃,來控制,向上與WinMain.cpp交互,來完成項目,向下協調MyDirectDraw.cpp,MyDirectAudio.cpp,MyDirectInput.cpp之間的工作。MyDirectDraw.cpp,MyDirectAudio.cpp,MyDirectInput.cpp這三個家伙就是員工了,負責各自的工作,完成相應的功能給GameMain.cpp。
組織程序應該就是這麼個思路,當然具體問題具體分析。那麼我們下面來開始看DirectDraw部分了。
首先,做準備工作,安裝DirectX SDK,在VC中添加dxguid.lib和ddraw.lib(本來不想說這個,看到有個教程,它少加了dxguid.lib,鬱悶了我好一陣子,害人頗深感覺)這樣,directdraw程序才能通過編譯。提一下,dxguid.lib中定義了DirectX中會用到的所有全局句柄,ddraw.lib是DirectDraw使用的函數庫。
下面就可以寫代碼了,這裡我們當然主要看MyDirectDraw.cpp該怎麼寫了 為此,我選出了幾個源代碼,做參考研究,它們會與本文一起打包。 我還是習慣先從整體上鳥瞰一下:
一般,在MyDirectDraw.cpp(注意不要忘記引用頭文件ddraw.h)中至少要有兩部分:初始化和結束。先看初始化,所謂初始化無非是個準備工作,需要的東西定義創建出來擺在手邊以備後用。來看看初始化函數intMyDirectDrawInit(void)該怎麼寫。首先定義一個指向DirectDraw對象的指針,創建DirectDraw對象,查詢以獲取最新的DirectDraw接口,設置協作等級,設置顯示模式。通過這些步驟可以創建一個黑色的屏幕了,也就是說已經開闢了我們需要的空間了,當然DirectDraw程序的初始化不會這麼簡單。要操作2d圖形,我們還要接著創建主頁面和緩衝頁面以及離屏頁面,總之根據需要,凡是需要在操作前需要準備好的東西都可以放在這裡。那麼結束 int MyDirectDrawShut(void)就應該釋放我們開闢的東西,一般要釋放主頁面指針,和DirectDraw接口等。
大體就是這麼個樣子,go on,該細一點了,呵呵
先定義指針:LPDIRECTDRAW lpDDraw_temp;代表整個顯示系統 創建對象: if (FAILED(DirectDrawCreate(NULL, &lpDDraw_temp, NULL))) { MessageBox(NULL,TEXT("Direct Draw Create error!"), TEXT("Wrong!"),MB_OK); return(0); }
這裡用了一個FAILED宏來檢測是否創建成功,這可以幫我們跟蹤錯誤。
函數DirectDrawCreate(NULL, &lpDDraw_temp, NULL)完成創建,第一個參數是顯示驅動的全局唯一標志符,這裡null表示目前的顯示設備;第二個參數用來接受創建出來的DirectDraw對象地址,這裡用&lpDDraw_temp接受;第三個參數?不要問,就給它null,不想惹麻煩的話。
查詢DirectDraw接口:if(FAILED(lpDDraw_temp->QueryInterface(IID_IDirectDraw7, (LPVOID *)&lpDDraw7))) { MessageBox(NULL,TEXT("DirectDraw QueryInterface error!"), TEXT("Wrong!"),MB_OK); return(0); }
通過QueryInterface()方法來獲取新接口,這裡是IDirectDraw7而不是IDirectDraw8,指向IDirectDraw7的指針放在lpDDraw7中,這是個全局變量,可以這樣定義LPDIRECTDRAW7 lpDDraw7=NULL;
順便說一下,一般情況下你是應該知道你使用的接口的,這和SDK有關,所以說這一步不是必須的。
設置協作等級: if (FAILED(lpDDraw7->SetCooperativeLevel(main_window_handle, DDSCL_FULLSCREEN | DDSCL_ALLOWMODEX | DDSCL_EXCLUSIVE | DDSCL_ALLOWREBOOT))) { MessageBox(NULL,TEXT("DirectDraw SetCooperativeLevel error!"), TEXT("Wrong!"),MB_OK); return(0); }
決定你這個程序和windows的關系,它向windows申請所用資源,比如它要全屏,獨佔等。第一個參數是主窗口句柄,就是你WinMain()中創建的那個了,第二個參數有幾個控制標志,常用的用法如下:
DDSCL_FULLSCREEN:全屏模式,必須和DDSCL_EXCLUSIVE同時使用 DDSCL_EXCLUSIVE:請求獨佔級別,須和DDSCL_FULLSCREEN同時使用 DDSCL_ALLOWREBOOT:允許系統檢測ctrl+alt+del按鍵消息(這很有用)
我想,這三個就夠用了,其他的就先不用管了 設置顯示模式:if(FAILED(lpDDraw7->SetDisplayMode(800, 600, 16,0,0))) { MessageBox(NULL,TEXT("DirectDraw SetDisplayMode error!"), TEXT("Wrong!"),MB_OK); return(0); }
遊戲中要使用的顯示模式可能和用戶當前顯示模式不一樣,要在此統一設置SetDisplayMode()強制使用它設置的模式,它的前三個參數很容易懂吧,第四個,用0表示使用默認的刷新率,第五個參數這裡是0,有書上說必須用DDSDM_STANDVGAMODE(可以理解,只是不知道這個0什麼意思,我想應該是default的意思吧。
到此為止,我想已經創建出來我們需要的空間了,以後,隨著我們要求的提高,再逐步完善初始化函數,now看看結束函數: 釋放接口: if (lpDDraw7) { lpDDraw7->Release(); lpDDraw7 = NULL; }
以後還要釋放主頁面,緩衝頁面等,需要注意一點的是一定要釋放你申請的資源,這是個好習慣,更應該注意的一點是先創建的一定要後釋放,因為後創建的可能是在先創建的環境下工作的。
到此為止,我們只是做好了最基礎的準備工作,什麼還都不能做呢 想做點什麼嗎?歇會吧,說點不得不說的題外話:
那麼我們來看看顏色吧。有關色彩,分這麼幾種,256色(8位的),16位增強色,24位真彩和32位真彩。256色估計很少用了,16位目前還是主流,所以我們著重看一下16位增強色,通常16位增強色有兩種格式:5.5.5和5.6.5,一般用RGB表示法表示。其中: 5.5.5格式,最高位為Alpha位,表示是不是透明,其餘15位表示顏色,紅綠藍各5位,這種格式可以表示32786種顏色。通過宏
#define _RGB16BIT555(r,g,b)((b%32)+((g%32)<<5)+((r%32)<<10))來轉變成5.5.5格式
對5.6.5格式,顯然,紅藍各5位,綠6位,這樣可以表示65536種顏色,同樣,宏
#define _RGB16BIT565(r,g,b) ((b%32)+((g%64)<<6)+((r%32)<<11))來轉變成5.6.5格式
中間的移位我也搞不清楚是怎麼回事,姑且先不看了,看的越多可能越胡塗哦 那麼到底該用哪種格式?看機器了,大部分可以用5.6.5,當然你可以檢測一下,至于怎麼檢測嘛,我就不說了,查查相關資料就可以了。24位呢?紅綠藍各8位唄,32位?添個Alpha位,其餘同24位。好了顏色就說到這裡。
下面想幹嘛?想在屏幕上搞點顏色出來,參看附的源代碼code1 你會不會發現我們還應該在上面的基礎上添點什麼?對,應該在初始化函數裡創建頁面,也就是DirectDrawSurface對象,那它和DirectDraw對象什麼區別?DirectDraw對象,我們知道是表示整個顯示系統,也就是你的顯卡和顯屏構成的那個系統,你能在顯示器屏幕上直接畫點東西嗎?不行,顯屏上的東西是通過顯存和內存操作把裡面的東西顯示出來,那麼相對應于顯屏,內存中就應該有一張矩形白紙供你作畫,然後才能把它在顯屏上顯示。那張白紙就是DirectDrawSurface對象,代表了顯存或內存裡的一個連續的線性的數據區。這個數據區可以被代表顯示硬件的DirectDraw對象所識別和確認。一般,可以創建的頁面有4種,我們常用的有主頁面(primary surface)和離屏頁面(offscreen plain)先說主頁面,就是一塊顯存,在主頁面中的圖形會顯示到屏幕中,直接在主頁面上操作會有個問題,數據一多,圖象就會不連續,為此可以採用緩衝技術,即建立一個Back buffer(後台緩衝),說白了,就是在內存中再開闢一塊區域,和主頁面的區域對應,這樣就可以不直接操作主頁面,先把數據寫入到這裡,然後通過換頁成為可見。離屏頁面不同了,它是和主頁面一模一樣的畫面,但是它永遠不在屏幕上表現出來,通常被用來存儲位圖,用于將後來的位圖圖象Blit到主頁面或後台緩衝上。那麼,我們來看一下這幾個頁面在工作當中的位置及作用:
(此處有一圖表,顯示不出來)
這樣,我們大體了解了頁面的作用,那麼初始化時就應該創建好,以等待到時對頁面的操作。于是我們的初始化函數中就應該再添加:
memset(&ddsd,0,sizeof(ddsd)); ddsd.dwSize=sizeof(ddsd); //設置dwFlags,告訴DirectDraw哪些成員可用 ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; //定義ddsCaps.dwCaps,請求一個帶後台緩衝的主頁面 ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_COMPLEX | DDSCAPS_FLIP; //定義設置後台緩衝的數量為1 ddsd.dwBackBufferCount = 1; //創建主頁面 if (FAILED(lpDDraw7->CreateSurface(&ddsd, &lpDDprimary, NULL))) { MessageBox(NULL,TEXT("DirectDraw Create primary Surface error!"), TEXT("Wrong!"),MB_OK); return(0); } //設置ddsCaps.dwCaps ddsd.ddsCaps.dwCaps = DDSCAPS_BACKBUFFER; //連接主頁面及後台緩衝 if (FAILED(lpDDprimary->GetAttachedSurface(&ddsd.ddsCaps, &lpDDback))) { MessageBox(NULL,TEXT("DirectDraw Create back Surface error!"), TEXT("Wrong!"),MB_OK); return(0); }
在這裡,我們要定義幾個全局變量: extern LPDIRECTSURFACE7 lpDDprimary; extern LPDIRECTSURFACE7 lpDDback; extern DDSURFACEDESC2 ddsd;
這是一個指向主頁面的指針,一個指向後台緩衝的指針,和一個頁面描述結構。不用說,這些定義你可以放在MyDirectDraw.h中。通過填充ddsd結構的成員來申明你所想創建的頁面的類型。這裡我們沒創建離屏頁面。用主頁面及後台緩衝可以完成一些相對簡單,數據不是很多的圖形顯示,數據過于復雜,就應該創建離屏頁面了。
相應的,在結束時,除了釋放DirectDraw7接口外,還要依次釋放後台緩衝指針和主頁面指針。還是提醒一下,先創建的一定要後釋放,不然你會死的很難堪的。怎麼去Release這些東西,看看code1中的代碼,很容易明白的。
順便我們看一下如何創建離屏頁面,看下面代碼: DDSURFACEDESC2 ddsd; LPDIRECTSURFACE7 lpDDopl; //這兩個定義不用說了吧 memset(&ddsd,0,sizeof(ddsd)); //清空結構內容 ddsd.dwSize=sizeof(ddsd); //設置大小 ddsd.dwFlags = DDSD_CAPS |DDSD_HEIGHT|DDSD_WIDTH; ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;//指定頁面類型 ddsd.dwWidth=600; ddsd.dwHeight=800; //設置離屏頁面大小 if (FAILED(lpDDraw7->CreateSurface(&ddsd, &lpDDopl, NULL))) { MessageBox(NULL,TEXT("DirectDraw Create offscreen plain error!"), TEXT("Wrong!"),MB_OK); return(0); } //創建離屏頁面
Okay!離屏頁面就創建好了,說一下,因為離屏頁面是個獨立的頁面,不隸屬于任何其他頁面,所以你必須指定它的大小。
關于頁面的創建我們就說到這,到這兒,是不是有一種萬事具備,只欠東風的感覺啊?
抬頭一看,天亮了,該睡覺了,睡醒咱們再接著說,先去呼呼了。
……n小時後……
好了,既然只欠東風,我們就來說東風。
簡單的畫圖,我們可以參看code1(在屏幕上打點)
有關頁面的運用的位圖的操作(作圖也就這兩個東西)我還組織不起來,無法把理解到的東西組織到程序中(汗!還沒真正理解,就好意思在這說)我也在學嘛,多理解幾遍,說不定就能夠組織了,那麼,那麼,我們只能像我看過的幾本資料一樣,來拆開來說了,開始照單全收的抄書。希望抄完後,能有點組織的眉目。
從以前那個頁面表,可以看出,這裡的操作無非是載入位圖,貼圖,翻頁顯示,以及對畫面進行剪貼。那我們一步步來說吧。這裡就事論事,就模塊論模塊,代碼段和以前的文件沒多大關聯了。大家看不明白了不要罵我,理解萬歲。
先看載入位圖,即將位圖load到離屏頁面中,要通過windows的HDC來進行存取,用windowsAPI配合DirectX來完成。我們看代碼段:
HDC hdc,hdc1; //聲明HDC對象,hdc用來存儲位圖,hdc1代表離屏頁面的DC HBITMAP bitmap; //聲明HBITMAP對象 hdc=::CreateCompatatibleDC(NULL);//建立與目前顯示模式兼容的DC(參數為null) bitmap=(HBITMAP)::LoadImage(NULL,”bgroud.bmp”,IMAGE_BITMAP,640,480,LR_LOADFROMFILE); //加載640*480的位圖 ::SelectObject(hdc,bitmap); //使用windows函數設置hdc中的內容為bitmap
現在把位圖加載到了DC中,下面就要把DC中的位圖貼到離屏頁面中了
LPDIRECTSURFACE7 lpDDopl; //這個定義不用說了吧 HRESULT result;//幹嘛用的?往下看 lpDDopl->GetSurfaceDesc(&ddsd);//ddsd和我們前面定義過的一樣 result= lpDDopl->GetDC(&hdc1);//用GetDC()來取得離屏頁面的DC if(result!=DD_OK) MessageBox(“取得暫存區DC失敗”);//是否取得成功,了解result做這個用 ::Bitblt(hdc1,0,0,ddsd.dwWidth,ddsd.dwHeight,hdc,0,0,SRCCOPY); //這個就是貼圖用的windows函數 lpDDopl->releaseDC(hdc1);//釋放離屏頁面的DC,一定要釋放
到此我們已經把位圖貼到離屏頁面中了,下面應該把離屏頁面DC中的位圖填充到back buffer中,然後通過換頁顯示出來。先來了解兩個DirectDraw的貼圖函數Blt和BltFast。這兩個函數的原型在老王翻譯的directx開發手冊中有詳細說明,在我主頁上可以down到,你可以查閱一下。這裡我簡單說一下:
HRESULT Blt( LPRECT lpDestRect, //目標頁面的區域,lpDestRect定義其左上右下點坐標 LPDIRECTDRAWSURFACE7 lpDDSrcSurface,//源頁面指針 LPRECT lpSrcRect, //源頁面的區域 DWORD dwFlags,//控制標志,詳見老王的手冊 LPDDBLTFX lpDDBltFx)//圖形變換的信息結構,詳情請自己查閱 HRESULT BltFast( DWORD dwX, //目的區域左上x坐標 DWORD dwY, //目的區域左上y坐標 LPDIRECTDRAWSURFACE7 lpDDSrcSurface,//源頁面指針 LPRECT lpSrcRect, //源頁面的區域 DWORD dwTrans,//轉換參數,見老王手冊
這兩者的差別就是Blt多了圖形放縮功能,但是BltFast效率較高,如何選用已經很清楚了。調用這兩個函數中的一個就能夠實現從離屏頁面到back buffer的貼圖,代碼如下:
lpDDback->BltFast(0,0,lpDDopl,CRect(0,0,640,480),DDBLTFAST_WAIT);
//lpDDback是我們以前聲明過的後台緩衝,CRect(…)是個CRect類的對象,如果我們已聲明了一個CRect rect;這裡就可用&rect來代替 單看貼圖這步操作,還是很easy的。 看起來好像離顯示只有一步之遙了啊,right,只要翻頁(flip)一下就okay了
先看翻頁函數: HRESULT Flip( LPDIRECTDRAWSURFACE7 lpDDDestSurface,//你想翻到的目標頁 DWORD dwFlags) //通常設為DDFLIP_WAIT
說明一下:第一個參數為null時,表示翻到目前頁面的所連接的下一個頁面。當換頁對象是可見的頁面,比如主頁面換頁鏈,進行換頁的Flip函數與系統CPU是異步執行的。這就是說,在這些可見的頁面上,調用Flip函數,它只是簡單的告訴顯示硬件該進行換頁了,並不需要等待換頁操作在硬件設備中實際完成後才返回。這是因為顯示硬件(顯示器)只有在完成一次垂直刷新後才能進行一次換頁。所以,Flip函數調用成功,並不意味著換頁已經完成,在實際的換頁操作進行之前,對即將成為主頁面的後台緩存是不能鎖定和進行Blit操作的。要讓Flip函數成為與系統CPU同步的操作,在調用時指定DDFLIP_WAIT標志即可
代碼同樣簡單: lpDDprimary->Flip(NULL,DDFLIP_WAIT);
小功告成,到這兒我們已經把一個指定的位圖bgroud.bmp在屏幕上顯示出來了,這個就可以作為你的遊戲的背景圖,比如潛水艇遊戲的那張大海圖。
需要說明一下的是,如果我們要在哪個頁面上操作(一般是back buffer),最好操作前先鎖定,用完再解鎖,防止其他GDI程序的幹擾。舉個例子: lpDDback->Lock(NULL,&ddsd,DDLOCK-WAIT|DDLOCK_SURFACEMEMORYPTR, NULL); //鎖定後台緩衝 lpDDback->Unlock(NULL);//解鎖後台緩衝
把這兩句代碼分別添加到相應位置即可。
再往下我們該做什麼了?背景有了,應該引進我們的精靈了(精靈這個術語真是可愛),然後想想看如何能讓我們的精靈動起來。老實說,到這,我快崩潰了。下面的內容應該屬于陌生的部分吧(如果前面的內容我還有點熟悉的話)。好在我這個人是很執著的,所以只有繼續硬著頭皮往下寫了,理解不到位的地方還請大家包涵,同時希望大家指教。
先做做準備工作,去吃飯先,休息一會再來…………
……又是n個小時……
有飯吃的日子真爽啊,珍惜吧,朋友們:)深吸一口氣,let’s go on
運用DirectDraw來做動畫,我們把不同的圖片載入到離屏頁面中,然後定時貼入到back buffer中,翻頁顯示就出現動畫效果了。來看個例程:
(待續) From:活著為了遊戲 ─ GameRes Blog | |
|
|
|
|
 |
|
|
 |
|
|
|
|