使用標準GDI實現遊戲品質的動畫系統

 

 

使用標準GDI實現遊戲品質的動畫系統
燕良 2002年1月
http://www.diamondgarden.net/
 

前言 2
GDI基礎 3
繪製一個位圖(BITMAP)對象 3
常用像素格式 4
WINDOWS下的基本動畫系統 4
動畫驅動方式 4
播放動畫 5
消除閃爍 6
透明色(COLOR KEY)處理 7
ALPHA混合 9
讀取JPEG,GIF文件 10
子窗口管理 12
進階技巧--使用DIB 14
像素操作 14
RLE壓縮 15
參考 15
華山論鍵 15
其它類庫 16
前言
說到實現遊戲品質的動畫,很多人會立刻想到DirectX,沒錯DirectDraw很強大,但是並不是必須用DirectDraw才行。動畫後面的理論和技巧都是一樣的,這和末端使用什麼API沒有太大關係(如果那API不是太~~慢的話)。就筆者實現的NewImage Lib的測試結果,內部所有像素數據的存儲和運算都純軟件實現,最後一步輸出到屏幕使用GDI的性能比DirectDraw低不到10%,在Window9X系統上要低20%左右,這對很多軟件來說是絕對可以接受的。

現在應用程序界面越做越華麗,除了支持SKIN外,很多人都想在程序中加入一些例如sprite動畫這種原本用在遊戲上的技術,因爲這原因引入DirectX API,顯然是不值得的(況且DX版本升級頻繁,DX8中已經用DirectGraphic取代了DirectDraw)。本文將以筆者使用標準GDI函數實現的商業遊戲爲例,帶你進入高品質2D動畫編程領域,並且保證其設備無關性。

本文假設讀者有C/C++語言知識,Windows編程基礎,GDI基本概念。下面我將主要講述我在過去工作中積累的經驗和一些技巧,但是將不講解以上基本概念。讀者最好有MFC基礎,本文給出的代碼將主要使用MFC,但是其中的道理卻不限於MFC。

GDI基礎
繪製一個位圖(Bitmap)對象
  GDI的所有操作都是在DC(device context)上進行的,所以首先你應該有DC的概念,如果你對DC還不瞭解,現在就去翻一翻Windows編程的書吧。
 首先我們要Load一個Bitmap對象,使用Win32 API可以寫成這樣:
 file://從資源Load一個位圖,如果從文件load的話,可以使用::LoadImage()
HBITMAP hbmp=::LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_MYBMP));
如果使用MFC可以這樣寫:
    CBitmap bmp;
    Bmp.LoadBitmap(IDB_MYBMP);
想把這個位圖對象繪製到窗口上就要先得到窗口的DC,然後對這個DC操作。請留意創建MemoryDC的代碼,後面會用到。
Win32 API的版本:
file://假設位圖大小爲100*100像素
    file://假設hwnd是要繪製的窗口的HANDLE
    HDC hwnddc=::GetDC(hwnd);
    HDC memdc=::CreateCompatibleDC(hwnddc);
    HBITMAP oldbmp=::SelectObject(memdc,hbmp);
    ::BitBlt(hwnddc,0,0,100,100,memdc,0,0,SRCCOPY);
    if(oldbmp)
        ::SelectObject(memdc,oldbmp);
    DeleteDC(memdc);
    ::ReleaseDC(hwnd,hwnddc);
MFC版本:
    file://假設是在一個CWnd派生類的成員函數中
    CClientDC dc(this);
    CDC memdc;
    memdc.CreateCompatibleDC(&dc);
    CBitmap *oldbmp=memdc.SelectObject(&bmp);
    dc.BitBlt(0,0,100,100,&memdc,0,0,SRCCOPY);
    if(oldbmp)
        memdc.SelectObject(oldbmp);
也可以這樣:
     CClientDC dc(this);
 dc.DrawState(CPoint(0,0),CSize(100,100),&bmp,DST_BITMAP);

基本的代碼就是這樣,當然有更多的API可以用,這就要看你自己的了。J
常用像素格式
  要進行圖像編程的化對像素格式不瞭解似乎說不過去。我想應該有較多的人並不太瞭解,所以這裏簡要的介紹一下。
1. 8bit
  也叫做256色模式。每個像素佔一個字節, 使用調色板。調色板實際上是一個顏色表,簡單的講就是,我們有256個油漆桶(因爲像素的取值範圍是0到255),每個油漆桶裏面漆的顏色都由紅,綠,藍(RGB)三中基本的油漆按不同比例配置而成。所以我們指定一個像素的顏色的時候只需要指定它用的第幾號桶就好了。
  這種模式造就了DOS時代的神奇模式—13H(320*200*256色),因爲320*200*1Byte正好是16bit指針尋址能力的範圍。這種模式有2的18次方種顏色(通過改變調色板實現),可以同時顯示256中顏色。這模式剛剛推出的時候,有人驚呼這是人類智慧的結晶呢!也是這種模式造就了1992年WestWood的<<卡蘭蒂亞傳奇>>和1995年大宇資訊的<<仙劍奇俠傳>>這樣的經典遊戲。
  在Windows下硬件調色板應該極少用到,但是你可以用軟件調色板來壓縮你的動畫,這也是在2D遊戲中常用的技巧。
2. 16bit
  這也是筆者最喜歡的模式。它不使用調色板。每個像素佔兩個字節,存儲RGB值。我覺得這種像素格式的效果(同時顯示顏色數)和存儲量(也影響速度)取得了比較好的統一。但是如果你是寫應用程序的話,我勸你不要用它。因爲它的RGB值都不是整個BYTE,例如565模式(16bit的一種模式),它的RGB所佔用的bit就是這樣的:
      RRRR  RGGG  GGGB  BBBB
3. 24bit
  每個像素有三個BYTE,分別存儲RGB值,這對你來說是不是很方便?是不是太好了?可惜對我們可憐的計算機卻不是,因爲CPU訪問奇數的地址會很費勁,而且在硬件工藝上也有很多困難(具體我也不太清楚,請做過硬件的高手指點),所以你會發現你的顯卡不支持這種模式,但是你可以在自己的軟件中使用。
4. 32bit
  每個像素4個BYTE,分別存儲RGBA,A值就是Alpha,也就是透明度,可以用像素混合算法實現多種效果,後面你就會看到。
Windows下的基本動畫系統
動畫驅動方式
  先略說一下動畫的基本原理,程序播放動畫一般過程都是: 繪製—擦除—繪製,這樣的重複過程,只要你重複的夠快,至少每秒16次(被稱作16FPS,Frame per Second),我們可憐的眼睛就分辨不出單幀的圖像了,看上去就是動畫了。
  在Windows環境下要驅動這樣重複不停的操作有兩種方法:
1. 設置Timer
  這很簡單,只要設置一個足夠短的Timer,然後響應WM_TIME(對應MFC中的OnTimer函數)就可以滿足絕大部分應用程序的需要。缺點是不夠精確,而且Win2000和Win9x系統的精確性又有較大差異。
2. 在消息循環中執行動畫操作
這是在遊戲中常用的方法,一般都會把WinMain中的消息循環寫成這樣:
    while( TRUE )
    {
        // Look for messages, if none are found then
        // update the state and display it
        if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
        {
            if( 0 == GetMessage(&msg, NULL, 0, 0 ) )
            {
                // WM_QUIT was posted, so exit
                return (int)msg.wParam;
            }
           TranslateMessage( &msg );
           DispatchMessage( &msg );
        }
        else
        {
            if( g_bActive )//在主窗口不激活時不更新,以節省資源
            {
         file://執行動畫更新操作
            }
            // Make sure we go to sleep if we have nothing else to do
            else WaitMessage();
        }
 }
如果你使用MFC,則需要重載CWinAPP的Run虛函數,把上述消息循環替換進去。
播放動畫
現在我們有了一個適當的時機執行更新操作了,現在就讓我們試試動畫吧。下面的代碼將不再提供Win32的版本。
爲了敘述方便,我需要一個播放動畫的窗口,它必須是一個CWnd的派生類,假設這個類叫做CMyView,我們將在這個窗口中繪製動畫。首先我們爲這個類添加一個成員函數”void CMyView::RenderView()”,你可以使用上面提到的方法調用這個函數。
現在準備工作都做好了,我們的動畫該怎麼存儲呢?別提動畫GIF89a格式(如果你覺得只有GIF纔有動畫的話,那我勸你去做美術好了,別幹程序了),如果你只想要個簡單的動畫播放當然可以,但是如果你想要做複雜點的,交互式動畫,我勸你還是別用那東西。假設我們有一個4幀的動畫,怎麼存儲它呢?我首先想到的就是存4個BMP文件,然後讀入到一個CBitmap對象數組中,但是尊敬的大師Scott Meyers警告我們不要使用多態數組,因爲編譯器在某些情況下不能準確計算數組中對象的大小,所以下標運算符會產生可怕的效果。然後我就想到了用CBitmap指針數組,這到是不錯,不過管理起來稍嫌麻煩。現在看看我最終的解決方法吧。把一個幀序列安順序拼接成一個文件,象這樣:
 
然後用它創建一個CImageList對象,讓我們仔細看一下創建的方法,使用
BOOL CImageList::Create( int cx, int cy, UINT nFlags, int nInitial, int nGrow );
函數,前面兩個參數用來指定我們一幀動畫的尺寸。這樣就創建了一個空的ImageList,這樣做的好處是可擴展行比較強。下面我們需要把那個幀序列文件Load到一個CBitmap對象中,你可以存成JPG或者GIF文件來節省容量(後面將提到讀取這些文件的簡單方法,並且附一個實用類)。當我們有了一個合適的CBitmap對象後,可以把他添加到我們的ImageList中,使用:
BOOL CImageList::int Add( CBitmap* pbmImage, COLORREF crMask );
一個實例:
const int SPRIRT_WIDTH=32;
const int SPIRIT_HEIGHT=32;
….
m_myimglist.Create(SPIRIT_WIDTH,SPIRIT_HIGHT,ILC_COLOR24|ILC_MASK,1,1);
if(bmp.Load(“myani.bmp”))
m_myimglist.Add(&bmp,RGB(152,152,152));
  好了,現在我們已經準備好了這些數據,讓我們來實作渲染函數吧,下面這端代碼可以循環播放上面的4幀動畫,並且支持透明色(如果你不知道這個名字,稍後有講解)哦!
  void CMyView::RenderView()
  {
        CclientDC dc(this);
        Static int curframe=0;
         m_myimglist.Draw(&dc,curframe,Cpoint(0,0),ILD_TRANSPARENT);
        curframe++;
        If(curframe > m_myimglist.GetImageCount())
            Curframe=0;
}
 上面這個代碼沒有寫擦除的操作,因爲這根據具體需要有較大不同。如果你只有一個精靈動畫的話,你可以用一個Bitmap對象保存精靈所佔矩形區域的圖像。你也可能需要有一個大的背景圖每幀都要更新(這裏我不討論象dirty rect這樣的優化方法),所以你只要每次都畫背景,然後畫精靈就好了。
怎麼樣?你已經實現了基本的動畫系統,就是這麼簡單。
消除閃爍
  如果你真正實現上面的代碼的話,你會發現畫面一閃一閃的,十分的不爽。L 很多人都會怪到GDI頭上,他們又會罵MS,說GDI太慢了。其實非也(不是指MS不該罵,呵呵),任何直接寫屏幕的操作都會產生閃爍,在DOS下直接寫顯存或者用DirectDraw API直接寫Primary Surface都會閃爍,因爲你每個更新顯示的操作都會被用戶馬上看到(因爲垂直回掃的原因, 或許會有延遲)。
  消除閃爍最簡單也是最經典的方法就是雙緩衝(Double buffer)。所謂的雙緩衝其實道理非常簡單,就是說我們在其它地方(簡單的說就是不針對屏幕,不顯示出來的地方)開闢一個存儲空間,我們把所有的動畫都要渲染到這個地方,而不是直接渲染到屏幕上(針對屏幕的存儲區域)。在GDI中,直接針對屏幕就是窗口DC,”不可見的地方”一般可以用Memory DC。在把所有動畫渲染到後臺緩衝之後,再一下次整體拷貝到屏幕緩衝區!
在純軟件2D圖形引擎中,雙緩衝一般意味着在內存中開闢一個區域用來存儲像素數據。而在DirectDraw中可以創建Back Surface,在把所有動畫渲染到Back Suface上之後,然後使用Flip操作使其可見,Flip操作因爲只是設置可見surface的地址,所以非常快速。
讓我們重寫一下void CMyView::RenderView()函數,來用GDI實現雙緩衝:
  void CMyView::RenderView()
  {
        CClientDC dc(this);
        CRect rc;
        GetClientRect(rc);
  CDC memdc;
   memdc.CreateCompatibleDC(&dc);
    CBitmap bmp;
        Bmp. CreateCompatibleBitmap (&dc,rc.Width(),rc.Height());
    CBitmap *oldbmp=memdc.SelectObject(&bmp);

        Static int curframe=0;
         m_myimglist.Draw(&memdc,curframe,Cpoint(0,0),ILD_TRANSPARENT);
        curframe++;
        If(curframe > m_myimglist.GetImageCount())
Curframe=0;
    if(oldbmp)
           memdc.SelectObject(oldbmp);
        dc.BitBlt(0,0,rc.Width(),rc.Height(),&memdc,0,0,SRCCOPY);
}
其中創建一個Bitmap對象,然後選入Memory DC是必須的,因爲CreateCompatibleDC所創建的DC裏面只含有一個1*1像素的單色Bitmap對象,所以如果缺了這個步驟,任何在MemoryDC上的繪圖操作都會沒有效果。延伸出一個問題, CreateCompatibleBitmap函數的第一個參數顯然不可寫成&memdc,如果那樣的化,你就創建了一個單色的位圖,我想你肯定不希望這樣。J
重寫後的函數看上去似乎多了很多無謂的操作,這是因爲我們現在只有一個動畫對象,如果我們有多個動畫,而且還需要繪製動畫的子窗口,那這樣做的效果就會非常的好,不會有任何閃爍,而且向文章最後提到的圖形MUD客戶端,還能達到60FPS呢(在我家的賽陽433上)。
到此爲止,我們的基本動畫系統已經有了一個很好的基礎了。
透明色(color key)處理
  透明色就是指在繪製一張圖片的時候,該顏色的像素不會被繪製上去,這通常用來做遊戲的spirit動畫,所以你可以看到各種形狀不規則的人物動畫。但是他們的數據都是一個矩形的像素區域,只是繪製的時候有些像素不被畫上去罷了。
  GDI提供一個TransparentBlt()函數來支持Color Key,你可以在MSDN中查到該函數的說明。但是我的代碼中使用這個函數後,在Win9X系統下產生了嚴重的資源泄漏,但是在Win2000下卻沒事,所以如果你也發現這問題的話,我建議你使用下面的代碼,來把一個CBitmap透明的繪製到DC上。假設你有一個CBitmap的派生類CMyBitmap:

BOOL CMyBitmap::DrawTransparentInPoint(CDC *pdc, int x, int y, COLORREF mask/*要過濾掉的顏色值*/)
{
    file://Quick return
    if(pdc->GetSafeHdc()==NULL)
        return FALSE;
    if (m_hObject == NULL)
        return FALSE;

    CRect DRect;
    DRect=Rect();
    DRect.OffsetRect(x,y);
    if(!pdc->RectVisible(&DRect))
        return FALSE;

    COLORREF crOldBack=pdc->SetBkColor(RGB(255,255,255));
    COLORREF crOldText=pdc->SetTextColor(RGB(0,0,0));

    CDC dcimg,dctrans;
    if(dcimg.CreateCompatibleDC(pdc)!=TRUE)
        return FALSE;
    if(dctrans.CreateCompatibleDC(pdc)!=TRUE)
        return FALSE;

    CBitmap *oldbmpimg=dcimg.SelectObject(this);

    CBitmap bmptrans;
    if(bmptrans.CreateBitmap(Width(),Height(),1,1,NULL)!=TRUE)
        return FALSE;

    CBitmap *oldbmptrans=dctrans.SelectObject(&bmptrans);

    dcimg.SetBkColor(mask);
    dctrans.BitBlt(0,0,Width(),Height(),&dcimg,0,0,SRCCOPY);

    pdc->BitBlt(x,y,Width(),Height(),&dcimg,0,0,SRCINVERT);
    pdc->BitBlt(x,y,Width(),Height(),&dctrans,0,0,SRCAND);
    pdc->BitBlt(x,y,Width(),Height(),&dcimg,0,0,SRCINVERT);

    if(oldbmpimg)
        dcimg.SelectObject(oldbmpimg);
    if(oldbmptrans)
        dctrans.SelectObject(oldbmptrans);
    pdc->SetBkColor(crOldBack);
    pdc->SetTextColor(crOldText);

    return TRUE;
}
Alpha混合
  Alpha混合是一種像素混合的方法。所謂的像素混合就是使用一定的算法把兩個像素的值混合成一個新的像素值(倒,和沒說一樣),通常我們都把兩個像素的值,分別叫做源(src)和目的(dst),然後把混合後的結果存入dst中:
  dst= src blend dst
如果源像素和目的像素都是RGBA格式,你可以使用每個像素的Alpha信息(或者叫做Alpha通道)組合出各種運算公式,例如
  dst= src*src.alpha+dst*dst.alpha;
  或者
dst=src*src.alpha + dst*(1-src.alpha)//這裏我們假設alpha值是0~1的浮點數。
可惜標準GDI沒有支持類似這種操作的函數(起碼我沒找到),它只支持另一種Alpha混合,我把它叫做const alpha blend,也就是把兩幅都不包含Alpha通道的圖像的按照一個固定的Alpha值混合到一起,也就是每個像素都使用同一Alpha值。GDI的支持這個操作的函數是:
AlphaBlend(
  HDC hdcDest,
  int nXOriginDest,
  int nYOriginDest,
  int nWidthDest,
  int hHeightDest,
  HDC hdcSrc,
  int nXOriginSrc,
  int nYOriginSrc,
  int nWidthSrc,
  int nHeightSrc,
  BLENDFUNCTION blendFunction
);
這個API的參數個數略多了一些,但是我想其中的位置參數你可以輕鬆搞定,還有就是源DC和目的DC,當然了,我們的GDI只能對DC操作,而不是對我們的像素數據,而我們只要把我的位圖select到DC中就OK了,最後一個參數是一個結構,是用來指定Alpha的運算方式的,請看一個實際的例子:
BLENDFUNCTION bf;
 bf.AlphaFormat=0;
 bf.BlendFlags=0;
 bf.BlendOp=AC_SRC_OVER;
 bf.SourceConstantAlpha=100;//指明透明度,取值範圍是0~255
 
 AlphaBlend(pdc->GetSafeHdc(),rc.left,rc.top,rc.Width(),rc.Height(),
  memdc.GetSafeHdc(),0,0,rc.Width(),rc.Height(),bf);
 也許你看過很多遊戲,在彈出文字對話框的時候都是在遊戲畫面上蒙一層半透明的黑色,然後在這上面印字。使用上述操作就可以達到此效果。你可以先建立一個Memory DC,然後把他填充爲黑,然後把Alpha值設爲128,然後混合到你要繪製的DC上(不一定是窗口DC哦,記得我們前面將的雙緩衝嗎?)就OK了。
讀取JPEG,GIF文件
 JPEG壓縮算法綜合的信號學和視覺心理學,而GIF格式,特別是支持動畫的GIF89a格式爲了節約容量也做了很多種非常變態的優化,所以要寫一個完全支持這些標準格式的解碼器相當困難,也沒有必要。
 如果你需要進行JPEG文件的讀寫我推薦你使用Intel Jpeg Lib,速度相當令人滿意。而GIF由於授權問題,沒有任何官方組織提供的讀寫代碼。
 如果你只是需要讀入JPEG和靜態GIF(或者只一幀的動態GIF),我推薦你使用Windows提供的OleLoadPicture函數,下面這段代碼可以把一個JPG,GIF,BMP讀入到Bitmap對象中:
BOOL CIJLBitmap::Load(LPCTSTR lpszPathName)
{
    BOOL bSuccess = FALSE;
   
    file://Free up any resource we may currently have
    DeleteObject();
   
    file://open the file
    CFile f;
    if (!f.Open(lpszPathName, CFile::modeRead))
    {
        TRACE(_T("Failed to open file %s, Error:%x/n"), lpszPathName, ::GetLastError());
        return FALSE;
    }
   
    file://get the file size
    DWORD dwFileSize = f.GetLength();
   
    file://Allocate memory based on file size
    LPVOID pvData = NULL;
    HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, dwFileSize);
    if (hGlobal == NULL)
    {
        TRACE(_T("Failed to allocate memory for file %s, Error:%x/n"), lpszPathName, ::GetLastError());
        return FALSE;
    }
    pvData = GlobalLock(hGlobal);
    file://ASSERT(pvData);
    if(pvData==NULL)
    {
        TRACE(_T("Failed to lock memory/r/n"));
        return FALSE;
    }
    // read file and store in global memory
    if (f.Read(pvData, dwFileSize) != dwFileSize)
    {
        TRACE(_T("Failed to read in image date from file %s, Error:%x/n"), lpszPathName, ::GetLastError());
        GlobalUnlock(hGlobal);
        GlobalFree(hGlobal);
        return FALSE;
    }
   
    file://Tidy up the memory and close the file handle
    GlobalUnlock(hGlobal);
   
    file://create IStream* from global memory
    LPSTREAM pStream = NULL;
    if (FAILED(CreateStreamOnHGlobal(hGlobal, TRUE, &pStream)))
    {
        TRACE(_T("Failed to create IStream interface from file %s, Error:%x/n"), lpszPathName, ::GetLastError());
        GlobalFree(hGlobal);
        return FALSE;
    }
   
    // Create IPicture from image file
    if (SUCCEEDED(::OleLoadPicture(pStream, dwFileSize, FALSE, IID_IPicture, (LPVOID*)&m_pPicture)))
    {
        short nType = PICTYPE_UNINITIALIZED;
        if (SUCCEEDED(m_pPicture->get_Type(&nType)) && (nType == PICTYPE_BITMAP))
        {
            OLE_HANDLE hBitmap;
            OLE_HANDLE hPalette;
            if (SUCCEEDED(m_pPicture->get_Handle(&hBitmap)) &&
                SUCCEEDED(m_pPicture->get_hPal(&hPalette)))
            {
                Attach((HBITMAP) hBitmap);
                m_Palette.Attach((HPALETTE) hPalette);
                bSuccess = TRUE;
            }
        }
    }
   
    file://Free up the IStream* interface
    pStream->Release();
   
    return bSuccess;
}
這個class的完整代碼請看文章最後的參考。
子窗口管理
  你也許注意過幾乎所有遊戲界面中的窗口都是使用動畫的從屏幕外飛出(而且是半透明的,這你已經可以做到了)。遊戲中一般都使用自己的UI系統。這裏我們可以藉助Windows對窗口的管理來輕鬆實現各種動畫子窗口。
  首先讓我們從最簡單的開始。假設在我們的動畫窗口中需要一個漂亮的按鈕怎麼辦,我勸你最好不要使用CBitmapButton,因爲你已經上了每秒重畫窗口16次以上這條賊船,我建議你在每次重畫父窗口的時候重畫所有子窗口,如此一來子窗口上如果要求有動畫操作,也可以輕鬆實現了。既然做了,就把它做到最好。J
那我們怎麼定義一個button呢?你也許想到自己定義一個矩形區域,然後在父窗口的消息響應函數中檢測是否是對此區域操作,這樣在重畫父窗口的時候特殊的畫一次這個矩形區域就好了。這樣是可以實現,但是顯然不符合我們的OOP精神,界面元素一多,你很可能就會亂了陣腳。最後的解決方法當然是使用我們可愛的CWnd類,顯然所有的界面元素都可以作爲一個CWnd派生類的對象。不過我建議你不要從CButton派生,這帶來的麻煩遠多於它的價值。從CWnd派生一個類,然後在Create時注意使用WS_CHILD風格,並且指定父窗口爲我們的動畫窗口。
下面一個問題是如何調用這些子窗口重畫操作呢?第一種較好的解決方法是先建立這樣一個虛基類:
CmyAniWnd :public CWnd
{
 …
 virtual void Render(CDC *pdc)=0;
 …
}
假設你有一個Button類和一個TextBox類:
CmyButton : public CmyAniWnd
CmyTextBox: public CmyAniWnd
這兩個類都必須實現Render函數,這樣在父窗口類中你可以保存一個指針數組,例如這樣:
CPtrArray m_allchild;
在創建一個Button時這樣寫:
CmyButton *pbtn=new CmyButton;
m_allchild.Add(pbtn);
pbtn->Create(…);
然後在我們父窗口的RenderView函數(前面提到的,每次更新調用)中這樣寫即可:
CmyAniWnd *pchild=NULL;
for(int I;I<m_allchild.GetSize();I++)
{
 pchild=static_cast<CmyAniWnd*>(m_allchild.GetAt(i));
 ASSERT(::IsWindow(pchild->GetSafeHwnd());
 pchild->Render(&memdc);
}
這是一個典型的虛函數的應用,在調用這些子窗口的Render函數時,我們不需要知道它到底是Button還是TextBox,虛函數機制會自動幫我們找到該調用的函數。還有一點就是,請注意,一定要把子窗口渲染到我們的後臺緩衝,也就是Memory DC中,否則還是會閃爍的。
上面這種方法適合於子窗口數目固定,更高級的界面會要求觸發某個事件的時候產生一個子窗口,子窗口不斷更新自己,並且在適當的時候把自己從UI系統中去除。讓每個子窗口管理自己的生命期,是個不錯的主意,不是嗎?那你最好不要使用上面保存指針數組的方法,那樣的話,子窗口在殺死自己的時候還要通知父窗口,以讓父窗口把它的指針從數組中移除,這顯然具有很高的偶合性,不是我們想要的。因爲我們的所有子窗口都是標準的Windows對象,所以這使得我們有使用Windows消息的機會。我們首先要枚舉所有子窗口,然後發一個自定義的更新消息給它,並把我們的MemoryDC的指針作爲參數,具體例子代碼如下:
void CMyView::RenderView()
{
 …//其它更新操作
 ::EnumChildWindows(GetSafeHwnd(),CMyView::UpdateChildWnd,LPARAM(&memdc));
 …//其它更新操作
}
其中第二個參數是一個回調函數,你必須把它聲明成全局函數,或者類的static成員函數,這裏我們使用了後者。
BOOL CALLBACK CMyWnd::UpdateChildWnd(HWND hwnd, LPARAM lParam/*CDC* */)
{
 ::SendMessage(hwnd,WM_COMMAND,CHILDCMD_RENDER,lParam);
 return TRUE;
}
這裏我沒有使用自定義消息,而是發送標準的WM_COMMAND,這樣你可以給那個CmyAniWnd虛基類添加一個CWnd虛函數OnCommand(),然後在那裏面檢測如果wParam是CHILDCMD_RENDER的話,就調用純虛函數Render(以lParam作爲參數),子窗口派生類只要實現自己的Render函數就好,其它不用管了。
這裏還有一個要注意的問題就是繪製的順序問題,如果你想讓子窗口蓋住某些動畫,就應該先渲染那些動畫,然後渲染子窗口,反之亦反。
進階技巧--使用DIB
像素操作
 以上所有操作都侷限於標準GDI函數,如果我們要實現更進一步的操作,例如當傍晚你希望把整個畫面的顏色渲染能淡紅色調,晚上的時候你要把整個畫面變暗,早上再把它恢復到原來的亮度這些GDI都無法幫你做到。 如果想達到上述效果,就必須自己對像素的RGB值進行操作。
首先讓我們要得到一個Bitmap對象中的像素數據。讓我們看一下具體該怎麼操作。假設我們有一個mybmp是一個CBitmap對象(或者其派生類對象),下面的代碼把CBitmap中的像素取出:
BITMAP bm;
mybmp.GetBitmap(&bm);
BITMAPINFO binfo;
 ZeroMemory(&binfo,sizeof(BITMAPINFO));
 binfo.bmiHeader.biBitCount=24; file://24bit像素格式
 binfo.bmiHeader.biCompression=0;
 binfo.bmiHeader.biHeight=-bm.bmHeight;
 binfo.bmiHeader.biPlanes=1;
 binfo.bmiHeader.biSizeImage=0;
 binfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
 binfo.bmiHeader.biWidth=bm.bmWidth;

CClientDC dc(this);
 BYTE *pbuf;//用來存儲像素數據
 int linebytes=(bm.bmWidth*3+3)&(~3);//4字節對齊
 int size=linebytes*bm.bmHeight;
 pbuf=new BYTE[size];
 ::GetDIBits(dc,m_bmpSword,0,bm.bmHeight,pbuf,&binfo,DIB_RGB_COLORS);上面代碼執行後,我們的pbuf中就存儲了從mybmp拷貝而來的像素數據,而且是24bit模式的,這樣你就可以對所有這些像素進行你所需要的操作了,例如晚上了,你想把這個Bitmap變暗,我這裏粗略的把每個像素的RGB值都降低一半,可以使用下面的循環:
 for(int I;I<size;I++)
  pbuf[i]=pbuf[i]/2;
 得到了像素你就得到了一切,所有操作你都可以進行,例如上面提到的標準GDI不支持的Alpha通道。
 呵呵,像素交給你了,這樣我就放心了,那我走了…。。等等,你得到了這些像素,但是渲染時我們還是要使用標準GDI操作,所以好把這些像素設置回Bitmap對象中才行,好吧,這其實很簡單,繼續上面的代碼:
SetDIBits(dc,mybmp,0,bm.bmHeight,pbuf,&binfo,DIB_RGB_COLORS);
最後別忘了:
delete[] pbuf;
RLE壓縮
 現在的個人電腦內存容量已經非常大了,但是對某些人來說還顯得不夠(或者他們不願意浪費這些可憐的資源雖然它們可再生),例如在Diablo中一個骷髏兵從地上站起來的動畫爲96*96像素*100幀,所以你有很多這樣的動畫,最好壓縮一下.
RLE是遊戲常用的技巧,但是似乎已經超出了本文的範圍。而且這方面的文章很多,我這裏就不贅述了,留給你自己去進一步發掘.J
最後,更多編程文章,請訪問我個人網站http://www.diamondgarden.net/。
參考
華山論鍵
2001年上半年,我爲號稱國內最大武俠社區的笑傲江湖.com實現的圖形MUD客戶端軟件,基於上述技術。詳情請見http://hslj.Xajh.com
 
其它類庫
CIJLBitmap  一個CBitmap的派生類,可以Load BMP,JPG,GIF文件
NewImage Lib  純軟件2D圖像引擎,支持RLE,Alpha通道等,與GDI和DX無關,所謂的Open-ending。
以上兩個都可以到我個人網站http://www。diamondgarden。net下載。
鄭重聲明:本文所有使用的所有圖片,其版權都歸笑傲江湖.Com(http://www.xajh.com)武俠文化社區所有!不得擅自使用,否則責任自負,與本文作者無關。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章